SAS-style PROC FORMAT for R — value→label mappings, numeric ranges, reverse formatting (invalue), date/time/datetime, expression labels, multilabel & more
v0.3.4
Vladimir Larchenko · GPL-3
install.packages("ksformat")
Format Creation
fnew() — Discrete value→label VALUE
fnew(..., name=NULL, type="auto", default=NULL, multilabel=FALSE, ignore_case=FALSE) ... = named pairs key="label" | .missing/.other = special labels | name = register in library
fnew(
  "M" = "Male",
  "F" = "Female",
  .missing = "Unknown",
  .other = "Other Gender",
  name = "sex"
)
sexchar
MMale
FFemale
.missingUnknown
.otherOther Gender
finput() — Reverse (label→value) INVALUE
finput(..., name=NULL, target_type="numeric", missing_value=NA) ... = named pairs label=value | target_type = "numeric"|"character" output type
finput(
  "Male" = 1,
  "Female" = 2,
  name = "sex_inv"
)
sex_invinvalue→num
Male1
Female2
fnew_bid() — Bidirectional VALUE+INVALUE
fnew_bid(..., name=NULL, type="auto") Creates both a VALUE format and matching _inv INVALUE simultaneously
fnew_bid(
  "A" = "Active",
  "I" = "Inactive",
  name = "status"
)

Creates both:

"status" VALUE format

"status_inv" INVALUE

fnew_date() — Date/Time/Datetime format
fnew_date(pattern, name=NULL, type="auto", .missing=NULL) pattern = SAS format name (DATE9.) or R strftime pattern (%d.%m.%Y)
fnew_date("DATE9.", name = "bday")

fnew_date("%d.%m.%Y",
  name = "ru", type = "date")

fnew_date("MMDDYY10.",
  name = "us",
  .missing = "NO DATE")
bdaydate
Pattern: %d%b%Y (DATE9.)

SAS names auto-resolved; R patterns also supported

Options for fnew()
ParamDefaultWhat it does
nameNULLRegister in format library
type"auto""numeric"/"character" key type
multilabelFALSEAllow multiple labels per value
ignore_caseFALSECase-insensitive matching
.missingLabel for NA/NaN/""
.otherFallback for unmatched values
Format Application
fput() — Apply format (generic)
fput(x, format, ..., keep_na=FALSE) x = values | format = name or object | ... = extra args for expression labels
fput(
  c("M", "F", "M", NA, "X"),
  "sex"
)
"Male""Female""Male""Unknown""Other Gender"
fputn() fputc() — Typed apply
fputn(x, format_name, ...) · fputc(x, format_name, ...) Numeric/character shorthand — format_name is always a string name
fputn(c(5, 30, 70), "age")

fputc(c("M", "F"), "sex")
"Child""Adult""Senior"
"Male""Female"
fput_df() — Format data frame columns
fput_df(data, ..., suffix="_fmt", replace=FALSE) ... = col=format pairs | suffix = new column suffix | replace = overwrite original
fput_df(df,
  sex = format_get("sex"),
  age = format_get("age"),
  suffix = "_lbl"
)
sexagesex_lblage_lbl
M15MaleChild
F25FemaleAdult
NA35UnknownAdult
Case-insensitive matching
fnew(
  "M" = "Male",
  "F" = "Female",
  name = "sex_nc",
  ignore_case = TRUE
)
fput(c("m", "F", "M"), "sex_nc")
"Male""Female""Male"

"m" matches "M" with ignore_case

Missing Value Handling
#ConditionResult
NA / NaN / "".missing label
Exact matchMapped label
Range matchRange label
No match.other / original
is_missing() — Check for missing values
is_missing(x) Returns TRUE for NA, NaN, and empty string ""
is_missing(NA)    # TRUE
is_missing(NaN)   # TRUE
is_missing("")    # TRUE
is_missing("x")  # FALSE
TRUE
TRUE
TRUE
FALSE
keep_na option
fput(c("M", NA), "sex")

fput(c("M", NA), "sex",
  keep_na = TRUE)
