Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
export(dataset_json)
export(datasetjson_example)
export(get_column_metadata)
export(read_dataset_dsjc)
export(read_dataset_json)
export(read_dataset_ndjson)
export(set_dataset_label)
Expand All @@ -16,8 +17,10 @@ export(set_originator)
export(set_source_system)
export(set_study_oid)
export(set_variable_attributes)
export(validate_dataset_dsjc)
export(validate_dataset_json)
export(validate_dataset_ndjson)
export(write_dataset_dsjc)
export(write_dataset_json)
export(write_dataset_ndjson)
importFrom(hms,as_hms)
Expand Down
159 changes: 159 additions & 0 deletions R/dsjc.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
#' Write out a Dataset JSON Compressed (DSJC) file
#'
#' Writes the compressed representation of Dataset JSON: the Dataset NDJSON
#' content of the dataset, compressed as a zLib stream. The format carries no
#' wrapper of its own - the file is the zLib stream and nothing else - and uses
#' the `.dsjc` extension.
#'
#' @details
#'
#' Rows are compressed as they are serialized, so writing a large dataset never
#' holds its uncompressed NDJSON in memory.
#'
#' The default `level = 9` follows the specification's recommendation for data
#' exchange. Note that the top of the range buys little: on a 26 MB dataset,
#' level 1 wrote in a fifth of the time for a file only 4% larger. If write
#' time matters more than the last few percent, a lower level is a reasonable
#' choice. Read time is unaffected by the level used to write.
#'
#' @param x datasetjson object
#' @param file File path to save the DSJC file. If not provided, the compressed
#' bytes are returned as a raw vector.
#' @param float_as_decimals If TRUE, write float variables as "decimal" data
#' types. This is an interoperability choice; it is not needed for precision,
#' as numbers are written at full precision either way, and setting it raises
#' a warning to that effect.
#' @param level zLib compression level, 0 (none) to 9 (maximum). Defaults to 9,
#' which the specification recommends for data exchange. Lower levels
#' compress faster and less.
#' @param digits Deprecated and ignored. See `write_dataset_json()`.
#'
#' @return NULL when writing to a file, otherwise a raw vector
#' @export
#'
#' @examples
#' ds_json <- dataset_json(
#' iris,
#' item_oid = "IG.IRIS",
#' name = "IRIS",
#' dataset_label = "Iris",
#' columns = iris_items
#' )
#' bytes <- write_dataset_dsjc(ds_json)
#'
#' # Write to disk
#' write_dataset_dsjc(ds_json, tempfile(fileext = ".dsjc"))
write_dataset_dsjc <- function(
x,
file,
float_as_decimals = FALSE,
level = 9L,
digits = NULL
) {
if (!is.numeric(level) || length(level) != 1 || is.na(level) ||
level < 0 || level > 9) {
stop("`level` must be a single number between 0 and 9", call. = FALSE)
}

prepared <- prepare_dataset_for_write(x, float_as_decimals, digits)

if (!missing(file)) {
if (!dir.exists(dirname(file))) {
stop("Folder supplied to `file` does not exist", call. = FALSE)
}
}

.Call(
C_write_dsjc,
prepared$meta,
prepared$columns,
prepared$data,
prepared$as_decimal,
as.integer(level),
if (missing(file)) NULL else file
)
}

#' Read a Dataset JSON Compressed (DSJC) file to a datasetjson object
#'
#' Reads the compressed representation of Dataset JSON: a zLib stream holding
#' Dataset NDJSON content. The object returned is the same one
#' `read_dataset_json()` and `read_dataset_ndjson()` return for the equivalent
#' uncompressed file, metadata attributes included.
#'
#' @param file File path or URL of a DSJC file, or a raw vector holding the
#' compressed bytes
#'
#' @return A dataframe with additional attributes attached containing the
#' DatasetJSON metadata.
#' @export
#'
#' @examples
#' # Read one of the example files shipped with the package
#' dm <- read_dataset_dsjc(datasetjson_example("dm.dsjc"))
#'
#' # Or from bytes held in memory
#' ds_json <- dataset_json(
#' iris,
#' item_oid = "IG.IRIS",
#' name = "IRIS",
#' dataset_label = "Iris",
#' columns = iris_items
#' )
#' dat <- read_dataset_dsjc(write_dataset_dsjc(ds_json))
read_dataset_dsjc <- function(file) {
if (is.raw(file)) {
parsed <- .Call(C_read_dsjc_raw, file)
} else if (path_is_url(file)) {
parsed <- .Call(C_read_dsjc_raw, read_raw_from_url(file))
} else if (length(file) == 1 && file.exists(file)) {
parsed <- .Call(C_read_dsjc_file, file)
} else {
stop("`file` must be a path or URL of an existing file, or a raw vector",
call. = FALSE)
}

build_datasetjson(parsed)
}

