Nothing
# Test suite for ksformat package
context("Format Creation (fnew)")
test_that("fnew works with discrete values", {
sex_fmt <- fnew(
"M" = "Male",
"F" = "Female",
.missing = "Unknown",
name = "sex"
)
expect_s3_class(sex_fmt, "ks_format")
expect_equal(sex_fmt$name, "sex")
expect_equal(sex_fmt$type, "character")
expect_equal(length(sex_fmt$mappings), 2)
fclear()
})
test_that("fnew auto-registers named formats", {
fnew("M" = "Male", "F" = "Female", name = "test_auto")
expect_true("test_auto" %in% ls(envir = ksformat:::.format_library))
fclear()
})
test_that("fnew without name does not register", {
fmt <- fnew("Y" = "Yes", "N" = "No")
expect_null(fmt$name)
# Should not be in library (no name to store under)
fclear()
})
context("Format Application (fput)")
test_that("fput handles missing values correctly", {
sex_fmt <- fnew(
"M" = "Male",
"F" = "Female",
.missing = "Unknown"
)
result <- fput(c("M", "F", NA, "X"), sex_fmt)
expect_equal(result[1], "Male")
expect_equal(result[2], "Female")
expect_equal(result[3], "Unknown")
expect_equal(result[4], "X")
})
test_that("fput preserves NA when keep_na = TRUE", {
sex_fmt <- fnew(
"M" = "Male",
"F" = "Female",
.missing = "Unknown"
)
result <- fput(c("M", NA), sex_fmt, keep_na = TRUE)
expect_equal(result[1], "Male")
expect_true(is.na(result[2]))
})
test_that("fput accepts format name from library", {
fnew("M" = "Male", "F" = "Female", .missing = "Unknown", name = "sex")
result <- fput(c("M", "F", NA), "sex")
expect_equal(result[1], "Male")
expect_equal(result[2], "Female")
expect_equal(result[3], "Unknown")
fclear()
})
context("SAS-like Functions (fputn, fputc, finputn, finputc)")
test_that("fputn applies numeric format by name", {
fparse(text = '
VALUE age (numeric)
[0, 18) = "Child"
[18, 65) = "Adult"
[65, HIGH] = "Senior"
;
')
result <- fputn(c(5, 25, 70), "age")
expect_equal(result, c("Child", "Adult", "Senior"))
fclear()
})
test_that("fputc applies character format by name", {
fnew("M" = "Male", "F" = "Female", name = "sex")
result <- fputc(c("M", "F"), "sex")
expect_equal(result, c("Male", "Female"))
fclear()
})
test_that("fputn warns for non-numeric format", {
fnew("M" = "Male", name = "char_fmt")
expect_warning(fputn("M", "char_fmt"), "not.*numeric")
fclear()
})
test_that("fputc warns for non-character format", {
fparse(text = '
VALUE nums (numeric)
[0, 10) = "Low"
;
')
expect_warning(fputc("5", "nums"), "not.*character")
fclear()
})
test_that("finputn applies numeric invalue by name", {
finput("Male" = 1, "Female" = 2, name = "sex_inv")
result <- finputn(c("Male", "Female"), "sex_inv")
expect_equal(result, c(1, 2))
fclear()
})
test_that("finputc applies character invalue by name", {
finput("Male" = "M", "Female" = "F",
name = "sex_inv", target_type = "character")
result <- finputc(c("Male", "Female"), "sex_inv")
expect_equal(result, c("M", "F"))
fclear()
})
test_that(".find_cheatsheet_path returns a path for known formats", {
html_result <- ksformat:::.find_cheatsheet_path("html")
expect_type(html_result, "character")
pdf_result <- ksformat:::.find_cheatsheet_path("pdf")
expect_type(pdf_result, "character")
})
# ===========================================================================
# Named vector support
# ===========================================================================
context("Named Vector Support")
test_that("fnew works with named character vector", {
fmt <- fnew(c(Male = "M", Female = "F"), name = "sex_vec")
expect_s3_class(fmt, "ks_format")
expect_equal(fmt$type, "character")
expect_equal(length(fmt$mappings), 2)
result <- fput(c("M", "F"), fmt)
expect_equal(result, c("Male", "Female"))
fclear()
})
test_that("fnew named vector with .missing and .other directives", {
fmt <- fnew(c(Male = "M", Female = "F", .missing = "Unknown", .other = "Other"))
result <- fput(c("M", "F", NA, "X"), fmt)
expect_equal(result, c("Male", "Female", "Unknown", "Other"))
})
test_that("fnew mixed: named vector + scalar ... args", {
fmt <- fnew(c(Male = "M", Female = "F"), .missing = "Unknown")
result <- fput(c("M", NA), fmt)
expect_equal(result, c("Male", "Unknown"))
})
test_that("fnew with named list", {
fmt <- fnew(list(Male = "M", Female = "F"))
result <- fput(c("M", "F"), fmt)
expect_equal(result, c("Male", "Female"))
})
test_that("fnew with multiple named vectors", {
fmt <- fnew(c(Male = "M", Female = "F"), c(Child = "C", Adult = "A"))
result <- fput(c("M", "F", "C", "A"), fmt)
expect_equal(result, c("Male", "Female", "Child", "Adult"))
})
test_that("fnew errors on unnamed vector without names", {
expect_error(fnew(c("M", "F")), "fully named")
})
test_that("fmap creates ks_fmap class with correct names/values", {
result <- fmap(c("A", "B"), c("Label-A", "Label-B"))
expect_s3_class(result, "ks_fmap")
expect_equal(names(result), c("A", "B"))
expect_equal(unclass(result), c(A = "Label-A", B = "Label-B"))
})
test_that("fmap errors on length mismatch", {
expect_error(fmap(c("A", "B"), c("X")), "same length")
})
test_that("fmap suppresses reversal for character type in fnew", {
# Without fmap: c(Male = "M") means "M" -> "Male" (auto-reversed)
fmt_default <- fnew(c(Male = "M", Female = "F"))
expect_equal(fput("M", fmt_default), "Male")
# With fmap: keys are input keys, values are output labels (no reversal)
fmt_fmap <- fnew(fmap(c("M", "F"), c("Male", "Female")))
expect_equal(fput("M", fmt_fmap), "Male")
expect_equal(fput("F", fmt_fmap), "Female")
})
test_that("fmap enables consistent pattern for all types in fnew", {
keys <- c("A", "B")
vals_chr <- c("Label-A", "Label-B")
vals_date <- as.Date(c("2021-01-01", "2021-06-15"))
# Same fmap(keys, values) pattern for both types
fmt_chr <- fnew(fmap(keys, vals_chr), type = "character")
fmt_date <- fnew(fmap(keys, vals_date), type = "Date")
expect_equal(fput("A", fmt_chr), "Label-A")
expect_equal(fput("B", fmt_chr), "Label-B")
expect_equal(fput("A", fmt_date), vals_date[1])
expect_equal(fput("B", fmt_date), vals_date[2])
})
test_that("fnew default auto-reversal still works without fmap", {
# character reverses
fmt_chr <- fnew(c(Male = "M"))
expect_equal(fput("M", fmt_chr), "Male")
# Date does not reverse
fmt_date <- fnew(c("M" = as.Date("2021-01-01")))
expect_equal(fput("M", fmt_date), as.Date("2021-01-01"))
})
test_that("finput works with named numeric vector", {
inv <- finput(c(Male = 1, Female = 2), name = "sex_inv_vec")
result <- finputn(c("Male", "Female"), "sex_inv_vec")
expect_equal(result, c(1, 2))
fclear()
})
test_that("finput works with named character vector", {
inv <- finput(c(Male = "M", Female = "F"),
name = "sex_inv_chr", target_type = "character")
result <- finputc(c("Male", "Female"), "sex_inv_chr")
expect_equal(result, c("M", "F"))
fclear()
})
test_that("fnew_bid works with named vector", {
bi <- fnew_bid(c(Male = "M", Female = "F"), name = "sex_bid_vec")
expect_s3_class(bi$format, "ks_format")
expect_s3_class(bi$invalue, "ks_invalue")
fwd <- fputc(c("M", "F"), "sex_bid_vec")
expect_equal(fwd, c("Male", "Female"))
rev <- finputc(c("Male", "Female"), "sex_bid_vec_inv")
expect_equal(rev, c("M", "F"))
fclear()
})
context("Invalue Creation and Application (finput)")
test_that(".invalue_apply reverses formatting", {
sex_inv <- finput(
"Male" = 1,
"Female" = 2,
name = "sex_inv"
)
result <- ksformat:::.invalue_apply(c("Male", "Female", "Unknown"), sex_inv)
expect_equal(result[1], 1)
expect_equal(result[2], 2)
expect_true(is.na(result[3]))
fclear()
})
test_that(".invalue_apply accepts name from library", {
finput("Male" = 1, "Female" = 2, name = "sex_inv")
result <- ksformat:::.invalue_apply(c("Male", "Female"), "sex_inv")
expect_equal(result, c(1, 2))
fclear()
})
test_that("finput defaults to numeric target_type", {
inv <- finput("X" = 10, "Y" = 20)
expect_equal(inv$target_type, "numeric")
})
test_that("INVALUE is numeric by default (no type specified)", {
txt <- '
INVALUE score_inv
"Low" = 1
"Medium" = 5
"High" = 10
;
'
result <- fparse(text = txt)
inv <- result$score_inv
expect_s3_class(inv, "ks_invalue")
expect_equal(inv$target_type, "numeric")
# Apply it
applied <- ksformat:::.invalue_apply(c("Low", "High"), inv)
expect_equal(applied, c(1, 10))
fclear()
})
context("Utility Functions")
test_that("is_missing detects various missing types", {
expect_true(is_missing(NA))
expect_true(is_missing(NaN))
expect_true(is_missing(""))
expect_false(is_missing("text"))
})
test_that("is_missing handles NULL", {
expect_equal(is_missing(NULL), logical(0))
})
context("Format Library (fprint, fclear)")
test_that("fprint and fclear work", {
fnew("M" = "Male", "F" = "Female", name = "test_sex")
expect_true("test_sex" %in% ls(envir = ksformat:::.format_library))
# fprint should produce output without errors
expect_output(fprint("test_sex"), "KS Format")
# fclear specific format
fclear("test_sex")
expect_false("test_sex" %in% ls(envir = ksformat:::.format_library))
})
test_that("fclear removes all formats", {
fnew("A" = "Alpha", name = "fmt1")
fnew("B" = "Beta", name = "fmt2")
fclear()
expect_equal(length(ls(envir = ksformat:::.format_library)), 0)
})
test_that("fprint shows format list", {
fnew("X" = "Ex", name = "test_print")
expect_output(fprint(), "Registered formats:")
expect_output(fprint(), "test_print")
fclear()
})
test_that("fnew_bid creates both format and invalue", {
bi <- fnew_bid(
"M" = "Male",
"F" = "Female",
name = "sex_bi"
)
expect_s3_class(bi$format, "ks_format")
expect_s3_class(bi$invalue, "ks_invalue")
# Test forward
formatted <- fput("M", bi$format)
expect_equal(formatted, "Male")
# Test reverse
original <- ksformat:::.invalue_apply("Male", bi$invalue)
expect_equal(original, "M")
fclear()
})
# ===================================================================
# SAS-like text parsing and export
# ===================================================================
context("Format Parsing (fparse)")
test_that("fparse parses a basic VALUE block", {
txt <- '
VALUE sex (character)
"M" = "Male"
"F" = "Female"
.missing = "Unknown"
.other = "N/A"
;
'
result <- fparse(text = txt)
expect_length(result, 1)
expect_true("sex" %in% names(result))
fmt <- result$sex
expect_s3_class(fmt, "ks_format")
expect_equal(fmt$name, "sex")
expect_equal(fmt$type, "character")
expect_equal(fmt$mappings[["M"]], "Male")
expect_equal(fmt$mappings[["F"]], "Female")
expect_equal(fmt$missing_label, "Unknown")
expect_equal(fmt$other_label, "N/A")
fclear()
})
test_that("fparse handles numeric ranges", {
txt <- '
VALUE age (numeric)
0 - 18 = "Child"
18 - 65 = "Adult"
65 - HIGH = "Senior"
.missing = "Age Unknown"
;
'
result <- fparse(text = txt)
fmt <- result$age
expect_s3_class(fmt, "ks_format")
expect_equal(fmt$type, "numeric")
expect_equal(fmt$missing_label, "Age Unknown")
expect_equal(length(fmt$mappings), 3)
expect_true("0,18,TRUE,FALSE" %in% names(fmt$mappings))
expect_true("65,Inf,TRUE,FALSE" %in% names(fmt$mappings))
fclear()
})
test_that("fparse handles LOW keyword", {
txt <- '
VALUE score (numeric)
LOW - 0 = "Negative"
0 - 100 = "Normal"
;
'
result <- fparse(text = txt)
fmt <- result$score
expect_true("-Inf,0,TRUE,FALSE" %in% names(fmt$mappings))
expect_equal(fmt$mappings[["-Inf,0,TRUE,FALSE"]], "Negative")
fclear()
})
test_that("fparse parses INVALUE block (numeric by default)", {
txt <- '
INVALUE score_inv
"Low" = 1
"Medium" = 5
"High" = 10
;
'
result <- fparse(text = txt)
inv <- result$score_inv
expect_s3_class(inv, "ks_invalue")
expect_equal(inv$target_type, "numeric")
fclear()
})
test_that("fparse handles INVALUE with explicit character type", {
txt <- '
INVALUE sex_inv (character)
"Male" = "M"
"Female" = "F"
;
'
result <- fparse(text = txt)
inv <- result$sex_inv
expect_s3_class(inv, "ks_invalue")
expect_equal(inv$target_type, "character")
expect_equal(inv$mappings[["Male"]], "M")
expect_equal(inv$mappings[["Female"]], "F")
fclear()
})
test_that("fparse auto-registers all formats", {
txt <- '
VALUE sex (character)
"M" = "Male"
"F" = "Female"
;
INVALUE score_inv
"Low" = 1
"High" = 10
;
'
fparse(text = txt)
expect_true("sex" %in% ls(envir = ksformat:::.format_library))
expect_true("score_inv" %in% ls(envir = ksformat:::.format_library))
fclear()
})
test_that("fparse handles multiple blocks", {
txt <- '
VALUE sex (character)
"M" = "Male"
"F" = "Female"
;
INVALUE sex_inv (character)
"Male" = "M"
"Female" = "F"
;
'
result <- fparse(text = txt)
expect_length(result, 2)
expect_s3_class(result$sex, "ks_format")
expect_s3_class(result$sex_inv, "ks_invalue")
fclear()
})
test_that("fparse skips comments", {
txt <- '
// This is a comment
/* Block comment */
VALUE status (character)
# Another comment
"A" = "Active"
* SAS-style comment
"I" = "Inactive"
;
'
result <- fparse(text = txt)
fmt <- result$status
expect_equal(length(fmt$mappings), 2)
expect_equal(fmt$mappings[["A"]], "Active")
fclear()
})
test_that("fparse auto-detects type from ranges", {
txt <- '
VALUE age
0 - 18 = "Child"
18 - 65 = "Adult"
;
'
result <- fparse(text = txt)
expect_equal(result$age$type, "numeric")
fclear()
})
test_that("fparse reads from file", {
tmp <- tempfile(fileext = ".fmt")
writeLines(c(
'VALUE color (character)',
' "R" = "Red"',
' "G" = "Green"',
';'
), tmp)
result <- fparse(file = tmp)
expect_s3_class(result$color, "ks_format")
expect_equal(result$color$mappings[["R"]], "Red")
unlink(tmp)
fclear()
})
test_that("fparse errors with no input", {
expect_error(fparse(), "Either .text. or .file. must be provided")
})
test_that("fparse handles hyphenated format names", {
fclear()
txt <- '
VALUE 014-001
"M" = "Male",
"F" = "Female",
.missing = "No sex";
'
result <- fparse(text = txt)
expect_length(result, 1)
expect_s3_class(result[["014-001"]], "ks_format")
expect_equal(result[["014-001"]]$mappings[["M"]], "Male")
# Verify auto-registration
out <- fput(c("M", "F", ""), "014-001")
expect_equal(out, c("Male", "Female", "No sex"))
fclear()
})
test_that("fparse errors with both inputs", {
expect_error(
fparse(text = "x", file = "y"),
"Only one of .text. or .file."
)
})
test_that("fparse handles unquoted values", {
txt <- '
VALUE yn (character)
Y = Yes
N = No
;
'
result <- fparse(text = txt)
fmt <- result$yn
expect_equal(fmt$mappings[["Y"]], "Yes")
expect_equal(fmt$mappings[["N"]], "No")
fclear()
})
context("Format Export (SAS-like syntax)")
test_that("fexport produces valid SAS-like text for VALUE", {
sex_fmt <- fnew(
"M" = "Male",
"F" = "Female",
.missing = "Unknown",
.other = "N/A",
name = "sex"
)
txt <- fexport(sex = sex_fmt)
expect_true(grepl("VALUE sex", txt))
expect_true(grepl('"M" = "Male"', txt))
expect_true(grepl('"F" = "Female"', txt))
expect_true(grepl('.missing = "Unknown"', txt))
expect_true(grepl('.other = "N/A"', txt))
expect_true(grepl(";", txt))
fclear()
})
test_that("fexport produces valid SAS-like text for INVALUE", {
sex_inv <- finput(
"Male" = 1,
"Female" = 2,
name = "sex_inv"
)
txt <- fexport(sex_inv = sex_inv)
expect_true(grepl("INVALUE sex_inv", txt))
expect_true(grepl('"Male" = "1"', txt))
fclear()
})
test_that("fexport roundtrips with fparse", {
original_fmt <- fnew(
"A" = "Active",
"I" = "Inactive",
.missing = "Unknown",
name = "status"
)
txt <- fexport(status = original_fmt)
fclear()
parsed <- fparse(text = txt)
expect_equal(parsed$status$mappings[["A"]], "Active")
expect_equal(parsed$status$mappings[["I"]], "Inactive")
expect_equal(parsed$status$missing_label, "Unknown")
fclear()
})
test_that("fexport writes to file", {
sex_fmt <- fnew("M" = "Male", "F" = "Female", name = "sex")
tmp <- tempfile(fileext = ".fmt")
result <- fexport(sex = sex_fmt, file = tmp)
expect_true(file.exists(tmp))
content <- readLines(tmp)
expect_true(any(grepl("VALUE sex", content)))
unlink(tmp)
fclear()
})
test_that("fexport handles multiple formats", {
fmt1 <- fnew("M" = "Male", "F" = "Female", name = "sex")
fmt2 <- fnew("Y" = "Yes", "N" = "No", name = "yn")
txt <- fexport(sex = fmt1, yn = fmt2)
expect_true(grepl("VALUE sex", txt))
expect_true(grepl("VALUE yn", txt))
fclear()
})
# ===================================================================
# Range bounds and decimal support
# ===================================================================
context("Range Bounds and Decimal Support")
test_that("fparse handles interval notation [low, high)", {
txt <- '
VALUE age (numeric)
[0, 18) = "Child"
[18, 65) = "Adult"
[65, HIGH] = "Senior"
;
'
result <- fparse(text = txt)
fmt <- result$age
expect_equal(fmt$type, "numeric")
expect_equal(length(fmt$mappings), 3)
expect_true("0,18,TRUE,FALSE" %in% names(fmt$mappings))
expect_true("65,Inf,TRUE,TRUE" %in% names(fmt$mappings))
fclear()
})
test_that("fparse handles exclusive lower bound (low, high]", {
txt <- '
VALUE score (numeric)
(0, 50] = "Low"
(50, 100] = "High"
;
'
result <- fparse(text = txt)
fmt <- result$score
expect_true("0,50,FALSE,TRUE" %in% names(fmt$mappings))
expect_true("50,100,FALSE,TRUE" %in% names(fmt$mappings))
fclear()
})
test_that("fparse handles fully open and fully closed intervals", {
txt <- '
VALUE test (numeric)
(0, 10) = "Open"
[10, 20] = "Closed"
;
'
result <- fparse(text = txt)
fmt <- result$test
expect_true("0,10,FALSE,FALSE" %in% names(fmt$mappings))
expect_true("10,20,TRUE,TRUE" %in% names(fmt$mappings))
fclear()
})
test_that("fparse handles decimal numbers in ranges", {
txt <- '
VALUE bmi (numeric)
[0, 18.5) = "Underweight"
[18.5, 25) = "Normal"
[25, 30) = "Overweight"
[30, HIGH] = "Obese"
;
'
result <- fparse(text = txt)
fmt <- result$bmi
expect_equal(length(fmt$mappings), 4)
expect_true("0,18.5,TRUE,FALSE" %in% names(fmt$mappings))
expect_true("18.5,25,TRUE,FALSE" %in% names(fmt$mappings))
expect_equal(fmt$mappings[["18.5,25,TRUE,FALSE"]], "Normal")
fclear()
})
test_that("fput matches ranges correctly with [low, high)", {
txt <- '
VALUE age (numeric)
[0, 18) = "Child"
[18, 65) = "Adult"
[65, HIGH] = "Senior"
;
'
result <- fparse(text = txt)
fmt <- result$age
applied <- fput(c(0, 5, 17.9, 18, 64.99, 65, 100), fmt)
expect_equal(applied[1], "Child")
expect_equal(applied[2], "Child")
expect_equal(applied[3], "Child")
expect_equal(applied[4], "Adult")
expect_equal(applied[5], "Adult")
expect_equal(applied[6], "Senior")
expect_equal(applied[7], "Senior")
fclear()
})
test_that("fput handles exclusive lower bound correctly", {
txt <- '
VALUE score (numeric)
(0, 50] = "Low"
(50, 100] = "High"
;
'
result <- fparse(text = txt)
fmt <- result$score
applied <- fput(c(0, 1, 50, 51, 100), fmt)
expect_equal(applied[1], "0")
expect_equal(applied[2], "Low")
expect_equal(applied[3], "Low")
expect_equal(applied[4], "High")
expect_equal(applied[5], "High")
fclear()
})
test_that("fput handles decimal ranges", {
txt <- '
VALUE bmi (numeric)
[0, 18.5) = "Underweight"
[18.5, 25) = "Normal"
[25, 30) = "Overweight"
;
'
result <- fparse(text = txt)
fmt <- result$bmi
applied <- fput(c(15.2, 18.5, 24.9, 25, 29.99), fmt)
expect_equal(applied[1], "Underweight")
expect_equal(applied[2], "Normal")
expect_equal(applied[3], "Normal")
expect_equal(applied[4], "Overweight")
expect_equal(applied[5], "Overweight")
fclear()
})
test_that("fput handles missing + ranges together", {
txt <- '
VALUE temp (numeric)
[LOW, 0) = "Freezing"
[0, 20) = "Cold"
[20, HIGH] = "Warm"
.missing = "No data"
;
'
result <- fparse(text = txt)
fmt <- result$temp
applied <- fput(c(-10, 0, 15, 25, NA), fmt)
expect_equal(applied[1], "Freezing")
expect_equal(applied[2], "Cold")
expect_equal(applied[3], "Cold")
expect_equal(applied[4], "Warm")
expect_equal(applied[5], "No data")
fclear()
})
test_that("fexport roundtrips interval notation", {
txt_in <- '
VALUE bmi (numeric)
[0, 18.5) = "Underweight"
[18.5, 25) = "Normal"
[25, HIGH] = "Overweight"
;
'
parsed <- fparse(text = txt_in)
exported <- fexport(bmi = parsed$bmi)
expect_true(grepl("\\[0, 18\\.5\\)", exported))
expect_true(grepl("\\[18\\.5, 25\\)", exported))
expect_true(grepl("\\[25, HIGH\\]", exported))
# Re-parse and verify roundtrip
fclear()
reparsed <- fparse(text = exported)
expect_equal(reparsed$bmi$mappings[["18.5,25,TRUE,FALSE"]], "Normal")
fclear()
})
test_that("range_spec supports inc_low and inc_high parameters", {
rs1 <- range_spec(0, 10, "test1")
expect_true(rs1$inc_low)
expect_false(rs1$inc_high)
rs2 <- range_spec(0, 10, "test2", inc_low = FALSE, inc_high = TRUE)
expect_false(rs2$inc_low)
expect_true(rs2$inc_high)
})
test_that("in_range respects bound inclusivity", {
rs_half_open <- range_spec(0, 10, "test")
rs_closed <- range_spec(0, 10, "test", inc_high = TRUE)
rs_open <- range_spec(0, 10, "test", inc_low = FALSE)
expect_true(in_range(0, rs_half_open))
expect_true(in_range(5, rs_half_open))
expect_false(in_range(10, rs_half_open))
expect_true(in_range(0, rs_closed))
expect_true(in_range(10, rs_closed))
expect_false(in_range(0, rs_open))
expect_true(in_range(5, rs_open))
expect_false(in_range(10, rs_open))
})
# ===================================================================
# franges()
# ===================================================================
context("franges")
test_that("franges extracts range entries from a numeric format", {
fmt <- fparse(text = '
VALUE age (numeric)
[0, 18) = "Child"
[18, 65) = "Adult"
[65, HIGH] = "Senior"
.missing = "Unknown"
;
')$age
df <- franges(fmt)
expect_s3_class(df, "data.frame")
expect_equal(nrow(df), 3L)
expect_equal(names(df), c("low", "high", "inc_low", "inc_high", "label"))
expect_equal(df$low, c(0, 18, 65))
expect_equal(df$high, c(18, 65, Inf))
expect_equal(df$inc_low, c(TRUE, TRUE, TRUE))
expect_equal(df$inc_high, c(FALSE, FALSE, TRUE))
expect_equal(df$label, c("Child", "Adult", "Senior"))
fclear()
})
test_that("franges accepts a registered format name", {
fparse(text = '
VALUE bmi (numeric)
[0, 25) = "Normal"
[25, HIGH] = "Overweight"
;
')
df <- franges("bmi")
expect_equal(nrow(df), 2L)
expect_equal(df$label, c("Normal", "Overweight"))
fclear()
})
test_that("franges returns empty data frame for discrete-only format", {
fmt <- fnew("M" = "Male", "F" = "Female", name = "sex")
df <- franges(fmt)
expect_s3_class(df, "data.frame")
expect_equal(nrow(df), 0L)
expect_equal(names(df), c("low", "high", "inc_low", "inc_high", "label"))
fclear()
})
test_that("franges errors on ks_invalue object", {
inv <- fparse(text = '
INVALUE yn
"Yes" = 1
"No" = 0
;
')$yn
expect_error(franges(inv), "ks_format")
fclear()
})
test_that("fmap_to_ranges returns bounds for matching labels", {
fparse(text = '
VALUE visit_ther (numeric)
[LOW, 1] = 0
[ 8, 22] = 2
[22, 36] = 4
[37, 50] = 6
;
')
res <- fmap_to_ranges(c(0, 2, 4, 6), "visit_ther")
expect_s3_class(res, "data.frame")
expect_equal(nrow(res), 4L)
expect_equal(res$low, c(-Inf, 8, 22, 37))
expect_equal(res$high, c(1, 22, 36, 50))
expect_true(all(res$inc_low) && all(res$inc_high))
fclear()
})
test_that("fmap_to_ranges returns NA for unmatched labels", {
fparse(text = '
VALUE v (numeric)
[0, 10) = 1
[10, 20) = 2
;
')
res <- fmap_to_ranges(c(1, 99), "v")
expect_equal(res$low, c(0, NA))
expect_equal(res$high, c(10, NA))
fclear()
})
# ===================================================================
# INVALUE numeric default
# ===================================================================
context("INVALUE Numeric Default")
test_that("INVALUE without type spec defaults to numeric", {
txt <- '
INVALUE age_inv
"Child" = 10
"Adult" = 40
"Senior" = 75
;
'
result <- fparse(text = txt)
inv <- result$age_inv
expect_equal(inv$target_type, "numeric")
applied <- ksformat:::.invalue_apply(c("Child", "Adult", "Senior"), inv)
expect_equal(applied, c(10, 40, 75))
expect_true(is.numeric(applied))
fclear()
})
test_that("finput creates numeric invalue by default", {
inv <- finput("Low" = 1, "Mid" = 5, "High" = 10)
expect_equal(inv$target_type, "numeric")
result <- ksformat:::.invalue_apply(c("Low", "High"), inv)
expect_equal(result, c(1, 10))
expect_true(is.numeric(result))
})
# ===================================================================
# fput_df
# ===================================================================
context("Data Frame Formatting")
test_that("fput_df works with fput", {
sex_fmt <- fnew("M" = "Male", "F" = "Female", .missing = "Unknown")
status_fmt <- fnew("A" = "Active", "I" = "Inactive", "P" = "Pending")
df <- data.frame(
sex = c("M", "F", "M", NA),
status = c("A", "I", "A", "P"),
stringsAsFactors = FALSE
)
result <- fput_df(df, sex = sex_fmt, status = status_fmt)
expect_true("sex_fmt" %in% names(result))
expect_true("status_fmt" %in% names(result))
expect_equal(result$sex_fmt, c("Male", "Female", "Male", "Unknown"))
})
# ===================================================================
# INVALUE export without (numeric) type
# ===================================================================
context("INVALUE Export")
test_that("INVALUE export omits numeric type (default)", {
inv <- finput("X" = 1, "Y" = 2, name = "test_inv")
txt <- fexport(test_inv = inv)
# Should say "INVALUE test_inv" without "(numeric)"
expect_true(grepl("INVALUE test_inv$", txt, perl = TRUE) ||
grepl("INVALUE test_inv\n", txt, fixed = TRUE))
expect_false(grepl("(numeric)", txt, fixed = TRUE))
fclear()
})
test_that("INVALUE export includes non-default type", {
inv <- finput("X" = "a", "Y" = "b",
name = "test_inv", target_type = "character")
txt <- fexport(test_inv = inv)
expect_true(grepl("(character)", txt, fixed = TRUE))
fclear()
})
# ===========================================================================
# Date/Time Format Tests
# ===========================================================================
test_that("fnew_date creates date format from SAS name", {
fmt <- fnew_date("DATE9.", name = "mydate")
expect_s3_class(fmt, "ks_format")
expect_equal(fmt$type, "date")
expect_equal(fmt$dt_pattern, "%d%b%Y")
expect_true(fmt$dt_toupper)
expect_equal(fmt$sas_name, "DATE9")
fclear()
})
test_that("fnew_date creates time format", {
fmt <- fnew_date("TIME8.", name = "mytime")
expect_s3_class(fmt, "ks_format")
expect_equal(fmt$type, "time")
expect_equal(fmt$dt_pattern, "%_H:%M:%S") # TIME has no leading zero
fclear()
})
test_that("fnew_date creates TOD format with leading zero", {
fmt <- fnew_date("TOD8.", name = "mytod")
expect_s3_class(fmt, "ks_format")
expect_equal(fmt$type, "time")
expect_equal(fmt$dt_pattern, "%H:%M:%S") # TOD has leading zero
fclear()
})
test_that("fnew_date creates datetime format", {
fmt <- fnew_date("DATETIME20.", name = "mydt")
expect_s3_class(fmt, "ks_format")
expect_equal(fmt$type, "datetime")
expect_equal(fmt$dt_pattern, "%d%b%Y:%H:%M:%S")
expect_true(fmt$dt_toupper)
fclear()
})
test_that("fnew_date with custom strftime pattern", {
fmt <- fnew_date("%Y/%m/%d", name = "custom", type = "date")
expect_equal(fmt$type, "date")
expect_equal(fmt$dt_pattern, "%Y/%m/%d")
expect_false(fmt$dt_toupper)
expect_null(fmt$sas_name)
fclear()
})
test_that("fnew_date auto-registers in library", {
fnew_date("DATE9.", name = "testdate")
expect_true("testdate" %in% ls(envir = ksformat:::.format_library))
fclear()
})
test_that("fnew_date requires type for custom patterns", {
expect_error(fnew_date("%Y-%m-%d"), "type")
fclear()
})
test_that("fnew_date handles default widths", {
fmt <- fnew_date("DATE.", name = "d1")
expect_equal(fmt$dt_pattern, "%d%b%Y") # DATE. defaults to DATE9.
fmt2 <- fnew_date("TIME.", name = "t1")
expect_equal(fmt2$dt_pattern, "%_H:%M:%S") # TIME. defaults to TIME8. (no leading zero)
fclear()
})
test_that("fnew_date uses R epoch (1970-01-01)", {
fmt <- fnew_date("DATE9.", name = "dorigin")
# dt_origin is no longer stored; epoch is always R's 1970-01-01
expect_null(fmt$dt_origin)
fclear()
})
test_that("fnew_date does not accept origin parameter", {
expect_error(fnew_date("DATE9.", name = "dsas", origin = "1960-01-01"))
fclear()
})
test_that("fput with Date objects and date format", {
fmt <- fnew_date("DATE9.", name = "datefmt")
result <- fput(as.Date("2020-01-15"), fmt)
expect_equal(result, "15JAN2020")
fclear()
})
test_that("fput with numeric R-epoch dates", {
fmt <- fnew_date("DATE9.", name = "datefmt")
# R epoch: days since 1970-01-01
r_date <- as.numeric(as.Date("2020-01-01"))
result <- fput(r_date, fmt)
expect_equal(result, "01JAN2020")
fclear()
})
test_that("fput with numeric R-epoch dates (default)", {
fmt <- fnew_date("DATE9.", name = "datefmt")
# R epoch: days since 1970-01-01
r_date <- as.numeric(as.Date("2020-01-01"))
result <- fput(r_date, fmt)
expect_equal(result, "01JAN2020")
fclear()
})
test_that("fput with MMDDYY10 format", {
fmt <- fnew_date("MMDDYY10.", name = "usfmt")
result <- fput(as.Date("2020-06-15"), fmt)
expect_equal(result, "06/15/2020")
fclear()
})
test_that("fput with YYMMDD10 format", {
fmt <- fnew_date("YYMMDD10.", name = "isofmt")
result <- fput(as.Date("2020-06-15"), fmt)
expect_equal(result, "2020-06-15")
fclear()
})
test_that("fput with DDMMYY10 format", {
fmt <- fnew_date("DDMMYY10.", name = "eufmt")
result <- fput(as.Date("2020-06-15"), fmt)
expect_equal(result, "15/06/2020")
fclear()
})
test_that("fput with MONYY7 format", {
fmt <- fnew_date("MONYY7.", name = "myfmt")
result <- fput(as.Date("2020-06-15"), fmt)
expect_equal(result, "JUN2020")
fclear()
})
test_that("fput with YEAR4 format", {
fmt <- fnew_date("YEAR4.", name = "yrfmt")
result <- fput(as.Date("2020-06-15"), fmt)
expect_equal(result, "2020")
fclear()
})
test_that("fput date format handles NA", {
fmt <- fnew_date("DATE9.", name = "dfmt", .missing = "No Date")
result <- fput(c(as.Date("2020-01-01"), NA), fmt)
expect_equal(result[2], "No Date")
fclear()
})
test_that("fput date format NA with keep_na", {
fmt <- fnew_date("DATE9.", name = "dfmt", .missing = "No Date")
result <- fput(c(as.Date("2020-01-01"), NA), fmt, keep_na = TRUE)
expect_true(is.na(result[2]))
fclear()
})
test_that("fput date with vector of dates", {
fmt <- fnew_date("DATE9.", name = "dfmt")
dates <- as.Date(c("2020-01-01", "2020-06-15", "2020-12-31"))
result <- fput(dates, fmt)
expect_equal(result, c("01JAN2020", "15JUN2020", "31DEC2020"))
fclear()
})
test_that("fput time format with numeric seconds", {
fmt <- fnew_date("TIME8.", name = "tfmt")
result <- fput(c(0, 3600, 45000), fmt)
expect_equal(result, c("0:00:00", "1:00:00", "12:30:00")) # TIME: no leading zero
fclear()
})
test_that("fput time format TIME5 (H:MM)", {
fmt <- fnew_date("TIME5.", name = "t5fmt")
result <- fput(c(0, 45000), fmt)
expect_equal(result, c("0:00", "12:30")) # TIME: no leading zero
fclear()
})
test_that("fput TOD format with leading zero", {
fmt <- fnew_date("TOD8.", name = "todfmt")
result <- fput(c(0, 3600, 45000), fmt)
expect_equal(result, c("00:00:00", "01:00:00", "12:30:00")) # TOD: leading zero
fclear()
})
test_that("fput dynamic TIME widths work", {
result4 <- fputn(c(0, 3600, 45000), "time4.")
expect_equal(result4, c("0", "1", "12")) # width < 5 → hours only
result6 <- fputn(c(0, 3600, 45000), "time6.")
expect_equal(result6, c("0:00", "1:00", "12:30")) # 5 ≤ width < 8 → H:MM
result9 <- fputn(c(0, 3600, 45000), "time9.")
expect_equal(result9, c("0:00:00", "1:00:00", "12:30:00")) # 8 ≤ width < 11
fclear()
})
test_that("fput datetime format with POSIXct", {
fmt <- fnew_date("DATETIME20.", name = "dtfmt")
dt <- as.POSIXct("2020-01-15 12:30:45", tz = "UTC")
result <- fput(dt, fmt)
expect_equal(result, "15JAN2020:12:30:45")
fclear()
})
test_that("fput datetime with R-epoch numeric", {
fmt <- fnew_date("DATETIME20.", name = "dtfmt")
# R epoch: seconds since 1970-01-01 00:00:00
r_dt <- as.numeric(as.POSIXct("2020-01-01 12:00:00", tz = "UTC"))
result <- fput(r_dt, fmt)
expect_equal(result, "01JAN2020:12:00:00")
fclear()
})
test_that("fputn auto-resolves SAS format names", {
# No need to create format first
result <- fputn(as.Date("2020-01-01"), "DATE9.")
expect_equal(result, "01JAN2020")
fclear()
})
test_that("fputn with R-epoch numeric dates", {
r_date <- as.numeric(as.Date("2020-06-15"))
result <- fputn(r_date, "MMDDYY10.")
expect_equal(result, "06/15/2020")
fclear()
})
test_that("fputn with R-epoch numeric dates (default)", {
r_date <- as.numeric(as.Date("2020-06-15"))
result <- fputn(r_date, "MMDDYY10.")
expect_equal(result, "06/15/2020")
fclear()
})
test_that("fputc with date format", {
result <- fputc("2020-01-01", "DATE9.")
expect_equal(result, "01JAN2020")
fclear()
})
test_that("fputn with time format", {
result <- fputn(45000, "TIME8.")
expect_equal(result, "12:30:00")
fclear()
})
test_that("fputn and fputc don't warn for datetime types", {
expect_silent(fputn(as.Date("2020-01-01"), "DATE9."))
expect_silent(fputc("2020-01-01", "YYMMDD10."))
fclear()
})
test_that("fput with NULL returns empty character", {
fmt <- fnew_date("DATE9.", name = "dfmt")
expect_equal(fput(NULL, fmt), character(0))
fclear()
})
test_that("fput QTR format", {
fmt <- fnew_date("QTR.", name = "qfmt")
dates <- as.Date(c("2020-01-15", "2020-04-15", "2020-07-15", "2020-10-15"))
result <- fput(dates, fmt)
expect_equal(result, c("1", "2", "3", "4"))
fclear()
})
test_that("print.ks_format for datetime format", {
fmt <- fnew_date("DATE9.", name = "pfmt")
out <- capture.output(print(fmt))
expect_true(any(grepl("date", out)))
expect_true(any(grepl("Pattern:", out)))
expect_true(any(grepl("DATE9", out)))
fclear()
})
test_that("fprint shows datetime format info", {
fnew_date("DATE9.", name = "printdate")
out <- capture.output(fprint())
expect_true(any(grepl("date", out)))
fclear()
})
# ===========================================================================
# Multilabel Format Tests
# ===========================================================================
test_that("fnew with multilabel flag", {
fmt <- fnew("1" = "A", "2" = "B", name = "ml_test",
type = "character", multilabel = TRUE)
expect_true(fmt$multilabel)
fclear()
})
test_that("print.ks_format shows multilabel flag", {
fmt <- fnew("1" = "A", "2" = "B", name = "ml_test",
type = "character", multilabel = TRUE)
out <- capture.output(print(fmt))
expect_true(any(grepl("multilabel", out)))
fclear()
})
test_that("fput_all returns all matching labels for multilabel ranges", {
fmt <- fnew(
"0,5,TRUE,TRUE" = "Infant",
"6,11,TRUE,TRUE" = "Child",
"12,17,TRUE,TRUE" = "Teen",
"0,17,TRUE,TRUE" = "Minor",
"18,64,TRUE,TRUE" = "Adult",
"65,Inf,TRUE,TRUE" = "Senior",
name = "age_ml", type = "numeric", multilabel = TRUE
)
result <- fput_all(c(3, 15, 25), fmt)
expect_type(result, "list")
expect_length(result, 3)
expect_true("Infant" %in% result[[1]])
expect_true("Minor" %in% result[[1]])
expect_true("Teen" %in% result[[2]])
expect_true("Minor" %in% result[[2]])
expect_equal(result[[3]], "Adult")
fclear()
})
test_that("fput_all handles NA values", {
fmt <- fnew(
"0,10,TRUE,TRUE" = "Low",
"0,100,TRUE,TRUE" = "All",
.missing = "Unknown",
name = "ml_na", type = "numeric", multilabel = TRUE
)
result <- fput_all(c(5, NA), fmt)
expect_true("Low" %in% result[[1]])
expect_true("All" %in% result[[1]])
expect_equal(result[[2]], "Unknown")
fclear()
})
test_that("fput_all with keep_na=TRUE", {
fmt <- fnew(
"0,10,TRUE,TRUE" = "Low",
.missing = "Unknown",
name = "ml_kna", type = "numeric"
)
result <- fput_all(c(5, NA), fmt, keep_na = TRUE)
expect_true(is.na(result[[2]]))
fclear()
})
test_that("fput_all with NULL returns empty list", {
fmt <- fnew("1" = "A", name = "ml_null")
result <- fput_all(NULL, fmt)
expect_equal(result, list())
fclear()
})
test_that("fput_all with other_label", {
fmt <- fnew("1" = "A", .other = "Unknown",
name = "ml_other", type = "character")
result <- fput_all(c("1", "X"), fmt)
expect_equal(result[[1]], "A")
expect_equal(result[[2]], "Unknown")
fclear()
})
test_that("fput returns first match for multilabel format", {
fmt <- fnew(
"0,5,TRUE,TRUE" = "Infant",
"0,17,TRUE,TRUE" = "Minor",
name = "ml_first", type = "numeric", multilabel = TRUE
)
result <- fput(3, fmt)
# fput should return first match only
expect_length(result, 1)
expect_true(result %in% c("Infant", "Minor"))
fclear()
})
test_that("fput_all with format name string", {
fnew("0,10,TRUE,TRUE" = "Low", "0,100,TRUE,TRUE" = "All",
name = "ml_name", type = "numeric", multilabel = TRUE)
result <- fput_all(5, "ml_name")
expect_true("Low" %in% result[[1]])
expect_true("All" %in% result[[1]])
fclear()
})
test_that("fput_all with date format returns list", {
fnew_date("DATE9.", name = "dt_ml")
result <- fput_all(as.Date("2020-01-01"), "dt_ml")
expect_type(result, "list")
expect_equal(result[[1]], "01JAN2020")
fclear()
})
# ===========================================================================
# Parser: Date/Time and Multilabel Support
# ===========================================================================
test_that("fparse: date format block", {
txt <- '
VALUE mydate (date)
pattern = "DATE9."
;
'
result <- fparse(text = txt)
expect_s3_class(result$mydate, "ks_format")
expect_equal(result$mydate$type, "date")
expect_equal(result$mydate$dt_pattern, "%d%b%Y")
fclear()
})
test_that("fparse: time format block", {
txt <- '
VALUE mytime (time)
pattern = "TIME8."
;
'
result <- fparse(text = txt)
expect_equal(result$mytime$type, "time")
fclear()
})
test_that("fparse: datetime format block", {
txt <- '
VALUE mydt (datetime)
pattern = "DATETIME20."
;
'
result <- fparse(text = txt)
expect_equal(result$mydt$type, "datetime")
fclear()
})
test_that("fparse: date format with .missing", {
txt <- '
VALUE mydate (date)
pattern = "DATE9."
.missing = "No Date"
;
'
result <- fparse(text = txt)
expect_equal(result$mydate$missing_label, "No Date")
fclear()
})
test_that("fparse: date format block without pattern errors", {
txt <- '
VALUE mydate (date)
"x" = "y"
;
'
expect_error(fparse(text = txt), "pattern")
fclear()
})
test_that("fparse: multilabel block", {
txt <- '
VALUE ageml (numeric, multilabel)
[0, 5] = "Infant"
[0, 17] = "Minor"
[18, 64] = "Adult"
;
'
result <- fparse(text = txt)
expect_true(result$ageml$multilabel)
expect_equal(result$ageml$type, "numeric")
fclear()
})
test_that("fparse: multilabel without type", {
txt <- '
VALUE test (multilabel)
"A" = "Label A"
"B" = "Label B"
;
'
result <- fparse(text = txt)
expect_true(result$test$multilabel)
fclear()
})
test_that("fexport: datetime format round-trip", {
fnew_date("DATE9.", name = "rtdate")
fmt <- ksformat:::.format_get("rtdate")
txt <- fexport(rtdate = fmt)
expect_true(grepl("VALUE rtdate (date)", txt, fixed = TRUE))
expect_true(grepl("pattern = \"DATE9.\"", txt, fixed = TRUE))
fclear()
})
test_that("fexport: multilabel format includes flag", {
fmt <- fnew("1" = "A", "2" = "B", name = "ml_exp",
type = "character", multilabel = TRUE)
txt <- fexport(ml_exp = fmt)
expect_true(grepl("multilabel", txt))
fclear()
})
test_that("fexport: date format parse round-trip", {
txt_in <- '
VALUE mydate (date)
pattern = "DATE9."
;
'
fparse(text = txt_in)
fmt <- ksformat:::.format_get("mydate")
txt_out <- fexport(mydate = fmt)
expect_true(grepl("pattern", txt_out))
expect_true(grepl("DATE9", txt_out))
# Round-trip: parse the exported text
fclear()
result <- fparse(text = txt_out)
expect_equal(result$mydate$type, "date")
fclear()
})
test_that("custom strftime date format with fput", {
fmt <- fnew_date("%d.%m.%Y", name = "ru_date", type = "date")
result <- fput(as.Date("2020-06-15"), fmt)
expect_equal(result, "15.06.2020")
fclear()
})
# ===========================================================================
# Case-Insensitive Matching Tests
# ===========================================================================
test_that("ignore_case in fnew creates format with flag", {
fmt <- fnew("M" = "Male", "F" = "Female", ignore_case = TRUE)
expect_true(fmt$ignore_case)
fclear()
})
test_that("fput with ignore_case matches case-insensitively", {
fmt <- fnew("M" = "Male", "F" = "Female", ignore_case = TRUE)
result <- fput(c("m", "f", "M", "F"), fmt)
expect_equal(result, c("Male", "Female", "Male", "Female"))
fclear()
})
test_that("fput without ignore_case is case-sensitive (default)", {
fmt <- fnew("M" = "Male", "F" = "Female")
result <- fput(c("m", "M"), fmt)
expect_equal(result, c("m", "Male"))
fclear()
})
test_that("fput_all with ignore_case", {
fmt <- fnew("a" = "Alpha", "b" = "Beta",
type = "character", ignore_case = TRUE, multilabel = TRUE)
result <- fput_all(c("A", "b"), fmt)
expect_equal(result[[1]], "Alpha")
expect_equal(result[[2]], "Beta")
fclear()
})
test_that("fparse with nocase keyword", {
txt <- '
VALUE sex (character, nocase)
"M" = "Male"
"F" = "Female"
;
'
result <- fparse(text = txt)
expect_true(result$sex$ignore_case)
# Test case-insensitive apply
expect_equal(fput(c("m", "F"), "sex"), c("Male", "Female"))
fclear()
})
test_that("fexport with nocase flag", {
fmt <- fnew("M" = "Male", "F" = "Female",
name = "sex_nc", type = "character", ignore_case = TRUE)
txt <- fexport(sex_nc = fmt)
expect_true(grepl("nocase", txt))
fclear()
})
test_that("print.ks_format shows nocase flag", {
fmt <- fnew("M" = "Male", name = "nc_test", ignore_case = TRUE)
out <- capture.output(print(fmt))
expect_true(any(grepl("nocase", out)))
fclear()
})
test_that("ignore_case combined with .other and .missing", {
fmt <- fnew("Y" = "Yes", "N" = "No",
.missing = "Unknown", .other = "Invalid",
ignore_case = TRUE)
result <- fput(c("y", "N", NA, "x"), fmt)
expect_equal(result, c("Yes", "No", "Unknown", "Invalid"))
fclear()
})
# ===========================================================================
# Expression Label Tests
# ===========================================================================
test_that("expression label detected by .is_expr_label", {
expect_true(ksformat:::.is_expr_label("sprintf('%s', .x1)"))
expect_true(ksformat:::.is_expr_label("paste0(.x1, '%')"))
expect_false(ksformat:::.is_expr_label("Male"))
expect_false(ksformat:::.is_expr_label("x1")) # no dot
fclear()
})
test_that("fput with expression label and positional arg", {
fmt <- fnew("n" = "sprintf('%s', .x1)",
name = "stat", type = "character")
result <- fput(c("n", "n"), fmt, c(42, 100))
expect_equal(result, c("42", "100"))
fclear()
})
test_that("fput with multiple expression labels", {
fmt <- fnew(
"n" = "sprintf('%s', .x1)",
"pct" = "sprintf('%.1f%%', .x1 * 100)",
name = "stat2", type = "character"
)
result <- fput(c("n", "pct", "n", "pct"), fmt, c(42, 0.15, 100, 0.255))
expect_equal(result, c("42", "15.0%", "100", "25.5%"))
fclear()
})
test_that("fput with mixed static and expression labels", {
fmt <- fnew(
"label" = "LABEL",
"n" = "sprintf('%s', .x1)",
name = "mixed", type = "character"
)
result <- fput(c("label", "n", "label"), fmt, c(0, 42, 0))
expect_equal(result, c("LABEL", "42", "LABEL"))
fclear()
})
test_that("fput expression with two extra args", {
fmt <- fnew(
"ratio" = "sprintf('%s/%s', .x1, .x2)",
name = "two_arg", type = "character"
)
result <- fput(c("ratio", "ratio"), fmt, c(3, 7), c(10, 20))
expect_equal(result, c("3/10", "7/20"))
fclear()
})
test_that("fput expression with scalar extra arg (recycled)", {
fmt <- fnew(
"val" = "sprintf('%s (N=%s)', .x1, .x2)",
name = "scalar", type = "character"
)
result <- fput(c("val", "val"), fmt, c(42, 55), 100)
expect_equal(result, c("42 (N=100)", "55 (N=100)"))
fclear()
})
test_that("fput expression with no extra args warns", {
fmt <- fnew("n" = "sprintf('%s', .x1)", type = "character")
expect_warning(result <- fput("n", fmt))
expect_true(is.na(result))
fclear()
})
test_that("fput expression preserves NA handling", {
fmt <- fnew(
"n" = "sprintf('%s', .x1)",
.missing = "MISS",
name = "expr_na", type = "character"
)
result <- fput(c("n", NA), fmt, c(42, 0))
expect_equal(result[1], "42")
expect_equal(result[2], "MISS")
fclear()
})
test_that("fputn passes extra args to expression labels", {
fnew("1" = "sprintf('%.1f', .x1)", name = "expr_num", type = "numeric")
result <- fputn(1, "expr_num", 3.14159)
expect_equal(result, "3.1")
fclear()
})
test_that("fputc passes extra args to expression labels", {
fnew("a" = "paste0(.x1, '!')", name = "expr_chr", type = "character")
result <- fputc("a", "expr_chr", "hello")
expect_equal(result, "hello!")
fclear()
})
test_that("fput_all with expression labels", {
fmt <- fnew(
"0,100,TRUE,TRUE" = "sprintf('Score: %s', .x1)",
name = "expr_ml", type = "numeric"
)
result <- fput_all(50, fmt, 99)
expect_type(result, "list")
expect_equal(result[[1]], "Score: 99")
fclear()
})
test_that("expression with ifelse", {
fmt <- fnew(
"val" = "ifelse(.x1 > 0, paste0('+', .x1), as.character(.x1))",
name = "sign_fmt", type = "character"
)
result <- fput(c("val", "val", "val"), fmt, c(5, 0, -3))
expect_equal(result, c("+5", "0", "-3"))
fclear()
})
test_that("expression with .other label", {
fmt <- fnew(
"known" = "YES",
.other = "sprintf('Unknown: %s', .x1)",
name = "other_expr", type = "character"
)
result <- fput(c("known", "xyz"), fmt, c(1, 2))
expect_equal(result, c("YES", "Unknown: 2"))
fclear()
})
test_that("fparse roundtrip preserves expression labels", {
txt <- '
VALUE stat (character)
"n" = "sprintf(\'%s\', .x1)"
"pct" = "sprintf(\'%.1f%%\', .x1 * 100)"
;
'
result <- fparse(text = txt)
expect_equal(result$stat$mappings[["n"]], "sprintf('%s', .x1)")
# Apply the parsed format
out <- fput(c("n", "pct"), "stat", c(42, 0.15))
expect_equal(out, c("42", "15.0%"))
fclear()
})
# ===========================================================================
# Explicit (eval) / e() Expression Label Tests
# ===========================================================================
test_that("e() sets eval attribute on a string", {
label <- e("get_dttm('ru')")
expect_equal(as.character(label), "get_dttm('ru')")
expect_true(isTRUE(attr(label, "eval")))
fclear()
})
test_that("e() errors on non-string input", {
expect_error(e(42))
expect_error(e(c("a", "b")))
fclear()
})
test_that(".has_eval_attr detects eval attribute", {
expect_true(ksformat:::.has_eval_attr(e("test")))
expect_false(ksformat:::.has_eval_attr("test"))
fclear()
})
test_that("fnew with e() wrapper creates eval-marked labels", {
fmt <- fnew(
"ts" = e("format(Sys.time(), '%Y')"),
"static" = "Hello"
)
expect_true(isTRUE(attr(fmt$mappings[["ts"]], "eval")))
expect_null(attr(fmt$mappings[["static"]], "eval"))
fclear()
})
test_that("fput evaluates e() label calling user-defined function", {
my_greet <- function(lang) {
if (lang == "ru") "Привет" else "Hello"
}
fmt <- fnew(
"ru" = e("my_greet('ru')"),
"en" = e("my_greet('en')")
)
result <- fput(c("ru", "en"), fmt)
expect_equal(result, c("\u041f\u0440\u0438\u0432\u0435\u0442", "Hello"))
fclear()
})
test_that("fput with mixed e() and .xN labels", {
fmt <- fnew(
"ts" = e("format(Sys.time(), '%Y')"),
"n" = "sprintf('%s', .x1)"
)
result <- fput(c("ts", "n"), fmt, c(0, 42))
expect_equal(result[1], format(Sys.time(), "%Y"))
expect_equal(result[2], "42")
fclear()
})
test_that("fput with e() label and .xN args combined", {
fmt <- fnew(
"val" = e("sprintf('%s items', .x1)")
)
result <- fput("val", fmt, 5)
expect_equal(result, "5 items")
fclear()
})
test_that("fparse with (eval) marker creates eval-marked labels", {
result <- fparse(text = '
VALUE d_footer
"left_ru" = "paste(\"A\", \"B\")" (eval)
"left_en" = "Hello"
;
')
expect_true(isTRUE(attr(result$d_footer$mappings[["left_ru"]], "eval")))
expect_null(attr(result$d_footer$mappings[["left_en"]], "eval"))
fclear()
})
test_that("fparse (eval) label is evaluated by fput", {
my_suffix <- function() "!!!"
fparse(text = '
VALUE eval_test
"k" = "my_suffix()" (eval)
;
')
result <- fput("k", "eval_test")
expect_equal(result, "!!!")
fclear()
})
test_that("fparse (eval) with .missing and .other", {
result <- fparse(text = '
VALUE eval_mo
"a" = "Static"
.missing = "paste(\"M\", \"V\")" (eval)
.other = "paste(\"O\", \"V\")" (eval)
;
')
fmt <- result$eval_mo
expect_true(isTRUE(attr(fmt$missing_label, "eval")))
expect_true(isTRUE(attr(fmt$other_label, "eval")))
res <- fput(c("a", NA, "zzz"), fmt)
expect_equal(res[1], "Static")
expect_equal(res[2], "M V")
expect_equal(res[3], "O V")
fclear()
})
test_that("fexport roundtrip preserves (eval) flag", {
fmt <- fnew(
"ts" = e("format(Sys.time(), '%Y')"),
"static" = "Hello",
name = "rt_test"
)
txt <- fexport(rt_test = fmt)
expect_match(txt, '(eval)', fixed = TRUE)
fclear()
reparsed <- fparse(text = txt)
expect_true(isTRUE(attr(reparsed$rt_test$mappings[["ts"]], "eval")))
expect_null(attr(reparsed$rt_test$mappings[["static"]], "eval"))
fclear()
})
test_that("fexport roundtrip preserves (eval) on .missing/.other", {
fmt <- fnew(
"a" = "Static",
.missing = e("paste('miss')"),
.other = e("paste('other')"),
name = "rt_mo"
)
txt <- fexport(rt_mo = fmt)
expect_match(txt, '.missing = "paste(\'miss\')" (eval)', fixed = TRUE)
expect_match(txt, '.other = "paste(\'other\')" (eval)', fixed = TRUE)
fclear()
reparsed <- fparse(text = txt)
expect_true(isTRUE(attr(reparsed$rt_mo$missing_label, "eval")))
expect_true(isTRUE(attr(reparsed$rt_mo$other_label, "eval")))
fclear()
})
test_that("fput_all with e() eval labels", {
my_tag <- function() "TAG"
fmt <- fnew(
"a" = e("my_tag()"),
"b" = "Static"
)
result <- fput_all(c("a", "b"), fmt)
expect_equal(result[[1]], "TAG")
expect_equal(result[[2]], "Static")
fclear()
})
test_that("existing .xN expressions still work without (eval)", {
fmt <- fnew("n" = "sprintf('%s', .x1)")
result <- fput("n", fmt, 42)
expect_equal(result, "42")
fclear()
})
# ===========================================================================
# Edge Case Tests
# ===========================================================================
context("Edge Cases: Format Creation")
test_that("fnew errors with no mappings", {
expect_error(fnew(), "At least one")
})
test_that("fnew with default parameter overrides .other", {
fmt <- fnew("A" = "Alpha", .other = "Ignored", default = "Default-Win")
result <- fput(c("A", "Z"), fmt)
expect_equal(result, c("Alpha", "Default-Win"))
})
test_that("fnew with only .missing and .other", {
fmt <- fnew("X" = "Found", .missing = "Missing", .other = "Other")
result <- fput(c("X", NA, "Z"), fmt)
expect_equal(result, c("Found", "Missing", "Other"))
})
test_that("detect_format_type returns character for empty keys", {
expect_equal(detect_format_type(character(0)), "character")
})
test_that("detect_format_type returns numeric for comma keys", {
expect_equal(detect_format_type(c("0,18")), "numeric")
})
test_that("detect_format_type returns numeric for NA keys", {
expect_equal(detect_format_type(c(NA_character_)), "numeric")
})
test_that("detect_format_type returns numeric for empty string keys", {
expect_equal(detect_format_type(c("")), "numeric")
})
test_that("print.ks_format shows both multilabel and nocase flags", {
fmt <- fnew("A" = "Alpha", name = "dual",
type = "character", multilabel = TRUE, ignore_case = TRUE)
output <- capture.output(print(fmt))
expect_true(any(grepl("multilabel", output)))
expect_true(any(grepl("nocase", output)))
fclear()
})
test_that("print.ks_format shows range keys in interval notation", {
fmt <- fnew("0,18,TRUE,FALSE" = "Child",
"18,65,TRUE,FALSE" = "Adult",
name = "ranges_print", type = "numeric")
output <- capture.output(print(fmt))
expect_true(any(grepl("\\[0, 18\\)", output)))
expect_true(any(grepl("\\[18, 65\\)", output)))
fclear()
})
context("Edge Cases: Format Application")
test_that("fput with empty vector returns empty character", {
fmt <- fnew("M" = "Male")
result <- fput(character(0), fmt)
expect_equal(result, character(0))
})
test_that("fput with all-NA vector uses missing label", {
fmt <- fnew("M" = "Male", .missing = "N/A")
result <- fput(c(NA, NA, NA), fmt)
expect_equal(result, c("N/A", "N/A", "N/A"))
})
test_that("fput with NaN uses missing label", {
fmt <- fnew("1" = "One", .missing = "Miss", type = "numeric")
result <- fput(c(1, NaN), fmt)
expect_equal(result, c("One", "Miss"))
})
test_that("fput with NaN and keep_na preserves NA", {
fmt <- fnew("1" = "One", .missing = "Miss", type = "numeric")
result <- fput(c(1, NaN), fmt, keep_na = TRUE)
expect_equal(result[1], "One")
expect_true(is.na(result[2]))
})
test_that("fput with single element vector", {
fmt <- fnew("A" = "Alpha")
expect_equal(fput("A", fmt), "Alpha")
expect_equal(fput("Z", fmt), "Z")
expect_equal(fput(NA, fmt), NA_character_)
})
test_that("fput errors on invalid format type", {
expect_error(fput(c("a", "b"), 42), "ks_format")
expect_error(fput(c("a", "b"), list(a = 1)), "ks_format")
})
test_that("fput with non-numeric strings in numeric format range matching", {
fmt <- fnew("0,100,TRUE,FALSE" = "Valid", type = "numeric",
.other = "Other")
result <- fput(c("50", "abc", "200"), fmt)
expect_equal(result, c("Valid", "Other", "Other"))
})
context("Edge Cases: fput_df")
test_that("fput_df with replace = TRUE", {
df <- data.frame(sex = c("M", "F"), stringsAsFactors = FALSE)
fmt <- fnew("M" = "Male", "F" = "Female")
result <- fput_df(df, sex = fmt, replace = TRUE)
expect_equal(result$sex, c("Male", "Female"))
expect_false("sex_fmt" %in% names(result))
})
test_that("fput_df warns on missing column", {
df <- data.frame(x = 1:3)
fmt <- fnew("1" = "One", type = "numeric")
expect_warning(fput_df(df, nonexistent = fmt), "not found")
})
test_that("fput_df with custom suffix", {
df <- data.frame(sex = c("M", "F"), stringsAsFactors = FALSE)
fmt <- fnew("M" = "Male", "F" = "Female")
result <- fput_df(df, sex = fmt, suffix = "_label")
expect_true("sex_label" %in% names(result))
expect_equal(result$sex_label, c("Male", "Female"))
})
test_that("fput_df errors on non-data.frame", {
expect_error(fput_df(list(a = 1), a = fnew("1" = "x", type = "numeric")),
"data frame")
})
context("Edge Cases: Invalue")
test_that(".invalue_apply with na_if parameter", {
inv <- finput("Yes" = 1, "No" = 0)
result <- ksformat:::.invalue_apply(c("Yes", "No", "Unknown", "N/A"), inv,
na_if = c("Unknown", "N/A"))
expect_equal(result[1], 1)
expect_equal(result[2], 0)
expect_true(is.na(result[3]))
expect_true(is.na(result[4]))
})
test_that(".invalue_apply falls back to numeric conversion for unknown labels", {
inv <- finput("Low" = 1, "High" = 2)
result <- ksformat:::.invalue_apply(c("Low", "42", "High"), inv)
expect_equal(result, c(1, 42, 2))
})
test_that(".invalue_apply with custom missing_value", {
inv <- finput("A" = 1, "B" = 2, missing_value = -999)
result <- ksformat:::.invalue_apply(c("A", NA, "B"), inv)
expect_equal(result, c(1, -999, 2))
})
test_that(".invalue_apply with NULL returns empty typed vector", {
inv <- finput("A" = 1)
result <- ksformat:::.invalue_apply(NULL, inv)
expect_equal(length(result), 0)
expect_true(is.numeric(result))
inv_chr <- finput("A" = "x", target_type = "character")
result_chr <- ksformat:::.invalue_apply(NULL, inv_chr)
expect_equal(length(result_chr), 0)
expect_true(is.character(result_chr))
})
test_that(".invalue_apply with target_type integer", {
inv <- finput("One" = 1L, "Two" = 2L, target_type = "integer")
result <- ksformat:::.invalue_apply(c("One", "Two"), inv)
expect_equal(result, c(1L, 2L))
expect_true(is.integer(result))
})
test_that(".invalue_apply with target_type logical", {
inv <- finput("Yes" = TRUE, "No" = FALSE, target_type = "logical")
result <- ksformat:::.invalue_apply(c("Yes", "No"), inv)
expect_equal(result, c(TRUE, FALSE))
expect_true(is.logical(result))
})
test_that("finputn errors for non-invalue", {
fnew("M" = "Male", name = "not_inv")
expect_error(finputn(c("Male"), "not_inv"), "INVALUE")
fclear()
})
test_that("finputc errors for non-invalue", {
fnew("M" = "Male", name = "not_inv2")
expect_error(finputc(c("Male"), "not_inv2"), "INVALUE")
fclear()
})
test_that(".invalue_apply errors on invalid invalue type", {
expect_error(ksformat:::.invalue_apply(c("a"), 123), "ks_invalue")
})
test_that("fnew_bid without name", {
bi <- fnew_bid("X" = "Ex", "Y" = "Why")
expect_s3_class(bi$format, "ks_format")
expect_s3_class(bi$invalue, "ks_invalue")
expect_null(bi$format$name)
expect_null(bi$invalue$name)
})
test_that("print.ks_invalue shows custom missing_value", {
inv <- finput("A" = 1, missing_value = -99)
output <- capture.output(print(inv))
expect_true(any(grepl("-99", output)))
})
test_that("print.ks_invalue hides NA missing_value", {
inv <- finput("A" = 1)
output <- capture.output(print(inv))
expect_false(any(grepl("Missing value", output)))
})
context("Edge Cases: Utilities and Validation")
test_that("is_missing with NaN", {
expect_true(is_missing(NaN))
})
test_that("is_missing with empty string and include_empty", {
expect_true(is_missing(""))
})
test_that("is_missing with vector input", {
result <- is_missing(c(1, NA, NaN, 3))
expect_equal(result, c(FALSE, TRUE, TRUE, FALSE))
})
test_that("range_spec errors with non-numeric bounds", {
expect_error(range_spec("a", 10, "lbl"), "numeric")
expect_error(range_spec(1, "b", "lbl"), "numeric")
})
test_that("range_spec errors when low > high", {
expect_error(range_spec(100, 1, "lbl"), "less than")
})
test_that("in_range returns FALSE for non-range_spec", {
expect_false(in_range(5, list(low = 0, high = 10)))
})
test_that(".format_validate warns on duplicate keys", {
expect_warning(fnew("A" = "First", "A" = "Second"), "Duplicate")
})
test_that(".format_validate warns on invalid numeric keys", {
expect_warning(fnew("abc" = "Label", type = "numeric"),
"not a valid numeric")
})
test_that("fclear warns for non-existent format name", {
fclear()
expect_warning(fclear("no_such_fmt"), "not found")
})
test_that("fprint errors for non-existent format name", {
fclear()
expect_error(fprint("no_such_fmt"), "not found")
})
test_that("fprint shows empty library message", {
fclear()
output <- capture.output(fprint())
expect_true(any(grepl("empty", output)))
})
test_that(".format_register errors with overwrite = FALSE", {
fnew("A" = "Alpha", name = "ow_test")
fmt2 <- fnew("B" = "Beta")
expect_error(
ksformat:::.format_register(fmt2, name = "ow_test", overwrite = FALSE),
"already exists"
)
fclear()
})
test_that(".format_register silently ignores NULL/empty name", {
fmt <- fnew("A" = "Alpha")
# Should not error
expect_silent(ksformat:::.format_register(fmt, name = NULL))
expect_silent(ksformat:::.format_register(fmt, name = ""))
})
test_that(".format_get errors with clear message on empty library", {
fclear()
expect_error(ksformat:::.format_get("nonexistent"), "empty")
})
test_that(".format_get errors listing available formats", {
fnew("A" = "Alpha", name = "avail")
expect_error(ksformat:::.format_get("nonexistent"), "avail")
fclear()
})
context("Edge Cases: Expression Labels")
test_that("eval_expr_label warns and returns NA on error", {
fmt <- fnew("x" = "stop(.x1)", type = "character")
expect_warning(
result <- fput("x", fmt, "bad"),
"failed"
)
expect_true(is.na(result))
})
test_that("eval_expr_label with expression referencing .x1", {
fmt <- fnew("a" = "paste0('val=', .x1)", type = "character")
result <- fput(c("a", "a"), fmt, c(10, 20))
expect_equal(result, c("val=10", "val=20"))
})
context("Edge Cases: Format Parsing")
test_that("fparse with text as character vector", {
txt <- c(
"VALUE test (character)",
" \"A\" = \"Alpha\"",
" \"B\" = \"Beta\"",
";"
)
result <- fparse(text = txt)
expect_equal(result$test$mappings[["A"]], "Alpha")
expect_equal(result$test$mappings[["B"]], "Beta")
fclear()
})
test_that("fparse warns on unclosed block", {
txt <- '
VALUE unclosed (character)
"A" = "Alpha"
'
expect_warning(fparse(text = txt), "Closing automatically")
fclear()
})
test_that("fparse warns on new block before closing previous", {
txt <- '
VALUE first (character)
"A" = "Alpha"
VALUE second (character)
"B" = "Beta"
;
'
expect_warning(fparse(text = txt), "New block started")
fclear()
})
test_that("fparse handles inline comments", {
txt <- '
VALUE test (character)
"A" = "Alpha" // this is a comment
"B" = "Beta" /* another comment */
;
'
result <- fparse(text = txt)
expect_equal(result$test$mappings[["A"]], "Alpha")
expect_equal(result$test$mappings[["B"]], "Beta")
fclear()
})
test_that("fparse handles multilabel + nocase together", {
txt <- '
VALUE combo (numeric, multilabel, nocase)
[0, 50) = "Low"
[50, 100] = "High"
;
'
result <- fparse(text = txt)
expect_true(result$combo$multilabel)
expect_true(result$combo$ignore_case)
fclear()
})
test_that("fparse handles .missing in INVALUE block", {
txt <- '
INVALUE inv_miss
"Cat" = 1
"Dog" = 2
.missing = 0
;
'
result <- fparse(text = txt)
# .missing in invalue should produce a missing_value
expect_true("inv_miss" %in% ls(envir = ksformat:::.format_library))
fclear()
})
context("Edge Cases: Format Export")
test_that("fexport with formats parameter instead of ...", {
fmt1 <- fnew("A" = "Alpha", name = "exp1")
fmt2 <- fnew("B" = "Beta", name = "exp2")
txt <- fexport(formats = list(fmt1, fmt2))
expect_true(grepl("exp1", txt))
expect_true(grepl("exp2", txt))
fclear()
})
test_that("fexport warns for invalid object", {
fmt <- fnew("A" = "Alpha", name = "ok")
expect_warning(fexport(ok = fmt, bad = list(x = 1)), "not a.*ks_format")
fclear()
})
test_that("fexport errors with no objects", {
expect_error(fexport(), "At least one")
})
context("Edge Cases: Datetime Formats")
test_that("fnew_date errors for invalid type", {
expect_error(fnew_date("%Y", type = "integer"), "date.*time.*datetime")
})
test_that("fnew_date with .missing parameter", {
fmt <- fnew_date("DATE9.", .missing = "No Date")
result <- fput(c(0, NA), fmt)
expect_equal(result[2], "No Date")
})
test_that("fput date with character date strings", {
fmt <- fnew_date("DATE9.")
result <- fput(as.Date("2024-01-15"), fmt)
expect_true(nchar(result) > 0)
})
test_that("fput date with POSIXct conversion to date", {
fmt <- fnew_date("DATE9.")
dt <- as.POSIXct("2024-01-15 10:30:00")
result <- fput(dt, fmt)
expect_true(nchar(result) > 0)
})
test_that("fput time with character time strings", {
fmt <- fnew_date("TIME8.", type = "time")
result <- fput("12:30:00", fmt)
expect_true(nchar(result) > 0)
})
test_that("fput datetime with numeric R-epoch (default)", {
fmt <- fnew_date("DATETIME20.")
# R epoch: 0 = 1970-01-01
result <- fput(0, fmt)
expect_true(grepl("1970", result))
})
test_that("SAS format name DDMMYY8 resolves correctly", {
fmt <- fnew_date("DDMMYY8.")
result <- fput(as.Date("2024-03-15"), fmt)
expect_true(nchar(result) > 0)
})
test_that("SAS format DOWNAME resolves correctly", {
fmt <- fnew_date("DOWNAME.")
result <- fput(as.Date("2024-01-15"), fmt) # Monday
expect_true(nchar(result) > 0)
})
test_that("SAS format MONNAME resolves correctly", {
fmt <- fnew_date("MONNAME.")
result <- fput(as.Date("2024-03-15"), fmt)
expect_true(nchar(result) > 0)
})
test_that("fput_all with datetime format returns list", {
fmt <- fnew_date("DATE9.")
result <- fput_all(c(as.Date("2024-01-01"), as.Date("2024-06-15")), fmt)
expect_true(is.list(result))
expect_equal(length(result), 2)
})
context("Edge Cases: fput_all")
test_that("fput_all with empty vector returns empty list", {
fmt <- fnew("A" = "Alpha", type = "character")
result <- fput_all(character(0), fmt)
expect_equal(result, list())
})
test_that("fput_all with all-NA vector", {
fmt <- fnew("A" = "Alpha", .missing = "N/A")
result <- fput_all(c(NA, NA), fmt)
expect_equal(result[[1]], "N/A")
expect_equal(result[[2]], "N/A")
})
test_that("fput_all with NaN uses missing label", {
fmt <- fnew("1" = "One", .missing = "Miss", type = "numeric")
result <- fput_all(NaN, fmt)
expect_equal(result[[1]], "Miss")
})
test_that("fput_all unmatched without .other returns original value", {
fmt <- fnew("A" = "Alpha", type = "character")
result <- fput_all("Z", fmt)
expect_equal(result[[1]], "Z")
})
test_that("fput_all errors on invalid format", {
expect_error(fput_all(c(1, 2), 42), "ks_format")
})
context("Edge Cases: Library Management")
test_that("fclear on already empty library does not error", {
fclear()
expect_message(fclear(), "cleared")
})
test_that("fclear single format leaves others intact", {
fnew("A" = "Alpha", name = "keep_me")
fnew("B" = "Beta", name = "remove_me")
expect_message(fclear("remove_me"), "removed")
expect_true("keep_me" %in% ls(envir = ksformat:::.format_library))
expect_false("remove_me" %in% ls(envir = ksformat:::.format_library))
fclear()
})
test_that("fnew_bid registers both format and invalue", {
bi <- fnew_bid("X" = "Ex", name = "bi_test")
expect_true("bi_test" %in% ls(envir = ksformat:::.format_library))
expect_true("bi_test_inv" %in% ls(envir = ksformat:::.format_library))
fclear()
})
test_that(".format_get resolves SAS datetime format on the fly", {
fclear()
# SAS format name not registered, but should auto-create via fallback
result <- fput(as.Date("2024-01-15"), "DATE9.")
expect_true(nchar(result) > 0)
fclear()
})
# ============================================================
# Code review fixes — regression tests
# ============================================================
test_that("fput expression: scalar x with scalar extra arg", {
fparse(text = '
VALUE stat (character)
"mean" = "sprintf(\"%.2f\", .x1)"
;
')
result <- fput("mean", "stat", 1.23456)
expect_equal(result, "1.23")
fclear()
})
test_that("fput expression: scalar x with vector extra arg", {
fparse(text = '
VALUE stat (character)
"mean" = "sprintf(\"%.2f\", .x1)"
;
')
result <- fput("mean", "stat", c(1.23456, 2.34567, 3.45678))
expect_equal(result, c("1.23", "2.35", "3.46"))
fclear()
})
test_that("fput expression: matching-length x and extra arg", {
fparse(text = '
VALUE stat (character)
"mean" = "sprintf(\"%.2f\", .x1)"
;
')
result <- fput(c("mean", "mean", "mean"), "stat", c(1.23456, 2.34567, 3.45678))
expect_equal(result, c("1.23", "2.35", "3.46"))
fclear()
})
test_that("fput vector recycling: length mismatch errors", {
fparse(text = '
VALUE stat
"mean" = "sprintf(\"%.2f\", .x1)"
;
')
# x has length 2, extra arg has length 3 → error
expect_error(
fput(c("mean", "mean"), "stat", c(1.23, 2.34, 3.45)),
"Length mismatch"
)
# Multiple extra args with different non-scalar lengths → error
expect_error(
fput("mean", "stat", c(1, 2), c(3, 4, 5)),
"Length mismatch in extra arguments"
)
fclear()
})
test_that(".eval_expr_label handles unparseable expression gracefully", {
# Labels with .xN trigger expression evaluation
fmt <- fnew("x" = "this is not valid R {{{{ .x1", name = "bad_expr")
expect_warning(
result <- fput("x", fmt, 1),
"parse failed"
)
expect_equal(result, NA_character_)
fclear()
})
test_that(".eval_expr_label handles NULL/empty expression result", {
# NULL expression via .xN pattern to trigger eval
fmt <- fnew("x" = "{ .x1; NULL }", name = "null_expr")
expect_warning(
result <- fput("x", fmt, 1),
"empty result"
)
expect_equal(result, NA_character_)
fclear()
})
test_that(".invalue_apply preserves explicitly-mapped NA values", {
inv <- finput("Known" = 1, "Unknown" = NA, name = "na_test_inv")
result <- ksformat:::.invalue_apply(c("Known", "Unknown"), inv)
expect_equal(result, c(1, NA_real_))
fclear()
})
test_that("fnew_bid warns on duplicate values (ambiguous reverse)", {
expect_warning(
fnew_bid("M" = "Adult", "F" = "Adult", name = "dup_val"),
"Multiple keys map to the same value"
)
fclear()
})
test_that("finputn warns on non-numeric target_type", {
finput("Male" = "M", "Female" = "F",
name = "char_inv", target_type = "character")
expect_warning(
finputn(c("Male", "Female"), "char_inv"),
"not numeric"
)
fclear()
})
test_that(".format_get errors on ambiguous case-insensitive matches", {
fnew("M" = "Male", name = "Sex")
fnew("1" = "One", name = "sex")
expect_error(
fput("M", "SEX"),
"Ambiguous"
)
fclear()
})
test_that("in_range returns FALSE for NA input", {
rs <- range_spec(0, 10, "test")
expect_false(in_range(NA, rs))
})
test_that("finput rejects non-scalar mapping values", {
expect_error(
finput("A" = c(1, 2), name = "bad_inv"),
"must be scalar"
)
})
test_that("fimport reads SAS CNTLOUT CSV", {
csv_file <- system.file("testdata", "test_cntlout.csv", package = "ksformat")
skip_if(!nzchar(csv_file), "test_cntlout.csv not found")
imported <- fimport(csv_file)
expect_true(length(imported) > 0)
fclear()
})
# --- fputk (composite key lookup) ---
test_that("fputk pastes two vectors and applies format", {
fnew("A|1" = "X", "A|2" = "Y", "B|1" = "Z",
name = "fputk_test", type = "character")
result <- fputk(c("A", "A", "B"), c(1, 2, 1), format = "fputk_test")
expect_equal(result, c("X", "Y", "Z"))
fclear()
})
test_that("fputk uses .other for unmatched composite keys", {
fnew("A|1" = "X", .other = "MISS",
name = "fputk_other", type = "character")
result <- fputk(c("A", "B"), c(1, 1), format = "fputk_other")
expect_equal(result, c("X", "MISS"))
fclear()
})
test_that("fputk supports custom separator", {
fnew("A-1" = "X", name = "fputk_sep", type = "character")
result <- fputk("A", 1, format = "fputk_sep", sep = "-")
expect_equal(result, "X")
fclear()
})
test_that("fputk passes keep_na through", {
fnew("A|1" = "X", .missing = "NA_LABEL",
name = "fputk_na", type = "character")
result_mapped <- fputk(NA, 1, format = "fputk_na")
result_kept <- fputk(NA, 1, format = "fputk_na", keep_na = TRUE)
expect_equal(result_mapped, "NA_LABEL")
expect_true(is.na(result_kept))
fclear()
})
test_that("fputk works with three key components", {
fnew("A|1|X" = "found", .other = "nope",
name = "fputk_3", type = "character")
result <- fputk("A", 1, "X", format = "fputk_3")
expect_equal(result, "found")
fclear()
})
test_that("fputk accepts a ks_format object directly", {
fmt <- fnew("A|1" = "hit", .other = "miss", type = "character")
result <- fputk(c("A", "B"), c(1, 1), format = fmt)
expect_equal(result, c("hit", "miss"))
})
test_that("fputk na_as_string matches paste()/fmap()-built keys", {
# Format built via fmap(paste(...), values) — paste() stores literal "NA"
fnew(
fmap(
paste(c("CHEM", "COAG", "COAG"), c("ALB", "FIBRINO", "INR"),
c("g/L", "g/L", NA), sep = "|"),
c("ALB", "FIBRINO", "INR")
),
.other = NA, name = "fputk_na_string", type = "character"
)
# With na_as_string = TRUE the literal "NA" propagates and matches
res_keep <- fputk(c("CHEM", "COAG", "COAG"),
c("ALB", "FIBRINO", "INR"),
c("g/L", "g/L", NA),
format = "fputk_na_string", na_as_string = TRUE)
expect_equal(res_keep, c("ALB", "FIBRINO", "INR"))
# Default (FALSE) restores NA so the third row falls through to .other
res_default <- fputk(c("CHEM", "COAG", "COAG"),
c("ALB", "FIBRINO", "INR"),
c("g/L", "g/L", NA),
format = "fputk_na_string")
expect_equal(res_default, c("ALB", "FIBRINO", NA_character_))
fclear()
})
test_that("fputk na_as_string default still routes NA via .missing", {
fnew("A|1" = "hit", .missing = "MISS",
name = "fputk_default_na", type = "character")
# Default behavior unchanged
expect_equal(fputk(NA, 1, format = "fputk_default_na"), "MISS")
# na_as_string = TRUE keeps the literal "NA|1" key — no .missing routing
expect_equal(
fputk(NA, 1, format = "fputk_default_na", na_as_string = TRUE),
"NA|1"
)
fclear()
})
# --- finputk (composite label invalue lookup) ---
test_that("finputk pastes labels and applies invalue", {
finput(
fmap(paste(c("A", "A", "B"), c(1, 2, 1), sep = "|"), c(10, 20, 30)),
name = "finputk_test"
)
result <- finputk(c("A", "A", "B"), c(1, 2, 1),
invalue_name = "finputk_test")
expect_equal(result, c(10, 20, 30))
fclear()
})
test_that("finputk supports custom separator", {
finput("A-1" = 7, name = "finputk_sep")
result <- finputk("A", 1, invalue_name = "finputk_sep", sep = "-")
expect_equal(result, 7)
fclear()
})
test_that("finputk na_as_string matches paste()/fmap()-built labels", {
finput(
fmap(paste(c("CHEM", "COAG"), c("INR", "INR"),
c("g/L", NA), sep = "|"),
c(1, 2)),
name = "finputk_na_string"
)
res_keep <- finputk(c("CHEM", "COAG"), c("INR", "INR"), c("g/L", NA),
invalue_name = "finputk_na_string",
na_as_string = TRUE)
expect_equal(res_keep, c(1, 2))
res_default <- finputk(c("CHEM", "COAG"), c("INR", "INR"), c("g/L", NA),
invalue_name = "finputk_na_string")
expect_equal(res_default, c(1, NA_real_))
fclear()
})
test_that("finputk respects target_type from stored invalue", {
finput("A|1" = "X", name = "finputk_char", target_type = "character")
expect_type(finputk("A", 1, invalue_name = "finputk_char"), "character")
fclear()
})
test_that("finputk errors on non-ks_invalue lookup", {
fnew("A|1" = "X", name = "finputk_not_inv", type = "character")
expect_error(
finputk("A", 1, invalue_name = "finputk_not_inv"),
"not an INVALUE format"
)
fclear()
})
test_that("finputk errors when no components are provided", {
finput("A" = 1, name = "finputk_empty")
expect_error(finputk(invalue_name = "finputk_empty"),
"At least one label component")
fclear()
})
# ===========================================================================
# Value Type System (Date, POSIXct, logical)
# ===========================================================================
context("Value Type Formats")
# --- fnew with value types ---
test_that("fnew creates Date format from named Date vector", {
dates <- setNames(
as.Date(c("2025-01-15", "2025-02-20", "2025-03-10")),
c("A|1", "A|2", "B|1")
)
fmt <- fnew(dates, type = "Date")
expect_equal(fmt$type, "Date")
expect_equal(length(fmt$mappings), 3L)
expect_s3_class(fmt$mappings[["A|1"]], "Date")
expect_equal(fmt$mappings[["A|1"]], as.Date("2025-01-15"))
expect_null(fmt$missing_label)
expect_null(fmt$other_label)
})
test_that("fnew creates Date format from direct named args", {
fmt <- fnew(
"A" = as.Date("2025-01-15"),
"B" = as.Date("2025-02-20"),
type = "Date"
)
expect_equal(fmt$type, "Date")
expect_s3_class(fmt$mappings[["A"]], "Date")
expect_equal(fmt$mappings[["B"]], as.Date("2025-02-20"))
})
test_that("fnew auto-detects Date type from values", {
dates <- setNames(
as.Date(c("2025-01-01", "2025-06-01")),
c("key1", "key2")
)
fmt <- fnew(dates)
expect_equal(fmt$type, "Date")
})
test_that("fnew auto-detects logical type from values", {
fmt <- fnew("yes" = TRUE, "no" = FALSE)
expect_equal(fmt$type, "logical")
expect_true(fmt$mappings[["yes"]])
expect_false(fmt$mappings[["no"]])
})
test_that("fnew warns when .missing is non-NA for value types", {
expect_warning(
fnew("A" = as.Date("2025-01-01"), .missing = "Missing", type = "Date"),
"ignored"
)
})
test_that("fnew warns when .other is non-NA for value types", {
expect_warning(
fnew("A" = as.Date("2025-01-01"), .other = "Other", type = "Date"),
"ignored"
)
})
test_that("fnew allows .missing = NA and .other = NA without warning for value types", {
expect_silent(
fnew("A" = as.Date("2025-01-01"), .missing = NA, .other = NA, type = "Date")
)
})
test_that("fnew creates POSIXct format", {
fmt <- fnew(
"event1" = as.POSIXct("2025-01-15 10:30:00"),
"event2" = as.POSIXct("2025-02-20 14:00:00"),
type = "POSIXct"
)
expect_equal(fmt$type, "POSIXct")
expect_s3_class(fmt$mappings[["event1"]], "POSIXct")
})
test_that("fnew creates logical format", {
fmt <- fnew("Y" = TRUE, "N" = FALSE, type = "logical")
expect_equal(fmt$type, "logical")
expect_true(fmt$mappings[["Y"]])
expect_false(fmt$mappings[["N"]])
})
test_that("fnew stores and retrieves Date format by name", {
fnew(
"A" = as.Date("2025-01-15"),
"B" = as.Date("2025-02-20"),
name = "test_date_fmt", type = "Date"
)
fmt <- format_get("test_date_fmt")
expect_equal(fmt$type, "Date")
expect_s3_class(fmt$mappings[["A"]], "Date")
fclear()
})
# --- fput with value types ---
test_that("fput returns Date vector for Date format", {
fmt <- fnew(
"A" = as.Date("2025-01-15"),
"B" = as.Date("2025-02-20"),
type = "Date"
)
result <- fput(c("A", "B"), fmt)
expect_s3_class(result, "Date")
expect_equal(result, as.Date(c("2025-01-15", "2025-02-20")))
})
test_that("fput returns typed NA for unmatched values in Date format", {
fmt <- fnew("A" = as.Date("2025-01-15"), type = "Date")
result <- fput(c("A", "X"), fmt)
expect_s3_class(result, "Date")
expect_equal(result[1], as.Date("2025-01-15"))
expect_true(is.na(result[2]))
})
test_that("fput returns typed NA for missing values in Date format", {
fmt <- fnew("A" = as.Date("2025-01-15"), type = "Date")
result <- fput(c("A", NA), fmt)
expect_s3_class(result, "Date")
expect_equal(result[1], as.Date("2025-01-15"))
expect_true(is.na(result[2]))
})
test_that("fput returns logical vector for logical format", {
fmt <- fnew("Y" = TRUE, "N" = FALSE, type = "logical")
result <- fput(c("Y", "N", "Y"), fmt)
expect_type(result, "logical")
expect_equal(result, c(TRUE, FALSE, TRUE))
})
test_that("fput returns NA for unmatched in logical format", {
fmt <- fnew("Y" = TRUE, "N" = FALSE, type = "logical")
result <- fput(c("Y", "X", NA), fmt)
expect_type(result, "logical")
expect_true(result[1])
expect_true(is.na(result[2]))
expect_true(is.na(result[3]))
})
test_that("fput returns POSIXct vector for POSIXct format", {
t1 <- as.POSIXct("2025-01-15 10:30:00")
t2 <- as.POSIXct("2025-02-20 14:00:00")
fmt <- fnew("e1" = t1, "e2" = t2, type = "POSIXct")
result <- fput(c("e1", "e2"), fmt)
expect_s3_class(result, "POSIXct")
expect_equal(result, c(t1, t2))
})
test_that("fput preserves POSIXct tzone from source values (UTC)", {
t1 <- as.POSIXct("2021-02-09 11:35", format = "%Y-%m-%d %H:%M", tz = "UTC")
t2 <- as.POSIXct("2021-02-09 12:35", format = "%Y-%m-%d %H:%M", tz = "UTC")
fmt <- fnew(fmap(c("001|909", "002|910"), c(t1, t2)),
type = "POSIXct", name = "trtstart_dttn")
on.exit(fclear(), add = TRUE)
result <- fputk(c("001", "002"), c("909", "910"), format = "trtstart_dttn")
expect_s3_class(result, "POSIXct")
expect_equal(attr(result, "tzone"), "UTC")
# Display in UTC must match the source instants regardless of system TZ
expect_equal(format(result, tz = "UTC", format = "%Y-%m-%d %H:%M"),
c("2021-02-09 11:35", "2021-02-09 12:35"))
expect_equal(as.numeric(result), as.numeric(c(t1, t2)))
})
test_that("fput handles empty input for value types", {
fmt <- fnew("A" = as.Date("2025-01-15"), type = "Date")
result <- fput(NULL, fmt)
expect_s3_class(result, "Date")
expect_equal(length(result), 0L)
})
test_that("fput handles all-NA input for value types", {
fmt <- fnew("A" = as.Date("2025-01-15"), type = "Date")
result <- fput(c(NA, NA), fmt)
expect_s3_class(result, "Date")
expect_true(all(is.na(result)))
})
test_that("fput with keep_na for value types", {
fmt <- fnew("A" = as.Date("2025-01-15"), type = "Date")
result <- fput(c("A", NA), fmt, keep_na = TRUE)
expect_s3_class(result, "Date")
expect_equal(result[1], as.Date("2025-01-15"))
expect_true(is.na(result[2]))
})
# --- fputk with value types ---
test_that("fputk returns Date vector for Date format", {
fnew(
"A|1" = as.Date("2025-01-15"),
"A|2" = as.Date("2025-02-20"),
"B|1" = as.Date("2025-03-10"),
name = "visit_dt", type = "Date"
)
result <- fputk(c("A", "A", "B"), c(1, 2, 1), format = "visit_dt")
expect_s3_class(result, "Date")
expect_equal(result, as.Date(c("2025-01-15", "2025-02-20", "2025-03-10")))
fclear()
})
test_that("fputk returns typed NA for unmatched composite keys", {
fmt <- fnew(
"A|1" = as.Date("2025-01-15"),
type = "Date"
)
result <- fputk(c("A", "B"), c(1, 1), format = fmt)
expect_s3_class(result, "Date")
expect_equal(result[1], as.Date("2025-01-15"))
expect_true(is.na(result[2]))
})
# --- fput_df with value types ---
test_that("fput_df creates typed column for Date format", {
fmt <- fnew("A" = as.Date("2025-01-15"), "B" = as.Date("2025-02-20"),
type = "Date")
df <- data.frame(code = c("A", "B", "A"), stringsAsFactors = FALSE)
result <- fput_df(df, code = fmt)
expect_s3_class(result$code_fmt, "Date")
expect_equal(result$code_fmt, as.Date(c("2025-01-15", "2025-02-20", "2025-01-15")))
})
# --- fput_all with value types ---
test_that("fput_all returns list of typed values for Date format", {
fmt <- fnew("A" = as.Date("2025-01-15"), "B" = as.Date("2025-02-20"),
type = "Date")
result <- fput_all(c("A", "B"), fmt)
expect_type(result, "list")
expect_s3_class(result[[1]], "Date")
expect_equal(result[[1]], as.Date("2025-01-15"))
})
# --- Date range matching ---
test_that("fput matches Date ranges", {
fmt <- fnew(
"2020-01-01,2025-12-31,TRUE,FALSE" = as.Date("2022-06-15"),
"specific" = as.Date("2030-01-01"),
type = "Date"
)
# Input date "2022-07-01" falls in the range [2020-01-01, 2025-12-31)
result <- fput(c("specific", "2022-07-01"), fmt)
expect_s3_class(result, "Date")
expect_equal(result[1], as.Date("2030-01-01"))
expect_equal(result[2], as.Date("2022-06-15"))
})
# --- ignore_case with value types ---
test_that("fput with ignore_case for Date format", {
fmt <- fnew("a" = as.Date("2025-01-15"), type = "Date", ignore_case = TRUE)
result <- fput(c("A", "a"), fmt)
expect_s3_class(result, "Date")
expect_equal(result, as.Date(c("2025-01-15", "2025-01-15")))
})
# --- print for value types ---
test_that("print.ks_format works for Date format", {
fmt <- fnew("A" = as.Date("2025-01-15"), "B" = as.Date("2025-02-20"),
type = "Date")
output <- capture.output(print(fmt))
expect_true(any(grepl("Date", output)))
expect_true(any(grepl("2025-01-15", output)))
})
test_that("print.ks_format works for logical format", {
fmt <- fnew("Y" = TRUE, "N" = FALSE, type = "logical")
output <- capture.output(print(fmt))
expect_true(any(grepl("logical", output)))
expect_true(any(grepl("TRUE", output)))
})
# --- fparse with value types ---
test_that("fparse creates Date format from text", {
fparse(text = '
VALUE dtest (Date, format: %Y-%m-%d)
"A" = "2025-01-15"
"B" = "2025-02-20"
;
')
fmt <- format_get("dtest")
expect_equal(fmt$type, "Date")
expect_s3_class(fmt$mappings[["A"]], "Date")
expect_equal(fmt$mappings[["A"]], as.Date("2025-01-15"))
expect_equal(fmt$date_format, "%Y-%m-%d")
# Apply
result <- fput(c("A", "B"), "dtest")
expect_s3_class(result, "Date")
expect_equal(result, as.Date(c("2025-01-15", "2025-02-20")))
fclear()
})
test_that("fparse creates logical format from text", {
fparse(text = '
VALUE bool_test (logical)
"Y" = "TRUE"
"N" = "FALSE"
;
')
fmt <- format_get("bool_test")
expect_equal(fmt$type, "logical")
expect_true(fmt$mappings[["Y"]])
expect_false(fmt$mappings[["N"]])
fclear()
})
# --- fexport roundtrip with value types ---
test_that("fexport/fparse roundtrip for Date format", {
fmt_orig <- fnew("A" = as.Date("2025-01-15"), "B" = as.Date("2025-02-20"),
name = "dt_rt", type = "Date")
txt <- fexport(dt_rt = fmt_orig)
fclear()
fparse(text = txt)
fmt <- format_get("dt_rt")
expect_equal(fmt$type, "Date")
expect_equal(fmt$mappings[["A"]], as.Date("2025-01-15"))
expect_equal(fmt$mappings[["B"]], as.Date("2025-02-20"))
fclear()
})
# ============================================================================
# Range table fast path (findInterval-based numeric range matching)
# ============================================================================
test_that("range_table is attached at format creation (fparse)", {
fparse(text = '
VALUE age_rt (numeric)
[0, 18) = "Child"
[18, 65) = "Adult"
[65, HIGH] = "Senior"
;
')
fmt <- format_get("age_rt")
rt <- fmt$range_table
expect_false(is.null(rt))
expect_equal(rt$low, c(0, 18, 65))
expect_equal(rt$high, c(18, 65, Inf))
expect_true(all(rt$inc_low))
expect_equal(rt$inc_high, c(FALSE, FALSE, TRUE))
expect_true(rt$sorted_disjoint)
fclear()
})
test_that("range_table is attached by fparse for VALUE blocks", {
fparse(text = '
VALUE age_p (numeric)
[0, 18) = "Child"
[18, 65) = "Adult"
[65, HIGH] = "Senior"
;
')
fmt <- format_get("age_p")
expect_false(is.null(fmt$range_table))
expect_true(fmt$range_table$sorted_disjoint)
fclear()
})
test_that("fput numeric range fast path matches generic path", {
fparse(text = '
VALUE age_fp (numeric)
[0, 18) = "Child"
[18, 65) = "Adult"
[65, HIGH] = "Senior"
.missing = "Unknown"
;
')
vals <- c(-1, 0, 5, 17, 18, 30, 64, 65, 100, NA)
out <- fput(vals, "age_fp")
expect_equal(out, c("-1", "Child", "Child", "Child", "Adult",
"Adult", "Adult", "Senior", "Senior", "Unknown"))
fclear()
})
test_that("fput respects gaps between ranges (fast path)", {
fparse(text = '
VALUE gap_rt (numeric)
[0, 10) = "Low"
[20, 30) = "High"
;
')
fmt <- format_get("gap_rt")
expect_true(fmt$range_table$sorted_disjoint)
out <- fput(c(5, 15, 25, 10, 20), "gap_rt")
# 15 falls in gap; 10 excluded by high of first range — both returned as-is
expect_equal(out, c("Low", "15", "High", "10", "High"))
fclear()
})
test_that("fput with overlapping multilabel ranges falls back to general path", {
fparse(text = '
VALUE risk_rt (numeric, multilabel)
[0, 3] = "Low"
[0, 7] = "Monitored"
(3, 7] = "Medium"
(7, 10] = "High"
;
')
rt <- format_get("risk_rt")$range_table
expect_false(rt$sorted_disjoint)
# fput_all should return all matching labels
out <- fput_all(c(2, 5, 8), "risk_rt")
expect_setequal(out[[1]], c("Low", "Monitored"))
expect_setequal(out[[2]], c("Monitored", "Medium"))
expect_setequal(out[[3]], "High")
fclear()
})
test_that("fput with exclusive lower bound on first range uses general path", {
fparse(text = '
VALUE excl_rt (numeric)
(0, 10) = "Mid"
[10, 20) = "High"
;
')
out <- fput(c(0, 5, 10, 15), "excl_rt")
# 0 has inc_low=FALSE on first range → no match → returned as-is
expect_equal(out, c("0", "Mid", "High", "High"))
fclear()
})
test_that("fput with point range [5,5] matches single value", {
fparse(text = '
VALUE pt_rt (numeric)
[5, 5] = "Exactly5"
[10, 20) = "Teens"
;
')
out <- fput(c(4, 5, 6, 15), "pt_rt")
expect_equal(out, c("4", "Exactly5", "6", "Teens"))
fclear()
})
test_that("fput numeric ranges work without cached range_table (fallback)", {
fparse(text = '
VALUE fb_rt (numeric)
[0, 10) = "Low"
[10, 20) = "High"
;
')
fmt <- format_get("fb_rt")
# Strip the cached table to exercise the fallback path
fmt$range_table <- NULL
out <- fput(c(5, 15, 25), fmt)
expect_equal(out[1:2], c("Low", "High"))
fclear()
})
test_that("range_table sorts entries so fast path triggers on out-of-order ranges", {
# Define ranges in reverse order: still disjoint, should be canonicalised
fparse(text = '
VALUE rev_rt (numeric)
[65, HIGH] = "Senior"
[18, 65) = "Adult"
[0, 18) = "Child"
;
')
fmt <- format_get("rev_rt")
rt <- fmt$range_table
expect_true(rt$sorted_disjoint)
expect_equal(rt$low, c(0, 18, 65))
expect_equal(rt$label, c("Child", "Adult", "Senior"))
out <- fput(c(5, 30, 70), "rev_rt")
expect_equal(out, c("Child", "Adult", "Senior"))
fclear()
})
# ============================================================================
# Date range / datetime range bucketing (date_range, datetime_range)
# ============================================================================
context("Date Range Bucketing")
test_that("fnew creates a date_range format and buckets Date input", {
fmt <- fnew(
"2024-01-01,2025-01-01,TRUE,FALSE" = "FY24",
"2025-01-01,2026-01-01,TRUE,FALSE" = "FY25",
type = "date_range"
)
expect_equal(fmt$type, "date_range")
expect_true(fmt$range_table$sorted_disjoint)
out <- fput(as.Date(c("2024-06-15", "2025-03-01", "2023-12-31")), fmt)
expect_equal(out, c("FY24", "FY25", "2023-12-31"))
})
test_that("date_range accepts POSIXct input by truncating to date", {
fmt <- fnew(
"2024-01-01,2025-01-01,TRUE,FALSE" = "Y24",
type = "date_range"
)
x <- as.POSIXct(c("2024-06-15 10:30:00", "2025-02-01 00:00:00"), tz = "UTC")
out <- fput(x, fmt)
expect_equal(out, c("Y24", "2025-02-01"))
})
test_that("date_range supports LOW / HIGH bounds via fparse", {
fparse(text = '
VALUE era (date_range)
[LOW, 2000-01-01) = "Past"
[2000-01-01, 2025-01-01) = "Present"
[2025-01-01, HIGH] = "Future"
;
')
out <- fput(as.Date(c("1990-01-01", "2010-06-15", "2030-01-01")), "era")
expect_equal(out, c("Past", "Present", "Future"))
fclear()
})
test_that("date_range exclusive lower bound (parens) works", {
fparse(text = '
VALUE strict (date_range)
(2024-01-01, 2025-01-01) = "Y24"
;
')
out <- fput(as.Date(c("2024-01-01", "2024-06-01", "2025-01-01")), "strict")
expect_equal(out, c("2024-01-01", "Y24", "2025-01-01"))
fclear()
})
test_that("date_range honors .missing and .other", {
fparse(text = '
VALUE bucket (date_range)
[2024-01-01, 2025-01-01) = "Y24"
.missing = "MISS"
.other = "OTHER"
;
')
out <- fput(as.Date(c("2024-06-15", NA, "2030-01-01")), "bucket")
expect_equal(out, c("Y24", "MISS", "OTHER"))
fclear()
})
test_that("fput_all returns all matching labels for overlapping date_range", {
fparse(text = '
VALUE risk (date_range, multilabel)
[2024-01-01, 2024-07-01) = "H1"
[2024-04-01, 2024-10-01) = "Mid"
[2024-07-01, 2025-01-01) = "H2"
;
')
out <- fput_all(as.Date(c("2024-02-01", "2024-05-15", "2024-08-15")), "risk")
expect_setequal(out[[1]], "H1")
expect_setequal(out[[2]], c("H1", "Mid"))
expect_setequal(out[[3]], c("Mid", "H2"))
fclear()
})
test_that("auto-detect picks date_range when bounds are ISO dates", {
fparse(text = '
VALUE auto_dr
[2024-01-01, 2025-01-01) = "Y24"
;
')
expect_equal(format_get("auto_dr")$type, "date_range")
fclear()
})
test_that("fexport/fparse roundtrip for date_range", {
fparse(text = '
VALUE fyq (date_range)
[2024-01-01, 2024-04-01) = "Q1-24"
[2024-04-01, 2024-07-01) = "Q2-24"
;
')
fmt <- format_get("fyq")
txt <- fexport(fmt)
expect_true(grepl("date_range", txt))
expect_true(grepl("\\[2024-01-01, 2024-04-01\\)", txt))
fclear()
fparse(text = txt)
out <- fput(as.Date(c("2024-02-15", "2024-05-15")), "fyq")
expect_equal(out, c("Q1-24", "Q2-24"))
fclear()
})
context("Datetime Range Bucketing")
test_that("datetime_range buckets POSIXct (UTC) input", {
fparse(text = '
VALUE shift (datetime_range)
[2024-01-01 00:00:00, 2024-01-01 08:00:00) = "Night"
[2024-01-01 08:00:00, 2024-01-01 16:00:00) = "Day"
[2024-01-01 16:00:00, 2024-01-02 00:00:00) = "Evening"
;
')
y <- as.POSIXct(c("2024-01-01 03:00:00",
"2024-01-01 12:00:00",
"2024-01-01 20:00:00"), tz = "UTC")
expect_equal(fput(y, "shift"), c("Night", "Day", "Evening"))
fclear()
})
test_that("auto-detect picks datetime_range when bounds include time", {
fparse(text = '
VALUE auto_dtr
[2024-01-01 00:00:00, 2024-01-01 12:00:00) = "AM"
;
')
expect_equal(format_get("auto_dtr")$type, "datetime_range")
fclear()
})
test_that("date_range fast path engages for sorted disjoint bounds", {
fparse(text = '
VALUE fast (date_range)
[2024-01-01, 2024-04-01) = "Q1"
[2024-04-01, 2024-07-01) = "Q2"
[2024-07-01, 2024-10-01) = "Q3"
[2024-10-01, 2025-01-01) = "Q4"
;
')
rt <- format_get("fast")$range_table
expect_true(rt$sorted_disjoint)
expect_equal(length(rt$low), 4L)
fclear()
})
# ===========================================================================
# Stratified Range Builders
# ===========================================================================
context("Stratified Range Builders")
test_that("fmap_ranges builds canonical numeric keys", {
r <- fmap_ranges(c(0, 7, 14), c(7, 14, 30),
c("Wk0", "Wk1", "Wk2"))
expect_s3_class(r, "ks_fmap")
expect_equal(names(r), c("0,7,TRUE,FALSE", "7,14,TRUE,FALSE",
"14,30,TRUE,FALSE"))
expect_equal(unname(unclass(r)), c("Wk0", "Wk1", "Wk2"))
})
test_that("fmap_ranges supports Date and POSIXct bounds (ISO format)", {
d <- fmap_ranges(
low = as.Date(c("2024-01-01", "2024-02-01")),
high = as.Date(c("2024-02-01", "2024-03-01")),
label = c("Jan", "Feb")
)
expect_equal(names(d), c("2024-01-01,2024-02-01,TRUE,FALSE",
"2024-02-01,2024-03-01,TRUE,FALSE"))
p <- fmap_ranges(
low = as.POSIXct(c("2024-01-01 00:00:00"), tz = "UTC"),
high = as.POSIXct(c("2024-01-02 00:00:00"), tz = "UTC"),
label = "Day1"
)
expect_true(grepl("^2024-01-01 00:00:00,2024-01-02 00:00:00,TRUE,FALSE$",
names(p)))
})
test_that("fmap_ranges validates length and ordering", {
expect_error(fmap_ranges(c(0, 7), c(7), c("A", "B")),
"same length")
expect_error(fmap_ranges(c(7), c(0), "X"),
"<= corresponding")
})
test_that("fmap_ranges recycles scalar inclusivity", {
r <- fmap_ranges(c(0, 7), c(7, 14), c("A", "B"), inc_high = TRUE)
expect_equal(names(r), c("0,7,TRUE,TRUE", "7,14,TRUE,TRUE"))
})
test_that("fmap_strata prefixes stratum and carries strata_sep attribute", {
s <- fmap_strata(c("A", "A", "B"), c(0, 7, 0), c(7, 14, 7),
c("V1", "V2", "V1"), sep = "/")
expect_s3_class(s, "ks_fmap")
expect_equal(attr(s, "strata_sep"), "/")
expect_equal(names(s), c("A/0,7,TRUE,FALSE", "A/7,14,TRUE,FALSE",
"B/0,7,TRUE,FALSE"))
})
# ===========================================================================
# Stratified Range Lookup
# ===========================================================================
context("Stratified Range Lookup")
test_that("fnew builds per-stratum range tables", {
fclear()
fmt <- fnew(
fmap_strata(c("A", "A", "B"), c(0, 7, 0), c(7, 14, 10),
c("V1", "V2", "V1")),
type = "stratified_range", name = "vw"
)
expect_equal(fmt$type, "stratified_range")
expect_equal(fmt$range_subtype, "numeric")
expect_equal(names(fmt$range_tables), c("A", "B"))
expect_true(fmt$range_tables$A$sorted_disjoint)
expect_equal(length(fmt$range_tables$A$low), 2L)
fclear()
})
test_that("fputk applies stratified format with numeric values", {
fclear()
fnew(
fmap_strata(c("A", "A", "B"), c(0, 7, 0), c(7, 14, 10),
c("V1", "V2", "V1")),
type = "stratified_range", name = "vw", default = "OOR"
)
res <- fputk(c("A", "A", "B", "B", "C"), c(3, 10, 5, 99, 1),
format = "vw")
expect_equal(res, c("V1", "V2", "V1", "OOR", "OOR"))
fclear()
})
test_that("fputk applies stratified format with date subtype", {
fclear()
s <- fmap_strata(
stratum = c("S1", "S1", "S2"),
low = as.Date(c("2024-01-01", "2024-02-01", "2024-03-01")),
high = as.Date(c("2024-02-01", "2024-03-01", "2024-04-01")),
label = c("Jan", "Feb", "Mar")
)
fnew(s, type = "stratified_range", range_subtype = "date", name = "win")
v <- as.Date(c("2024-01-15", "2024-02-15", "2024-03-15", "2024-04-15"))
res <- fputk(c("S1", "S1", "S2", "S2"), v, format = "win")
expect_equal(res, c("Jan", "Feb", "Mar", "2024-04-15"))
fclear()
})
test_that("per-stratum .missing and .other override globals", {
fclear()
fnew(
fmap_strata(c("A", "A", "B"), c(0, 7, 0), c(7, 14, 7),
c("V1", "V2", "V1")),
".other|A" = "A_OOR",
".missing|A" = "A_MISS",
.other = "GLOBAL_OOR",
.missing = "GLOBAL_MISS",
type = "stratified_range", name = "vw"
)
expect_equal(
fputk(c("A", "A", "A", "B", "C"), c(3, 99, NA, 99, 99), format = "vw"),
c("V1", "A_OOR", "A_MISS", "GLOBAL_OOR", "GLOBAL_OOR")
)
fclear()
})
test_that("fput() refuses stratified formats and fputk requires 2+ args", {
fclear()
fnew(
fmap_strata("A", 0, 7, "V1"),
type = "stratified_range", name = "vw"
)
expect_error(fput("foo", "vw"), "fputk")
expect_error(fputk(c("A"), format = "vw"),
"at least 2 arguments")
fclear()
})
test_that("fparse parses stratified_range with friendly intervals", {
fclear()
txt <- '
VALUE visits (stratified_range, range_subtype: numeric)
"ARM_A"|[0, 7) = "Wk1"
"ARM_A"|[7, 14) = "Wk2"
"ARM_B"|[0, 10) = "Baseline"
".other|ARM_A" = "A_OOR"
.other = "GLOBAL_OOR"
;
'
fparse(text = txt)
fmt <- format_get("visits")
expect_equal(fmt$type, "stratified_range")
expect_equal(fmt$range_subtype, "numeric")
expect_equal(names(fmt$other_by_stratum), "ARM_A")
expect_equal(fmt$other_label, "GLOBAL_OOR")
expect_equal(
fputk(c("ARM_A", "ARM_A", "ARM_B", "ARM_C"),
c(3, 99, 5, 1), format = "visits"),
c("Wk1", "A_OOR", "Baseline", "GLOBAL_OOR")
)
fclear()
})
test_that("fparse handles custom strata_sep and date subtype", {
fclear()
txt <- '
VALUE dwin (stratified_range, range_subtype: date, strata_sep: /)
"S1"/[2024-01-01, 2024-02-01) = "Jan"
"S1"/[2024-02-01, 2024-03-01) = "Feb"
"S2"/[2024-03-01, 2024-04-01) = "Mar"
;
'
fparse(text = txt)
fmt <- format_get("dwin")
expect_equal(fmt$strata_sep, "/")
expect_equal(fmt$range_subtype, "date")
res <- fputk(
c("S1", "S1", "S2"),
as.Date(c("2024-01-15", "2024-02-15", "2024-03-15")),
format = "dwin"
)
expect_equal(res, c("Jan", "Feb", "Mar"))
fclear()
})
test_that("fexport / fparse roundtrip preserves stratified format", {
fclear()
fmt <- fnew(
fmap_strata(c("A", "A", "B"), c(0, 7, 0), c(7, 14, 7),
c("V1", "V2", "V1")),
".other|A" = "A_OOR",
.other = "GLOBAL_OOR",
type = "stratified_range", name = "vw"
)
txt <- fexport(fmt)
fclear()
fparse(text = txt)
fmt2 <- format_get("vw")
expect_equal(fmt2$mappings, fmt$mappings)
expect_equal(fmt2$range_subtype, fmt$range_subtype)
expect_equal(fmt2$strata_sep, fmt$strata_sep)
expect_equal(fmt2$other_by_stratum, fmt$other_by_stratum)
expect_equal(fmt2$other_label, fmt$other_label)
fclear()
})
test_that("print.ks_format renders stratified groups with headers", {
fclear()
fmt <- fnew(
fmap_strata(c("A", "A", "B"), c(0, 7, 0), c(7, 14, 7),
c("V1", "V2", "V1")),
type = "stratified_range", name = "vw"
)
out <- capture.output(print(fmt))
expect_true(any(grepl('Stratum "A":', out)))
expect_true(any(grepl('Stratum "B":', out)))
expect_true(any(grepl("\\[0, 7\\) => V1", out)))
fclear()
})
test_that("fmap_strata strata_sep is auto-picked up by fnew", {
fclear()
s <- fmap_strata(c("A", "B"), c(0, 0), c(7, 7), c("V1", "V1"),
sep = ":")
fmt <- fnew(s, type = "stratified_range", name = "vw")
expect_equal(fmt$strata_sep, ":")
expect_equal(fputk(c("A", "B"), c(3, 3), format = "vw"),
c("V1", "V1"))
fclear()
})
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.