"Male""Unknown"
"Male"NA
Text Parsing & Numeric Ranges
fparse() — Parse VALUE/INVALUE text
fparse(text=NULL, file=NULL) text = format definition string | file = path to .txt file with definitions
fparse(text = '
VALUE age (numeric)
  [0, 18)    = "Child"
  [18, 65)   = "Adult"
  [65, HIGH] = "Senior"
  .missing   = "Age Unknown"
;')
fputn(c(5,18,65,NA), "age")
"Child""Adult""Senior""Age Unknown"
Range Syntax Reference
SyntaxMeaningMath
[a, b]both inclusivea ≤ x ≤ b
[a, b)right exclusivea ≤ x < b
(a, b]left exclusivea < x ≤ b
(a, b)both exclusivea < x < b
HIGH+∞ upper bound
LOW−∞ lower bound
BMI with decimals + .missing
fparse(text = '
VALUE bmi (numeric)
  [0, 18.5)  = "Underweight"
  [18.5, 25) = "Normal"
  [25, 30)   = "Overweight"
  [30, HIGH] = "Obese"
  .missing   = "No data"
;')
bmiresult
16.2Underweight
22.7Normal
25.0Overweight
35.1Obese
NANo data
Exclusive / Inclusive bounds + .other
fparse(text = '
VALUE score (numeric)
  (0, 50]   = "Low"
  (50, 100] = "High"
  .other    = "Out of range"
;')
scorelabel
0Out of range
50Low
51High
101Out of range
Parse multiple formats at once
fparse(text = '
VALUE race (character)
  "W"="White" "B"="Black"
  "A"="Asian"
  .missing = "Unknown"
;
INVALUE race_inv
  "White"=1 "Black"=2
  "Asian"=3
;')

Registers both in library:

raceVALUE(char)
race_invINVALUE(num)
range_spec() — Build range strings
range_spec(low, high, label, inc_low=TRUE, inc_high=FALSE) Programmatic range key builder for fnew() — returns "low,high,inc_low,inc_high"
range_spec(0, 18)

range_spec(18, Inf,
  inc_high = FALSE)
"0,18,TRUE,FALSE"
"18,Inf,TRUE,FALSE"
Reverse Formatting (Invalue)
finputn() — Labels → Numeric
finputn(x, invalue_name) Applies named invalue, returns numeric vector
finputn(
  c("Male", "Female", "Unknown"),
  "sex_inv"
)
12NA
finputc() — Labels → Character
finputc(x, invalue_name) Applies named invalue, returns character vector
finputc(
  c("Active", "Pending"),
  "status_inv"
)
"A""P"
Round-trip: VALUE ↔ INVALUE
"A"
fputc "status"
"Active"
finputc "status_inv"
"A"
Date, Time & Datetime
SAS date formats (auto-resolved)
fputn(Sys.Date(), "DATE9.")
fputn(Sys.Date(), "MMDDYY10.")
fputn(Sys.Date(), "YYMMDD10.")
fputn(Sys.Date(), "WORDDATE.")
fputn(Sys.Date(), "QTR.")
format→ result
DATE9.18MAR2026
MMDDYY10.03/18/2026
YYMMDD10.2026-03-18
DDMMYY10.18/03/2026
MONYY7.MAR2026
WORDDATE.March 18, 2026
YEAR4.2026
QTR.1
R numeric dates (days since 1970-01-01)
days <- as.numeric(
  as.Date("2025-01-01")
)
fputn(days, "DATE9.")
fputn(days, "MMDDYY10.")

days = 20089

"01JAN2025"
"01/01/2025"
Time formats (seconds since midnight)
secsTIME8.TIME5.HHMM.
00:00:000:0000:00
36001:00:001:0001:00
4500012:30:0012:3012:30
8639923:59:5923:5923:59
Datetime formats
fputn(Sys.time(),
  "DATETIME20.")
format→ result
DATETIME20.18MAR2026:13:48:58
DATETIME13.18MAR26:13:48
DTDATE.18MAR2026
DTYYMMDD.2026-03-18
Custom + data frames
fnew_date("%d.%m.%Y",
  name = "ru_date")

fput(birthdays, "ru_date")
"25.03.1990""03.11.1985""14.07.2000"
v <- fnew_date("DATE9.",
  name = "vfmt",
  .missing = "NOT RECORDED")

fput_df(patients,
  visit_date = v)
idvisit_datevisit_fmt
12025-01-1010JAN2025
22025-02-1515FEB2025
3<NA>NOT RECORDED
PUTN workflow (SAS-style dynamic dispatch)
fnew(
  "1" = "date9.",
  "2" = "mmddyy10.",
  name = "wfmt",
  type = "numeric")

datefmt <- fputn(key, "wfmt")
date <- fputn(number, datefmt)
numkeyfmtdate
121031date9.03FEB2003
108992mmddyy10.11/07/1999
Parse date formats from text
fparse(text = '
VALUE enrldt (date)
  pattern = "DATE9."
  .missing = "Not Enrolled"
;
VALUE stamp (datetime)
  pattern = "DATETIME20."
;')
Format Library
fprint() — Inspect registered formats
fprint(name=NULL) No args = list all | name = show detail for one format
fprint()       # list all
fprint("sex")  # detail
ageVALUE(num)3
sexVALUE(char)2
sex_invINVALUE(num)2
format_get() fclear()
format_get(name) — retrieve format object   fclear(name=NULL) — remove one or all
fmt <- format_get("sex")

fclear("sex")  # remove one
fclear()       # clear all

Retrieve format object
Remove single format
Clear entire library

fexport() — Export to parseable text
fexport(..., formats=NULL, file=NULL) ... or formats = format objects | file = write to file (else returns string)
cat(fexport(bmi = bmi_fmt))
VALUE bmi (numeric) [0, 18.5) = "Underweight" [18.5, 25) = "Normal" [25, 30) = "Overweight" [30, HIGH] = "Obese" ;
fimport() — SAS CNTLOUT CSV
fimport(file, register=TRUE, overwrite=TRUE) file = CNTLOUT CSV path | register = auto-add to library | overwrite = replace existing
x <- fimport("cntlout.csv")

m <- fimport(csv,
  register = FALSE)

fput(v, m[["GENDER"]])
AGEGRPVALUE(num)
BMICATVALUE(num)
GENDERVALUE(char)
Multilabel Formats
fput_all() — One value → multiple labels multilabel=TRUE
fput_all(x, format, ..., keep_na=FALSE) Returns list of character vectors — all matching labels per value
fnew(
  "0,5,T,T"    = "Infant",
  "6,11,T,T"   = "Child",
  "12,17,T,T"  = "Adolescent",
  "0,17,T,T"   = "Pediatric",
  "18,64,T,T"  = "Adult",
  "65,Inf,T,T" = "Elderly",
  "18,Inf,T,T" = "Non-Pediatric",
  name = "age_cat",
  type = "numeric",
  multilabel = TRUE
)

fput_all(
  c(3, 14, 25, 70),
  "age_cat"
)
3
InfantPediatric
14
AdolescentPediatric
25
AdultNon-Pediatric
70
ElderlyNon-Pediatric
fput() → first match only · fput_all() → all matching labels (list)
AE severity grading (clinical)
fnew(
  "1,1,T,T" = "Mild",
  "2,2,T,T" = "Moderate",
  "3,3,T,T" = "Severe",
  "4,4,T,T" = "Life-threat.",
  "5,5,T,T" = "Fatal",
  "3,5,T,T" = "Serious",
  "1,2,T,T" = "Non-serious",
  name = "ae",
  type = "numeric",
  multilabel = TRUE
)
1
MildNon-serious
2
ModerateNon-serious
3
SevereSerious
4
Life-threat.Serious
5
FatalSerious
λ Expression Labels
sprintf with .x1 (evaluated at apply-time)
stat <- fnew(
  "n"   = "sprintf('%s', .x1)",
  "pct" = "sprintf('%.1f%%',
              .x1 * 100)",
  name = "stat"
)

fput(
  c("n", "pct", "n", "pct"),
  stat,
  c(42, .053, 100, .255)
)
"42""5.3%""100""25.5%"
Two args (.x1, .x2) + scalar recycling
r <- fnew(
  "ratio" = "sprintf('%s/%s',
              .x1, .x2)"
)
fput("ratio", r, 3, 10)

lbl <- fnew(
  "val" = "sprintf('%s(N=%s)',
             .x1, .x2)"
)
fput(c("val", "val"), lbl,
  c(42, 55), 100)
"3/10"
"42(N=100)""55(N=100)"

.x2=100 recycled for all elements

ifelse + mixed static/expression
sign <- fnew(
  "val" = "ifelse(.x1 > 0,
    paste0('+', .x1),
    as.character(.x1))"
)
fput(rep("val", 3), sign,
  c(5, 0, -3))

# Mixed: static + expression
fnew(
  "ok" = "OK",
  .other = "sprintf('Err(%s)',
               .x1)"
)
"+5""0""-3"
"OK""Err(timeout)"

"ok" → static · "E01" → .other expression

Vectorized Format Names
Each element uses a different format (SAS PUTC/PUTN)
fnew(
  "1" = "groupx",
  "2" = "groupy",
  "3" = "groupz",
  name = "typefmt",
  type = "numeric"
)
# define groupx, y, z...
respfmt <- fput(type,
  "typefmt")
word <- fputc(response,
  respfmt)
typerespfmtword
1positivegroupxagree
1negativegroupxdisagree
2positivegroupyaccept
2negativegroupyreject
3positivegroupzpass
3negativegroupzfail
ksformat v0.3.4 · Vladimir Larchenko · GPL-3 · install.packages("ksformat") · github.com/crow16384/ksformat