Partial dates

Impute year-only or year-month to a full date

SAS: partial --DTC / character dates; impute with INTNX beginning/end or a fixed mid day

Clinical data often arrive as incomplete ISO-like strings: year only (2025) or year-month (2024-04). SDTM keeps partial --DTC as collected. Imputing a full date and setting an imputation flag is ADaM work (for example ASTDTF, ADTF).

Derivation rules usually push these to the first, middle, or last day of the known period. Month bounds matter most for year-month; year-only is the same idea at year scale.

Always follow the study imputation convention. Examples below use generic STD-001-* subjects and one explicit mid rule.

Quick reference

library(dplyr)
library(lubridate)

# year-only "2025" -> first / mid / last day of year
ymd(paste0(year_txt, "-01-01"))
ymd(paste0(year_txt, "-06-30"))   # common mid-year convention
ymd(paste0(year_txt, "-12-31"))

# year-month "2024-04" -> first / mid / last day of month
ymd(paste0(ym_txt, "-01"))
ymd(paste0(ym_txt, "-15"))        # common mid-month convention
(ceiling_date(ymd(paste0(ym_txt, "-01")), "month") - days(1)) |> as.Date()

# AE start: only when known year-month equals first dose and first-of-month
# is still before dose, set ASTDT to RFXSTDT; otherwise keep first-of-period
# year-only partials: compare format(..., "%Y") instead of "%Y-%m"
ASTDT <- ymd(paste0(AESTDTC, "-01"))
if_else(
  format(ASTDT, "%Y-%m") == format(RFXSTDT, "%Y-%m") & ASTDT < RFXSTDT,
  RFXSTDT,
  ASTDT
)
R (after parsing the known part) SAS idea
first day of month/year impute to period start (INTNX beginning)
mid day (often 15 / 30 Jun) sponsor mid-point rule
last day of month/year impute to period end (INTNX end)

Traps: document the mid rule (15 vs true midpoint). ceiling_date(..., "month") alone is next month start, not month end. Missing completely stays missing. Imputation flags (ASTDTF, ADTF) are ADaM; SDTM keeps partial --DTC. Impute AE start to first dose only when the known year-month matches first dose and first-of-period is still before dose (year-only partials compare on "%Y"). Do not clamp every earlier month onto the dose date. The exact rule, including AE end-date checks, comes from the SAP.

Worked examples below.

Year-month to first, mid, last

Three partial year-month values, including February in a leap year.

library(dplyr)

Attaching package: 'dplyr'
The following objects are masked from 'package:stats':

    filter, lag
The following objects are masked from 'package:base':

    intersect, setdiff, setequal, union
library(lubridate)

Attaching package: 'lubridate'
The following objects are masked from 'package:base':

    date, intersect, setdiff, union
partial_ym <- data.frame(
  USUBJID = c("STD-001-0001", "STD-001-0001", "STD-001-0002"),
  DTC_YM  = c("2024-02", "2024-03", "2025-12")
)

partial_ym |>
  mutate(
    START = ymd(paste0(DTC_YM, "-01")),
    MID   = ymd(paste0(DTC_YM, "-15")),
    END   = (ceiling_date(ymd(paste0(DTC_YM, "-01")), "month") - days(1)) |>
      as.Date()
  )
       USUBJID  DTC_YM      START        MID        END
1 STD-001-0001 2024-02 2024-02-01 2024-02-15 2024-02-29
2 STD-001-0001 2024-03 2024-03-01 2024-03-15 2024-03-31
3 STD-001-0002 2025-12 2025-12-01 2025-12-15 2025-12-31
DTC_YM first mid (day 15) last
2024-02 2024-02-01 2024-02-15 2024-02-29
2024-03 2024-03-01 2024-03-15 2024-03-31
2025-12 2025-12-01 2025-12-15 2025-12-31

First day matches SAS INTNX('month', ..., 0, 'beginning') on a real date in that month. Last day matches INTNX(..., 'end'). Day 15 is a common mid-month convention; some protocols use other mid rules. Pick one and keep it consistent.

ImportantMonth end is not bare ceiling_date

ceiling_date(start, "month") is the first day of the next month. Last day of the current month is that value minus one day.

Year-only to first, mid, last

Year-only strings are easier: fixed calendar anchors.

partial_y <- data.frame(
  USUBJID = c("STD-001-0001", "STD-001-0002"),
  DTC_Y   = c("2024", "2025")
)

partial_y |>
  mutate(
    START = ymd(paste0(DTC_Y, "-01-01")),
    MID   = ymd(paste0(DTC_Y, "-06-30")),
    END   = ymd(paste0(DTC_Y, "-12-31"))
  )
       USUBJID DTC_Y      START        MID        END
1 STD-001-0001  2024 2024-01-01 2024-06-30 2024-12-31
2 STD-001-0002  2025 2025-01-01 2025-06-30 2025-12-31
DTC_Y first mid (30 Jun) last
2024 2024-01-01 2024-06-30 2024-12-31
2025 2025-01-01 2025-06-30 2025-12-31

30 June is a common mid-year convention (half-year boundary). Some conventions use 1 July or day 183. Document the choice the same way you document day 15 for months.

SAS analogs: INTNX('year', ..., 0, 'beginning') / 'end', or a fixed mid date constructed from the year.

Choosing first vs last vs mid

Situation Typical choice
Conservative "earliest possible" onset first of period
Conservative "latest possible" last of period
Neutral / midpoint rule in SAP mid (day 15 / 30 Jun or protocol rule)

Do not invent a mid rule in production code. Read the SAP or derivation specification. When an ADaM imputation flag is required, set ASTDTF / ADTF (or the study equivalent) to show which parts were imputed (for example D when day was imputed, M when month and day were imputed). Exact flag codes follow the ADaM spec and the study metadata.

Missing or unusable partial strings stay missing after failed parses. Do not silently coerce blank --DTC to period start.

Partial AE start in the same period as first dose

Impute year-month to the first of the month as usual. Move ASTDT to first dose only when the known year-month (or year) equals that of first dose and the first-of-period date is still before dose. A collected month that is already before first dose stays on the first of that month. Year-only partials compare on "%Y" instead of "%Y-%m". The exact rule, including AE end-date checks, comes from the SAP.

ae <- data.frame(
  USUBJID = c("STD-001-0001", "STD-001-0002", "STD-001-0003"),
  AESTDTC = c("2025-05", "2025-06", "2025-07"),
  RFXSTDT = as.Date(c("2025-06-03", "2025-06-03", "2025-06-03"))
)

ae |>
  mutate(
    ASTDT = ymd(paste0(AESTDTC, "-01")),
    ASTDT = if_else(
      format(ASTDT, "%Y-%m") == format(RFXSTDT, "%Y-%m") & ASTDT < RFXSTDT,
      RFXSTDT,
      ASTDT
    )
  )
       USUBJID AESTDTC    RFXSTDT      ASTDT
1 STD-001-0001 2025-05 2025-06-03 2025-05-01
2 STD-001-0002 2025-06 2025-06-03 2025-06-03
3 STD-001-0003 2025-07 2025-06-03 2025-07-01

STD-001-0001 is May, before the June dose, so ASTDT stays 2025-05-01. STD-001-0002 is the same month as dose, so ASTDT becomes 2025-06-03. STD-001-0003 is July, after dose, so ASTDT stays 2025-07-01.