diff --git a/.lintr b/.lintr index 37b9c939..54dd81ce 100644 --- a/.lintr +++ b/.lintr @@ -1,5 +1,5 @@ linters: linters_with_defaults( - cyclocomp_linter(complexity_limit = 18), + cyclocomp_linter(complexity_limit = 25), line_length_linter(120), object_usage_linter = NULL, object_name_linter = NULL, diff --git a/NEWS.md b/NEWS.md index 6536e96d..44bd475a 100644 --- a/NEWS.md +++ b/NEWS.md @@ -31,10 +31,13 @@ * New argument in `xportr_length()` allows selection between the length from metadata, as previously done, or from the calculated maximum length per variable when `length_source` is set to “data” (#91) +* Series of basic checks added to the `xportr_format()` function to ensure format lengths, prefixes are accurate for the variable type. Also to ensure that any numeric date/datetime/time variables have a format. (#164) + * Make `xportr_type()` drop factor levels when coercing variables * `xportr_length()` assigns the maximum length value instead of 200 for a character variable when the length is missing in the metadata (#207) + ## Deprecation and Breaking Changes * The `domain` argument for xportr functions will no longer be dynamically diff --git a/R/format.R b/R/format.R index e59e1c08..199caa77 100644 --- a/R/format.R +++ b/R/format.R @@ -8,6 +8,64 @@ #' #' @return Data frame with `SASformat` attributes for each variable. #' +#' @section Format Checks: This function carries out a series of basic +#' checks to ensure the formats being applied make sense. +#' +#' Note, the 'type' of message that is generated will depend on the value +#' passed to the `verbose` argument: with 'stop' producing an error, 'warn' +#' producing a warning, or 'message' producing a message. A value of 'none' +#' will not output any messages. +#' +#' 1) If the variable has a suffix of `DT`, `DTM`, `TM` (indicating a +#' numeric date/time variable) then a message will be shown if there is +#' no format associated with it. +#' +#' 2) If a variable is character then a message will be shown if there is +#' no `$` prefix in the associated format. +#' +#' 3) If a variable is character then a message will be shown if the +#' associated format has greater than 31 characters (excluding the `$`). +#' +#' 4) If a variable is numeric then a message will be shown if there is a +#' `$` prefix in the associated format. +#' +#' 5) If a variable is numeric then a message will be shown if the +#' associated format has greater than 32 characters. +#' +#' 6) All formats will be checked against a list of formats considered +#' 'standard' as part of an ADaM dataset. Note, however, this list is not +#' exhaustive (it would not be feasible to check all the functions +#' within the scope of this package). If the format is not found in the +#' 'standard' list, then a message is created advising the user to +#' check. +#' +#' | \strong{Format Name} | \strong{w Values} | \strong{d Values} | +#' |----------------------|-------------------|--------------------| +#' | w.d | 1 - 32 | ., 0 - 31 | +#' | $w. | 1 - 200 | | +#' | DATEw. | ., 5 - 11 | | +#' | DATETIMEw. | 7 - 40 | | +#' | DDMMYYw. | ., 2 - 10 | | +#' | HHMM. | | | +#' | MMDDYYw. | ., 2 - 10 | | +#' | TIMEw. | ., 2 - 20 | | +#' | WEEKDATEw. | ., 3 - 37 | | +#' | YYMMDDw. | ., 2 - 10 | | +#' | B8601DAw. | ., 8 - 10 | | +#' | B8601DTw.d | ., 15 - 26 | ., 0 - 6 | +#' | B8601TM. | | | +#' | IS8601DA. | | | +#' | IS8601TM. | | | +#' | E8601DAw. | ., 10 | | +#' | E8601DNw. | ., 10 | | +#' | E8601DTw.d | ., 16 - 26 | ., 0 - 6 | +#' | E8601DXw. | ., 20 - 35 | | +#' | E8601LXw. | ., 20 - 35 | | +#' | E8601LZw. | ., 9 - 20 | | +#' | E8601TMw.d | ., 8 - 15 | ., 0 - 6 | +#' | E8601TXw. | ., 9 - 20 | | +#' | E8601TZw.d | ., 9 - 20 | ., 0 - 6 | +#' #' @section Metadata: The argument passed in the 'metadata' argument can either #' be a metacore object, or a data.frame containing the data listed below. If #' metacore is used, no changes to options are required. @@ -44,6 +102,7 @@ xportr_format <- function(.df, metadata = NULL, domain = NULL, + verbose = NULL, metacore = deprecated()) { if (!missing(metacore)) { lifecycle::deprecate_stop( @@ -60,11 +119,18 @@ xportr_format <- function(.df, metadata <- metadata %||% attr(.df, "_xportr.df_metadata_") + # Verbose should use an explicit verbose option first, then the value set in + # metadata, and finally fall back to the option value + verbose <- verbose %||% + attr(.df, "_xportr.df_verbose_") %||% + getOption("xportr.length_verbose", "none") + ## End of common section assert_data_frame(.df) assert_string(domain, null.ok = TRUE) assert_metadata(metadata) + assert_choice(verbose, choices = .internal_verbose_choices) domain_name <- getOption("xportr.domain_name") format_name <- getOption("xportr.format_name") @@ -90,11 +156,91 @@ xportr_format <- function(.df, names(format) <- filtered_metadata[[variable_name]] + # vector of expected formats for clinical trials (usually character or date/time) + # https://documentation.sas.com/doc/en/pgmsascdc/9.4_3.5/leforinforref + # /n0p2fmevfgj470n17h4k9f27qjag.htm#n0wi06aq4kydlxn1uqc0p6eygu75 + + expected_formats <- .internal_format_list + + # w.d format for numeric variables + format_regex <- .internal_format_regex + + for (i in seq_len(ncol(.df))) { format_sas <- purrr::pluck(format, colnames(.df)[i]) if (is.na(format_sas) || is.null(format_sas)) { format_sas <- "" } + # series of checks for formats + + # check that any variables ending DT, DTM, TM have a format + if (isTRUE(grepl("DT$|DTM$|TM$", colnames(.df)[i])) && format_sas == "") { + message <- glue( + "(xportr::xportr_format) {encode_vars(colnames(.df)[i])} is expected to have a format but does not." + ) + xportr_logger(message, type = verbose) + } + + # remaining checks to be carried out if a format exists + if (format_sas != "") { + # if the variable is character + if (class(.df[[i]])[1] == "character") { + # character variable formats should start with a $ + if (isFALSE(grepl("^\\$", format_sas))) { + message <- glue( + "(xportr::xportr_format)", + " {encode_vars(colnames(.df)[i])} is a character variable", + " and should have a `$` prefix." + ) + xportr_logger(message, type = verbose) + } + # character variable formats should have length <= 31 (excluding the $) + if (nchar(gsub("\\.$", "", format_sas)) > 32) { + message <- glue( + "(xportr::xportr_format)", + " Format for character variable {encode_vars(colnames(.df)[i])}", + " should have length <= 31 (excluding `$`)." + ) + xportr_logger(message, type = verbose) + } + } + + # if the variable is numeric + if (class(.df[[i]])[1] == "numeric") { + # numeric variables should not start with a $ + if (isTRUE(grepl("^\\$", format_sas))) { + message <- glue( + "(xportr::xportr_format)", + " {encode_vars(colnames(.df)[i])} is a numeric variable and", + " should not have a `$` prefix." + ) + xportr_logger(message, type = verbose) + } + # numeric variable formats should have length <= 32 + if (nchar(gsub("\\.$", "", format_sas)) > 32) { + message <- glue( + "(xportr::xportr_format)", + " Format for numeric variable {encode_vars(colnames(.df)[i])}", + " should have length <= 32." + ) + xportr_logger(message, type = verbose) + } + } + + # check if the format is either one of the expected formats or follows the regular expression for w.d format + if ( + !(format_sas %in% toupper(expected_formats)) && + (stringr::str_detect(format_sas, pattern = format_regex) == FALSE) + ) { + message <- glue( + "(xportr::xportr_format)", + " Check format {encode_vars(format_sas)} for variable {encode_vars(colnames(.df)[i])}", + " - is this correct?" + ) + xportr_logger(message, type = verbose) + } + } + attr(.df[[i]], "format.sas") <- format_sas } diff --git a/R/utils-xportr.R b/R/utils-xportr.R index ac7cfd0c..fd689f06 100644 --- a/R/utils-xportr.R +++ b/R/utils-xportr.R @@ -173,6 +173,66 @@ xpt_validate_var_names <- function(varnames, return(err_cnd) } +#' Internal list of formats to check +#' @noRd +.internal_format_list <- c( + NA, + "", + paste("$", 1:200, ".", sep = ""), + paste("date", 5:11, ".", sep = ""), + paste("time", 2:20, ".", sep = ""), + paste("datetime", 7:40, ".", sep = ""), + paste("yymmdd", 2:10, ".", sep = ""), + paste("mmddyy", 2:10, ".", sep = ""), + paste("ddmmyy", 2:10, ".", sep = ""), + "E8601DA.", + "E8601DA10.", + "E8601DN.", + "E8601DN10.", + "E8601TM.", + paste("E8601TM", 8:15, ".", sep = ""), + paste("E8601TM", 8:15, ".", sort(rep(0:6, 8)), sep = ""), + "E8601TZ.", + paste("E8601TZ", 9:20, ".", sep = ""), + paste("E8601TZ", 9:20, ".", sort(rep(0:6, 12)), sep = ""), + "E8601TX.", + paste("E8601TX", 9:20, ".", sep = ""), + "E8601DT.", + paste("E8601DT", 16:26, ".", sep = ""), + paste("E8601DT", 16:26, ".", sort(rep(0:6, 11)), sep = ""), + "E8601LX.", + paste("E8601LX", 20:35, ".", sep = ""), + "E8601LZ.", + paste("E8601LZ", 9:20, ".", sep = ""), + "E8601DX.", + paste("E8601DX", 20:35, ".", sep = ""), + "B8601DT.", + paste("B8601DT", 15:26, ".", sep = ""), + paste("B8601DT", 15:26, ".", sort(rep(0:6, 12)), sep = ""), + "IS8601DA.", + "B8601DA.", + paste("B8601DA", 8:10, ".", sep = ""), + "weekdate.", + paste("weekdate", 3:37, ".", sep = ""), + "mmddyy.", + "ddmmyy.", + "yymmdd.", + "date.", + "time.", + "hhmm.", + "IS8601TM.", + "E8601TM.", + "B8601TM." +) + +#' Internal regex for format w.d +#' @noRd +.internal_format_regex <- paste( + sep = "|", + "^([1-9]|[12][0-9]|3[0-2])\\.$", + "^([1-9]|[12][0-9]|3[0-2])\\.([1-9]|[12][0-9]|3[0-1])$" +) + #' Validate Dataset Can be Written to xpt #' #' Function used to validate dataframes before they are sent to @@ -222,57 +282,9 @@ xpt_validate <- function(data) { ## The usual expected formats in clinical trials: characters, dates # Formats: https://documentation.sas.com/doc/en/pgmsascdc/9.4_3.5/leforinforref/n0zwce550r32van1fdd5yoixrk4d.htm - expected_formats <- c( - NA, - "", - paste("$", 1:200, ".", sep = ""), - paste("date", 5:11, ".", sep = ""), - paste("time", 2:20, ".", sep = ""), - paste("datetime", 7:40, ".", sep = ""), - paste("yymmdd", 2:10, ".", sep = ""), - paste("mmddyy", 2:10, ".", sep = ""), - paste("ddmmyy", 2:10, ".", sep = ""), - "E8601DA.", - "E8601DA10.", - "E8601DN.", - "E8601DN10.", - "E8601TM.", - paste0("E8601TM", 8:15, "."), - paste0("E8601TM", 8:15, ".", 0:6), - "E8601TZ.", - paste("E8601TZ", 9:20, "."), - paste("E8601TZ", 9:20, ".", 0:6), - "E8601TX.", - paste0("E8601TX", 9:20, "."), - "E8601DT.", - paste0("E8601DT", 16:26, "."), - paste0("E8601DT", 16:26, ".", 0:6), - "E8601LX.", - paste0("E8601LX", 20:35, "."), - "E8601LZ.", - paste0("E8601LZ", 9:20, "."), - "E8601DX.", - paste0("E8601DX", 20:35, "."), - "B8601DT.", - paste0("B8601DT", 15:26, "."), - paste0("B8601DT", 15:26, ".", 0:6), - "IS8601DA.", - "B8601DA.", - paste0("B8601DA", 8:10, "."), - "weekdate.", - paste0("weekdate", 3:37, "."), - "mmddyy.", - "ddmmyy.", - "yymmdd.", - "date.", - "time.", - "hhmm.", - "IS8601TM.", - "E8601TM.", - "B8601TM." - ) - format_regex <- "^([1-9]|[12][0-9]|3[0-2])\\.$|^([1-9]|[12][0-9]|3[0-2])\\.([1-9]|[12][0-9]|3[0-1])$" + expected_formats <- .internal_format_list + format_regex <- .internal_format_regex # 3.1 Invalid types is_valid <- toupper(formats) %in% toupper(expected_formats) | diff --git a/inst/WORDLIST b/inst/WORDLIST index be70b75c..c0d8ae96 100644 --- a/inst/WORDLIST +++ b/inst/WORDLIST @@ -8,12 +8,23 @@ BMI CDISC Codelist Completers +DATETIMEw +DATEw +DAw DCREASCD +DDMMYYw DM +DNw +DTw +DXw Didenko fda GSK +HHMM JPT +LXw +LZw +MMDDYYw MMSE ORCID PHUSE @@ -24,12 +35,18 @@ SASformat SDSP SDTM Standardisation +TIMEw +TMw TRTDUR +TXw +TZw Thanikachalam Trt Vignesh Vis +WEEKDATEw XPT +YYMMDDw acrf adrg bootswatch diff --git a/man/xportr_format.Rd b/man/xportr_format.Rd index 0c00da1b..e45f66dc 100644 --- a/man/xportr_format.Rd +++ b/man/xportr_format.Rd @@ -4,7 +4,13 @@ \alias{xportr_format} \title{Assign SAS Format} \usage{ -xportr_format(.df, metadata = NULL, domain = NULL, metacore = deprecated()) +xportr_format( + .df, + metadata = NULL, + domain = NULL, + verbose = NULL, + metacore = deprecated() +) } \arguments{ \item{.df}{A data frame of CDISC standard.} @@ -16,6 +22,10 @@ xportr_format(.df, metadata = NULL, domain = NULL, metacore = deprecated()) the metadata object. If none is passed, then name of the dataset passed as .df will be used.} +\item{verbose}{The action this function takes when an action is taken on the +dataset or function validation finds an issue. See 'Messaging' section for +details. Options are 'stop', 'warn', 'message', and 'none'} + \item{metacore}{\ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#deprecated}{\figure{lifecycle-deprecated.svg}{options: alt='[Deprecated]'}}}{\strong{[Deprecated]}} Previously used to pass metadata now renamed with \code{metadata}} } @@ -27,6 +37,61 @@ Assigns a SAS format from a variable level metadata to a given data frame. If no format is found for a given variable, it is set as an empty character vector. This is stored in the '\code{format.sas}' attribute. } +\section{Format Checks}{ + This function carries out a series of basic +checks to ensure the formats being applied make sense. + +Note, the 'type' of message that is generated will depend on the value +passed to the \code{verbose} argument: with 'stop' producing an error, 'warn' +producing a warning, or 'message' producing a message. A value of 'none' +will not output any messages. +\enumerate{ +\item If the variable has a suffix of \code{DT}, \code{DTM}, \code{TM} (indicating a +numeric date/time variable) then a message will be shown if there is +no format associated with it. +\item If a variable is character then a message will be shown if there is +no \code{$} prefix in the associated format. +\item If a variable is character then a message will be shown if the +associated format has greater than 31 characters (excluding the \code{$}). +\item If a variable is numeric then a message will be shown if there is a +\code{$} prefix in the associated format. +\item If a variable is numeric then a message will be shown if the +associated format has greater than 32 characters. +\item All formats will be checked against a list of formats considered +'standard' as part of an ADaM dataset. Note, however, this list is not +exhaustive (it would not be feasible to check all the functions +within the scope of this package). If the format is not found in the +'standard' list, then a message is created advising the user to +check. +}\tabular{lll}{ + \strong{Format Name} \tab \strong{w Values} \tab \strong{d Values} \cr + w.d \tab 1 - 32 \tab ., 0 - 31 \cr + $w. \tab 1 - 200 \tab \cr + DATEw. \tab ., 5 - 11 \tab \cr + DATETIMEw. \tab 7 - 40 \tab \cr + DDMMYYw. \tab ., 2 - 10 \tab \cr + HHMM. \tab \tab \cr + MMDDYYw. \tab ., 2 - 10 \tab \cr + TIMEw. \tab ., 2 - 20 \tab \cr + WEEKDATEw. \tab ., 3 - 37 \tab \cr + YYMMDDw. \tab ., 2 - 10 \tab \cr + B8601DAw. \tab ., 8 - 10 \tab \cr + B8601DTw.d \tab ., 15 - 26 \tab ., 0 - 6 \cr + B8601TM. \tab \tab \cr + IS8601DA. \tab \tab \cr + IS8601TM. \tab \tab \cr + E8601DAw. \tab ., 10 \tab \cr + E8601DNw. \tab ., 10 \tab \cr + E8601DTw.d \tab ., 16 - 26 \tab ., 0 - 6 \cr + E8601DXw. \tab ., 20 - 35 \tab \cr + E8601LXw. \tab ., 20 - 35 \tab \cr + E8601LZw. \tab ., 9 - 20 \tab \cr + E8601TMw.d \tab ., 8 - 15 \tab ., 0 - 6 \cr + E8601TXw. \tab ., 9 - 20 \tab \cr + E8601TZw.d \tab ., 9 - 20 \tab ., 0 - 6 \cr +} +} + \section{Metadata}{ The argument passed in the 'metadata' argument can either be a metacore object, or a data.frame containing the data listed below. If diff --git a/tests/testthat/test-format.R b/tests/testthat/test-format.R index 63b4ff92..7769cd10 100644 --- a/tests/testthat/test-format.R +++ b/tests/testthat/test-format.R @@ -34,3 +34,255 @@ test_that("xportr_format: Works as expected with only one domain in metadata", { expect_silent(xportr_format(adsl, metadata)) }) + +test_that("xportr_format: Variable ending in DT should produce a warning if no format", { + adsl <- data.frame( + USUBJID = c(1001, 1002, 1003), + BRTHDT = c(1, 1, 2) + ) + + metadata <- data.frame( + dataset = c("adsl", "adsl"), + variable = c("USUBJID", "BRTHDT"), + format = c(NA, NA) + ) + + expect_warning( + xportr_format(adsl, metadata, verbose = "warn"), + regexp = "(xportr::xportr_format) `BRTHDT` is expected to have a format but does not.", + fixed = TRUE + ) +}) + +test_that("xportr_format: Variable ending in TM should produce an error if no format", { + adsl <- data.frame( + USUBJID = c(1001, 1002, 1003), + BRTHTM = c(1, 1, 2) + ) + + metadata <- data.frame( + dataset = c("adsl", "adsl"), + variable = c("USUBJID", "BRTHTM"), + format = c(NA, NA) + ) + + expect_error( + xportr_format(adsl, metadata, verbose = "stop"), + regexp = "(xportr::xportr_format) `BRTHTM` is expected to have a format but does not.", + fixed = TRUE + ) +}) + +test_that("xportr_format: Variable ending in DTM should produce a warning if no format", { + adsl <- data.frame( + USUBJID = c(1001, 1002, 1003), + BRTHDTM = c(1, 1, 2) + ) + + metadata <- data.frame( + dataset = c("adsl", "adsl"), + variable = c("USUBJID", "BRTHDTM"), + format = c(NA, NA) + ) + + expect_warning( + xportr_format(adsl, metadata, verbose = "warn"), + regexp = "(xportr::xportr_format) `BRTHDTM` is expected to have a format but does not.", + fixed = TRUE + ) +}) + +test_that( + "xportr_format: If a variable is character then an error should be produced if format does not start with `$`", + { + adsl <- data.frame( + USUBJID = c("1001", "1002", "1003"), + BRTHDT = c(1, 1, 2) + ) + + metadata <- data.frame( + dataset = c("adsl", "adsl"), + variable = c("USUBJID", "BRTHDT"), + format = c("4.", "DATE9.") + ) + + expect_error( + xportr_format(adsl, metadata, verbose = "stop"), + regexp = "(xportr::xportr_format) `USUBJID` is a character variable and should have a `$` prefix.", + fixed = TRUE + ) + } +) + +test_that("xportr_format: If a variable is character then a warning should be produced if format is > 32 in length", { + adsl <- data.frame( + USUBJID = c("1001", "1002", "1003"), + BRTHDT = c(1, 1, 2) + ) + + metadata <- data.frame( + dataset = c("adsl", "adsl"), + variable = c("USUBJID", "BRTHDT"), + format = c("$AVERYLONGFORMATNAMEWHICHISGREATERTHAN32.", "DATE9.") + ) + + res <- evaluate_promise(xportr_format(adsl, metadata, verbose = "warn")) + + expect_equal( + res$warnings, + c( + "(xportr::xportr_format) Format for character variable `USUBJID` should have length <= 31 (excluding `$`).", + paste0( + "(xportr::xportr_format) Check format ", + "`$AVERYLONGFORMATNAMEWHICHISGREATERTHAN32.` for variable ", + "`USUBJID` - is this correct?" + ) + ) + ) +}) + +test_that("xportr_format: If a variable is numeric then an error should be produced if a format starts with `$`", { + adsl <- data.frame( + USUBJID = c(1001, 1002, 1003), + BRTHDT = c(1, 1, 2) + ) + + metadata <- data.frame( + dataset = c("adsl", "adsl"), + variable = c("USUBJID", "BRTHDT"), + format = c("$4.", "DATE9.") + ) + + expect_error( + xportr_format(adsl, metadata, verbose = "stop"), + regexp = "(xportr::xportr_format) `USUBJID` is a numeric variable and should not have a `$` prefix.", + fixed = TRUE + ) +}) + +test_that("xportr_format: If a variable is numeric then a warning should be produced if format is > 32 in length", { + adsl <- data.frame( + USUBJID = c(1001, 1002, 1003), + BRTHDT = c(1, 1, 2) + ) + + metadata <- data.frame( + dataset = c("adsl", "adsl"), + variable = c("USUBJID", "BRTHDT"), + format = c("AVERYLONGFORMATNAMEWHICHISGREATERTHAN32.", "DATE9.") + ) + + res <- evaluate_promise(xportr_format(adsl, metadata, verbose = "warn")) + + expect_equal( + res$warnings, + c( + "(xportr::xportr_format) Format for numeric variable `USUBJID` should have length <= 32.", + paste0( + "(xportr::xportr_format) Check format ", + "`AVERYLONGFORMATNAMEWHICHISGREATERTHAN32.` for variable ", + "`USUBJID` - is this correct?" + ) + ) + ) +}) + +test_that( + "xportr_format: If a format is not one of the expected formats identified, then a message should be produced", + { + adsl <- data.frame( + USUBJID = c(1001, 1002, 1003), + BRTHDT = c(1, 1, 2) + ) + + metadata <- data.frame( + dataset = c("adsl", "adsl"), + variable = c("USUBJID", "BRTHDT"), + format = c("NOTASTDFMT.", "DATE9.") + ) + + expect_message( + xportr_format(adsl, metadata, verbose = "message"), + regexp = "(xportr::xportr_format) Check format `NOTASTDFMT.` for variable `USUBJID` - is this correct?", + fixed = TRUE + ) + } +) + +test_that( + "xportr_format: Check for case-sensitivity. Using lowercase should not affect anything", + { + adsl <- data.frame( + USUBJID = c(1001, 1002, 1003), + BRTHDT = c(1, 1, 2), + BRTHTM = c(2, 2, 5) + ) + + metadata <- data.frame( + dataset = c("adsl", "adsl", "adsl"), + variable = c("USUBJID", "BRTHDT", "BRTHTM"), + format = c(NA, "date9.", "time5.") + ) + + expect_silent(xportr_format(adsl, metadata)) + } +) + +test_that( + "xportr_format: Check for case-sensitivity. Using mixed case should not affect anything", + { + adsl <- data.frame( + USUBJID = c(1001, 1002, 1003), + BRTHDT = c(1, 1, 2), + BRTHTM = c(2, 2, 5) + ) + + metadata <- data.frame( + dataset = c("adsl", "adsl", "adsl"), + variable = c("USUBJID", "BRTHDT", "BRTHTM"), + format = c(NA, "daTe9.", "TimE5.") + ) + + expect_silent(xportr_format(adsl, metadata)) + } +) + +test_that( + "xportr_format: Check for case-sensitivity. Using a mixture of case should not affect anything", + { + adsl <- data.frame( + USUBJID = c(1001, 1002, 1003), + BRTHDT = c(1, 1, 2), + BRTHTM = c(2, 2, 5), + BRTHDTM = c(3, 5, 7) + ) + + metadata <- data.frame( + dataset = c("adsl", "adsl", "adsl", "adsl"), + variable = c("USUBJID", "BRTHDT", "BRTHTM", "BRTHDTM"), + format = c(NA, "DATE9.", "time5.", "DaTeTiMe16.") + ) + + expect_silent(xportr_format(adsl, metadata)) + } +) + +test_that( + "xportr_format: If 'verbose' option is 'none', then no messaging should appear", + { + adsl <- data.frame( + USUBJID = c(1001, 1002, 1003), + BRTHDT = c(1, 1, 2) + ) + + metadata <- data.frame( + dataset = c("adsl", "adsl"), + variable = c("USUBJID", "BRTHDT"), + format = c("NOTASTDFMT.", NA) + ) + + expect_silent( + xportr_format(adsl, metadata, verbose = "none") + ) + } +)