#' Validate a Dataset JSON Compressed (DSJC) file
#'
#' Decompresses the zLib stream and validates the Dataset NDJSON content it
#' holds, as `validate_dataset_ndjson()` does: the metadata object on line 1 is
#' checked against the Dataset NDJSON v1.1.0 schema, and each subsequent line
#' must be a JSON array carrying one value per declared column.
#'
#' @param x File path or URL of a DSJC file, or a raw vector holding the
#' compressed bytes
#'
#' @return A data frame of errors, empty when the file is valid
#' @export
#'
#' @examples
#' validate_dataset_dsjc(datasetjson_example("dm.dsjc"))
validate_dataset_dsjc <- function(x) {
if (is.raw(x)) {
bytes <- x
} else if (length(x) == 1 && path_is_url(x)) {
bytes <- read_raw_from_url(x)
} else if (length(x) == 1 && !is.raw(x) && file.exists(x)) {
bytes <- readBin(x, "raw", file.size(x))
} else {
stop("`x` must be a path or URL of an existing file, or a raw vector",
call. = FALSE)
}

txt <- tryCatch(
rawToChar(memDecompress(bytes, type = "gzip")),
error = function(e) NULL
)
if (is.null(txt)) {
warning("File contains errors!")
return(data.frame(
line = NA_integer_,
message = "Content is not a valid zLib compressed stream"
))
}

validate_dataset_ndjson(txt)
}
7 changes: 5 additions & 2 deletions R/read_dataset_ndjson.R
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,18 @@
#' @export
#'
#' @examples
#' # Read one of the example files shipped with the package
#' dm <- read_dataset_ndjson(datasetjson_example("dm.ndjson"))
#'
#' # Or from NDJSON text held in memory
#' ds_json <- dataset_json(
#' iris,
#' item_oid = "IG.IRIS",
#' name = "IRIS",
#' dataset_label = "Iris",
#' columns = iris_items
#' )
#' nd <- write_dataset_ndjson(ds_json)
#' dat <- read_dataset_ndjson(nd)
#' dat <- read_dataset_ndjson(write_dataset_ndjson(ds_json))
read_dataset_ndjson <- function(file) {
if (path_is_url(file)) {
parsed <- .Call(C_read_dsndjson_str, read_from_url(file))
Expand Down
22 changes: 22 additions & 0 deletions R/utils.R
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,28 @@ read_from_url <- function(path) {
paste(x, collapse = "\n")
}

#' Read binary data from a URL
#'
#' `read_from_url()` reads lines, which would corrupt a compressed stream. This
#' pulls the bytes through unchanged.
#'
#' @param path A URL
#'
#' @return A raw vector
#' @noRd
read_raw_from_url <- function(path) {
con <- url(path, open = "rb", method = "libcurl")
on.exit(close(con), add = TRUE)

chunks <- list()
repeat {
chunk <- readBin(con, "raw", n = 1048576L)
if (length(chunk) == 0L) break
chunks[[length(chunks) + 1L]] <- chunk
}
if (length(chunks) == 0L) raw() else unlist(chunks, use.names = FALSE)
}

#' Convert an dataframe into a named list of rows without NAs
#'
#' The variable attributes are stored as named lists within the output
Expand Down
6 changes: 4 additions & 2 deletions R/validate_dataset_json.R
Original file line number Diff line number Diff line change
Expand Up @@ -79,16 +79,18 @@ validate_dataset_json <- function(x) {
#' @export
#'
#' @examples
#' # Validate a file on disk
#' validate_dataset_ndjson(datasetjson_example("dm.ndjson"))
#'
#' ds_json <- dataset_json(
#' iris,
#' item_oid = "IG.IRIS",
#' name = "IRIS",
#' dataset_label = "Iris",
#' columns = iris_items
#' )
#' nd <- write_dataset_ndjson(ds_json)
#'
#' validate_dataset_ndjson(nd)
#' validate_dataset_ndjson(write_dataset_ndjson(ds_json))
validate_dataset_ndjson <- function(x) {
if (!requireNamespace("jsonvalidate", quietly = TRUE)) {
stop(
Expand Down
5 changes: 4 additions & 1 deletion _pkgdown.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,17 @@ reference:
- contents:
- starts_with("set")
- title: Read, Write, and Validate
desc: Functions to read, write, and validate Dataset JSON and Dataset NDJSON files
desc: Functions to read, write, and validate Dataset JSON, Dataset NDJSON and compressed (DSJC) files
- contents:
- read_dataset_json
- write_dataset_json
- validate_dataset_json
- read_dataset_ndjson
- write_dataset_ndjson
- validate_dataset_ndjson
- read_dataset_dsjc
- write_dataset_dsjc
- validate_dataset_dsjc
- title: Helper Functions
desc: Additional helpers
- contents:
Expand Down
10 changes: 10 additions & 0 deletions data-raw/data.R
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,16 @@ stopifnot(!inherits(
))
usethis::use_data(schema_ndjson_1_1_0, overwrite = TRUE)

# Example files shipped in inst/extdata for datasetjson_example()
#
# dm.json is the CDISC-published Dataset JSON example. The NDJSON and
# compressed forms are generated from it so all three stay in step.
dm_example <- read_dataset_json(
system.file("extdata", "dm.json", package = "datasetjson")
)
write_dataset_ndjson(dm_example, file.path("inst", "extdata", "dm.ndjson"))
write_dataset_dsjc(dm_example, file.path("inst", "extdata", "dm.dsjc"))

# Test data metadata

save_metadata <- function(df) {
Expand Down
Binary file added inst/extdata/dm.dsjc
Binary file not shown.
19 changes: 19 additions & 0 deletions inst/extdata/dm.ndjson
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{"datasetJSONCreationDateTime":"2026-09-02T17:21:59","datasetJSONVersion":"1.1.0","fileOID":"www.cdisc.org/StudyMSGv2/1/Define-XML_2.1.0/2024-11-11/dm","dbLastModifiedDateTime":"2020-08-21T09:14:29","originator":"CDISC SDTM MSG Team","sourceSystem":{"name":"SAS on X64_10PRO","version":"9.0401M7"},"studyOID":"cdisc.com/CDISCPILOT01","metaDataVersionOID":"MDV.MSGv2.0.SDTMIG.3.3.SDTM.1.7","metaDataRef":"define.xml","itemGroupOID":"IG.DM","records":18,"name":"DM","label":"Demographics","columns":[{"itemOID":"IT.DM.STUDYID","name":"STUDYID","label":"Study Identifier","dataType":"string","length":12,"keySequence":1},{"itemOID":"IT.DM.DOMAIN","name":"DOMAIN","label":"Domain Abbreviation","dataType":"string","length":2},{"itemOID":"IT.DM.USUBJID","name":"USUBJID","label":"Unique Subject Identifier","dataType":"string","length":8,"keySequence":2},{"itemOID":"IT.DM.SUBJID","name":"SUBJID","label":"Subject Identifier for the Study","dataType":"string","length":4},{"itemOID":"IT.DM.RFSTDTC","name":"RFSTDTC","label":"Subject Reference Start Date/Time","dataType":"date"},{"itemOID":"IT.DM.RFENDTC","name":"RFENDTC","label":"Subject Reference End Date/Time","dataType":"date"},{"itemOID":"IT.DM.RFXSTDTC","name":"RFXSTDTC","label":"Date/Time of First Study Treatment","dataType":"date"},{"itemOID":"IT.DM.RFXENDTC","name":"RFXENDTC","label":"Date/Time of Last Study Treatment","dataType":"date"},{"itemOID":"IT.DM.RFICDTC","name":"RFICDTC","label":"Date/Time of Informed Consent","dataType":"date"},{"itemOID":"IT.DM.RFPENDTC","name":"RFPENDTC","label":"Date/Time of End of Participation","dataType":"date"},{"itemOID":"IT.DM.DTHDTC","name":"DTHDTC","label":"Date/Time of Death","dataType":"date"},{"itemOID":"IT.DM.DTHFL","name":"DTHFL","label":"Subject Death Flag","dataType":"string","length":1},{"itemOID":"IT.DM.SITEID","name":"SITEID","label":"Study Site Identifier","dataType":"string","length":3},{"itemOID":"IT.DM.BRTHDTC","name":"BRTHDTC","label":"Date/Time of Birth","dataType":"date"},{"itemOID":"IT.DM.AGE","name":"AGE","label":"Age","dataType":"integer"},{"itemOID":"IT.DM.AGEU","name":"AGEU","label":"Age Units","dataType":"string","length":5},{"itemOID":"IT.DM.SEX","name":"SEX","label":"Sex","dataType":"string","length":1},{"itemOID":"IT.DM.RACE","name":"RACE","label":"Race","dataType":"string","length":41},{"itemOID":"IT.DM.ETHNIC","name":"ETHNIC","label":"Ethnicity","dataType":"string","length":22},{"itemOID":"IT.DM.ARMCD","name":"ARMCD","label":"Planned Arm Code","dataType":"string","length":8},{"itemOID":"IT.DM.ARM","name":"ARM","label":"Description of Planned Arm","dataType":"string","length":28},{"itemOID":"IT.DM.ACTARMCD","name":"ACTARMCD","label":"Actual Arm Code","dataType":"string","length":8},{"itemOID":"IT.DM.ACTARM","name":"ACTARM","label":"Description of Actual Arm","dataType":"string","length":28},{"itemOID":"IT.DM.ARMNRS","name":"ARMNRS","label":"Reason Arm and/or Actual Arm is Null","dataType":"string","length":14},{"itemOID":"IT.DM.ACTARMUD","name":"ACTARMUD","label":"Description of Unplanned Actual Arm","dataType":"string","length":200},{"itemOID":"IT.DM.COUNTRY","name":"COUNTRY","label":"Country","dataType":"string","length":3}]}
["CDISCPILOT01","DM","CDISC001","1115","2012-11-30","2013-01-23","2012-11-30","2013-01-23","2012-11-23","2013-05-20","","","701","1928",84,"YEARS","M","WHITE","NOT HISPANIC OR LATINO","ZAN_LOW","Zanomaline Low Dose (54 mg)","ZAN_LOW","Zanomaline Low Dose (54 mg)","","","USA"]
["CDISCPILOT01","DM","CDISC002","1211","2012-11-15","2013-01-14","2012-11-15","2013-01-12","2012-10-30","2013-01-14","2013-01-14","Y","701","1936",76,"YEARS","F","WHITE","NOT HISPANIC OR LATINO","ZAN_LOW","Zanomaline Low Dose (54 mg)","ZAN_LOW","Zanomaline Low Dose (54 mg)","","","USA"]
["CDISCPILOT01","DM","CDISC003","1302","2013-08-29","2013-11-05","2013-08-29","2013-11-05","2013-08-20","2014-02-13","","","701","1951",61,"YEARS","M","WHITE","NOT HISPANIC OR LATINO","ZAN_HIGH","Zanomaline High Dose (81 mg)","ZAN_HIGH","Zanomaline High Dose (81 mg)","","","USA"]
["CDISCPILOT01","DM","CDISC004","1345","2013-10-08","2014-03-18","2013-10-08","2014-03-18","2013-10-01","2014-03-18","","","701","1950",63,"YEARS","F","WHITE","NOT HISPANIC OR LATINO","PLACEBO","Placebo","PLACEBO","Placebo","","","USA"]
["CDISCPILOT01","DM","CDISC005","1383","2013-02-04","2013-08-06","2013-02-04","2013-08-06","2013-01-22","2013-08-06","","","701","1941",72,"YEARS","F","WHITE","NOT HISPANIC OR LATINO","ZAN_HIGH","Zanomaline High Dose (81 mg)","ZAN_HIGH","Zanomaline High Dose (81 mg)","","","USA"]
["CDISCPILOT01","DM","CDISC006","1429","2013-03-19","2013-04-30","2013-03-19","2013-04-30","2013-02-25","2013-04-30","","","701","1929",84,"YEARS","F","WHITE","NOT HISPANIC OR LATINO","ZAN_LOW","Zanomaline Low Dose (54 mg)","ZAN_LOW","Zanomaline Low Dose (54 mg)","","","USA"]
["CDISCPILOT01","DM","CDISC007","1444","2013-01-05","2013-02-13","2013-01-05","2013-02-12","2012-12-31","2013-06-20","","","701","1949",63,"YEARS","M","WHITE","HISPANIC OR LATINO","ZAN_HIGH","Zanomaline High Dose (81 mg)","ZAN_HIGH","Zanomaline High Dose (81 mg)","","","USA"]
["CDISCPILOT01","DM","CDISC008","1445","2014-05-11","2014-11-01","2014-05-11","2014-11-01","2014-05-01","2014-11-01","2014-11-01","Y","704","1939",75,"YEARS","M","MULTIPLE","NOT HISPANIC OR LATINO","PLACEBO","Placebo","PLACEBO","Placebo","","","USA"]
["CDISCPILOT01","DM","CDISC009","1087","2012-10-22","2013-04-28","2012-10-22","2013-04-28","2012-10-06","2013-04-28","","","708","1938",74,"YEARS","F","WHITE","NOT HISPANIC OR LATINO","PLACEBO","Placebo","PLACEBO","Placebo","","","USA"]
["CDISCPILOT01","DM","CDISC010","1236","2013-09-21","2013-09-26","2013-09-21","2013-09-21","2013-09-08","2013-09-26","","","708","1927",86,"YEARS","F","WHITE","NOT HISPANIC OR LATINO","ZAN_HIGH","Zanomaline High Dose (81 mg)","ZAN_HIGH","Zanomaline High Dose (81 mg)","","","USA"]
["CDISCPILOT01","DM","CDISC011","1336","2012-12-07","2013-06-05","2012-12-07","2013-06-05","2012-11-21","2013-07-05","","","708","1939",73,"YEARS","M","WHITE","NOT HISPANIC OR LATINO","ZAN_HIGH","Zanomaline High Dose (81 mg)","ZAN_HIGH","Zanomaline High Dose (81 mg)","","","USA"]
["CDISCPILOT01","DM","CDISC012","1378","2013-09-03","2014-01-28","2013-09-03","2014-01-28","2013-08-24","2014-01-28","","","708","1946",67,"YEARS","M","BLACK OR AFRICAN AMERICAN","NOT HISPANIC OR LATINO","PLACEBO","Placebo","PLACEBO","Placebo","","","USA"]
["CDISCPILOT01","DM","CDISC013","1083","2013-07-22","2013-08-03","2013-07-22","2013-08-01","2013-07-09","2013-08-03","2013-08-03","Y","710","1924",89,"YEARS","F","WHITE","NOT HISPANIC OR LATINO","PLACEBO","Placebo","PLACEBO","Placebo","","","USA"]
["CDISCPILOT01","DM","CDISC014","1012","2013-04-03","2013-05-02","2013-04-03","2013-04-29","2013-03-20","2013-09-18","","","711","1945",67,"YEARS","F","WHITE","NOT HISPANIC OR LATINO","ZAN_HIGH","Zanomaline High Dose (81 mg)","ZAN_HIGH","Zanomaline High Dose (81 mg)","","","USA"]
["CDISCPILOT01","DM","CDISC015","1022","","","","","2014-03-17","2014-03-17","","","711","1928",86,"YEARS","F","WHITE","NOT HISPANIC OR LATINO","","","","","SCREEN FAILURE","","USA"]
["CDISCPILOT01","DM","CDISC016","1143","2013-04-03","2013-06-01","2013-04-03","2013-05-30","2013-03-30","2013-09-22","","","711","1936",76,"YEARS","F","WHITE","NOT HISPANIC OR LATINO","ZAN_LOW","Zanomaline Low Dose (54 mg)","ZAN_LOW","Zanomaline Low Dose (54 mg)","","","USA"]
["CDISCPILOT01","DM","CDISC017","1250","2013-09-21","2014-02-08","2013-09-21","2014-01-31","2013-08-21","2014-03-08","","","718","1931",82,"YEARS","F","WHITE","HISPANIC OR LATINO","ZAN_LOW","Zanomaline Low Dose (54 mg)","ZAN_LOW","Zanomaline Low Dose (54 mg)","","","USA"]
["CDISCPILOT01","DM","CDISC018","1427","2012-12-17","2013-02-18","2012-12-17","2013-02-11","2012-12-13","2013-06-03","","","718","1938",74,"YEARS","F","BLACK OR AFRICAN AMERICAN","NOT HISPANIC OR LATINO","ZAN_HIGH","Zanomaline High Dose (81 mg)","ZAN_HIGH","Zanomaline High Dose (81 mg)","","","USA"]
36 changes: 36 additions & 0 deletions man/read_dataset_dsjc.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 5 additions & 2 deletions man/read_dataset_ndjson.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 24 additions & 0 deletions man/validate_dataset_dsjc.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading