ISO 8601 dates from clinical data sources are often
truncated: a patient visit recorded as
"2020-03" means “sometime in March 2020”, and a value of
"2020" means “sometime in 2020”. Statistical analyses and
listings typically require a fully-specified POSIXct, so
the truncated value must be imputed before use.
ks_impute_dtc() handles this imputation in a vectorised,
tidyverse- compatible way. It returns a two-column tibble —
dt (POSIXct) and imputation_flag
(character) — that can be appended to a data frame with
dplyr::bind_cols() or
tidyr::unpack(mutate(...)).
Each element is matched against a regex that independently captures:
| Token | Regex group | Notes |
|---|---|---|
| Year | \d{4} |
Required — absence produces NA |
| Month | \d{2} |
Optional |
| Day | \d{2} |
Optional |
| Hour | \d{2} |
Optional; T or space separator required |
| Minute | \d{2} |
Optional |
| Second | \d{2} |
Optional; defaults to 0 when absent |
Trailing timezone designators (Z, +HH:MM,
-HHMM) are silently stripped; all output
is placed in the timezone given by tz (default
"UTC").
A string is complete when year, month, and day are
all present, and — if a time separator is present — hour and minute are
also present. Complete strings skip imputation and receive
imputation_flag = NA, except for date-only
strings under direction = "end" (Case D).
The flag records the most significant component that was imputed:
| Flag | Meaning |
|---|---|
"M" |
Month was imputed (year was the only component present) |
"D" |
Day was imputed (year and month were present) |
"H" |
Hour (and minute) were imputed |
"S" |
Minute was imputed (hour was already present) |
NA |
No imputation was performed |
| Direction | Missing month | Missing day | Missing hour | Missing minute |
|---|---|---|---|---|
"start" |
→ 1 | → 1 | → 00 | → 00 |
"end" |
→ 12 | → last day of month | → 23 | → 59 |
"no" |
— | — | — | — (returns NA) |
For "end" the last day of month is computed per-year, so
leap years are handled correctly (e.g. February 2020 →
day 29).
A date-only string such as "2020-04-06" contains no time
separator and is otherwise complete. Because the time was not recorded,
it is always considered imputed when direction != "no":
| Case | Direction | Time imputed | Flag |
|---|---|---|---|
| D | "end" |
23:59:00 |
"H" |
| E | "start" |
00:00:00 |
"H" |
A reference-date match (Case C) overrides both Case D and Case E.
When ref is supplied and direction != "no",
the function checks whether the parsed components match the reference
before applying the directional fallback. Cases are
evaluated in order:
| Case | Condition | Action | Flag |
|---|---|---|---|
| A | Day missing; year + month match ref | Take day, hour, minute from ref | "D" |
| B | Month missing; year matches ref | Take month, day, hour, minute from ref | "M" |
| C | Date complete, time absent; full date matches ref | Take hour, minute from ref (overrides Case D) | "H" |
| Fallback | None of A–C | Apply directional start/end values | — |
ref is recycled to length(x) and coerced to
POSIXct in tz before component extraction.
direction = "no" — no imputationks_impute_dtc(c("2020-03-15T10:30:00", # complete → NA flag
"2020-03-15", # date-only → NA flag
"2020-03", # truncated → NA dt
"bad"), # invalid → NA dt
direction = "no")
#> # A tibble: 4 × 2
#> dt imputation_flag
#> <dttm> <chr>
#> 1 2020-03-15 10:30:00 <NA>
#> 2 2020-03-15 00:00:00 <NA>
#> 3 NA <NA>
#> 4 NA <NA>direction = "start" — earliest plausible valuesks_impute_dtc(c("2020", # year only
"2020-03", # year-month
"2020-03-15", # date-only → 00:00 / H (Case E)
"2020-03-15T10"), # partial datetime
direction = "start")
#> # A tibble: 4 × 2
#> dt imputation_flag
#> <dttm> <chr>
#> 1 2020-01-01 00:00:00 M
#> 2 2020-03-01 00:00:00 D
#> 3 2020-03-15 00:00:00 H
#> 4 2020-03-15 10:00:00 Sdirection = "end" — latest plausible valuesks_impute_dtc(c("2020", # year only → 2020-12-31 23:59
"2020-03", # year-month → 2020-03-31 23:59
"2020-03-15", # date-only → 2020-03-15 23:59 (Case D)
"2020-03-15T10"), # partial → 2020-03-15 10:59
direction = "end")
#> # A tibble: 4 × 2
#> dt imputation_flag
#> <dttm> <chr>
#> 1 2020-12-31 23:59:00 M
#> 2 2020-03-31 23:59:00 D
#> 3 2020-03-15 23:59:00 H
#> 4 2020-03-15 10:59:00 SThe last day of the month is calculated per-year using
lubridate::days_in_month(), so leap years are always
handled correctly:
ks_impute_dtc(c("2020-02", # leap year → 2020-02-29
"2021-02", # non-leap → 2021-02-28
"2000-02", # century leap → 2000-02-29
"1900-02"), # century non-leap → 1900-02-28
direction = "end")
#> # A tibble: 4 × 2
#> dt imputation_flag
#> <dttm> <chr>
#> 1 2020-02-29 23:59:00 D
#> 2 2021-02-28 23:59:00 D
#> 3 2000-02-29 23:59:00 D
#> 4 1900-02-28 23:59:00 DWhen only the year and month are present and they match the reference, the day and time are taken from the reference rather than being set to the 1st or 31st.
ref_a <- as.POSIXct("2020-03-15 10:30:00", tz = "UTC")
ks_impute_dtc("2020-03", direction = "start", ref = ref_a)
#> # A tibble: 1 × 2
#> dt imputation_flag
#> <dttm> <chr>
#> 1 2020-03-15 10:30:00 D
# → 2020-03-15 10:30:00 / "D" (from ref, not 2020-03-01 00:00:00)
ks_impute_dtc("2020-04", direction = "start", ref = ref_a)
#> # A tibble: 1 × 2
#> dt imputation_flag
#> <dttm> <chr>
#> 1 2020-04-01 00:00:00 D
# → 2020-04-01 00:00:00 / "D" (month mismatch → fallback)ref_b <- as.POSIXct("2020-05-20 08:00:00", tz = "UTC")
ks_impute_dtc("2020", direction = "start", ref = ref_b)
#> # A tibble: 1 × 2
#> dt imputation_flag
#> <dttm> <chr>
#> 1 2020-05-20 08:00:00 M
# → 2020-05-20 08:00:00 / "M" (from ref)
ks_impute_dtc("2021", direction = "start", ref = ref_b)
#> # A tibble: 1 × 2
#> dt imputation_flag
#> <dttm> <chr>
#> 1 2021-01-01 00:00:00 M
# → 2021-01-01 00:00:00 / "M" (year mismatch → fallback)Case C takes priority over Case D: when the full date matches the reference, the reference time is used instead of 23:59.
ref_c <- as.POSIXct("2020-04-06 14:30:00", tz = "UTC")
# Same date as ref → take time from ref (Case C beats Case D)
ks_impute_dtc("2020-04-06", direction = "end", ref = ref_c)
#> # A tibble: 1 × 2
#> dt imputation_flag
#> <dttm> <chr>
#> 1 2020-04-06 14:30:00 H
# Different date → Case D applies → 23:59
ks_impute_dtc("2020-04-07", direction = "end", ref = ref_c)
#> # A tibble: 1 × 2
#> dt imputation_flag
#> <dttm> <chr>
#> 1 2020-04-07 23:59:00 Hks_impute_dtc() returns a tibble, so it integrates
naturally with dplyr::mutate() +
tidyr::unpack():
adsl_dtc <- tibble::tibble(
USUBJID = sprintf("01-%03d", 1:6),
RFSTDTC = c("2020-03-15T08:00", # complete
"2020-03-15", # date-only → Case D
"2020-03", # year-month
"2020", # year-only
"bad", # invalid
NA_character_), # missing
RFSTREF = as.POSIXct(
c("2020-03-15 08:00:00", "2020-03-15 14:30:00",
"2020-03-10 09:00:00", "2020-05-01 00:00:00",
NA, NA),
tz = "UTC"
)
)
adsl_dtc |>
mutate(imp = ks_impute_dtc(RFSTDTC, "start", ref = RFSTREF)) |>
tidyr::unpack(imp) |>
select(USUBJID, RFSTDTC, dt, imputation_flag)
#> # A tibble: 6 × 4
#> USUBJID RFSTDTC dt imputation_flag
#> <chr> <chr> <dttm> <chr>
#> 1 01-001 2020-03-15T08:00 2020-03-15 08:00:00 <NA>
#> 2 01-002 2020-03-15 2020-03-15 14:30:00 H
#> 3 01-003 2020-03 2020-03-10 09:00:00 D
#> 4 01-004 2020 2020-05-01 00:00:00 M
#> 5 01-005 bad NA <NA>
#> 6 01-006 <NA> NA <NA>The imputation_flag column can then be used to flag
records for medical review or to drive sensitivity analyses:
adsl_dtc |>
mutate(imp = ks_impute_dtc(RFSTDTC, "start", ref = RFSTREF)) |>
tidyr::unpack(imp) |>
filter(!is.na(imputation_flag)) |>
select(USUBJID, RFSTDTC, imputation_flag)
#> # A tibble: 3 × 3
#> USUBJID RFSTDTC imputation_flag
#> <chr> <chr> <chr>
#> 1 01-002 2020-03-15 H
#> 2 01-003 2020-03 D
#> 3 01-004 2020 M