Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Timestamps and Dates

XPT files use the SAS date system for timestamps and dates. This page explains date handling in xportrs.

SAS Epoch

SAS uses January 1, 1960 as its epoch (day zero), different from Unix (1970):

graph LR
    subgraph "Date Epochs"
        SAS["SAS Epoch<br/>1960-01-01<br/>Day 0"]
        UNIX["Unix Epoch<br/>1970-01-01<br/>Day 3653"]
        TODAY["2024-01-15<br/>Day 23391"]
    end
    
    SAS --> |"3653 days"| UNIX
    UNIX --> |"19738 days"| TODAY

Date Types

TypeStorageUnitExample Format
Datef64Days since 1960-01-01DATE9.
Timef64Seconds since midnightTIME8.
DateTimef64Seconds since 1960-01-01 00:00:00DATETIME20.

Conversion Formulas

Date Conversions

#![allow(unused)]
fn main() {
use chrono::{NaiveDate, Datelike};

// SAS epoch
const SAS_EPOCH: NaiveDate = NaiveDate::from_ymd_opt(1960, 1, 1).unwrap();

/// Convert NaiveDate to SAS date number
fn to_sas_date(date: NaiveDate) -> f64 {
    (date - SAS_EPOCH).num_days() as f64
}

/// Convert SAS date number to NaiveDate
fn from_sas_date(sas_date: f64) -> NaiveDate {
    SAS_EPOCH + chrono::Duration::days(sas_date as i64)
}

// Examples:
// 1960-01-01 → 0
// 1970-01-01 → 3653
// 2024-01-15 → 23391
}

DateTime Conversions

#![allow(unused)]
fn main() {
use chrono::{NaiveDateTime, NaiveDate, NaiveTime};

/// Convert NaiveDateTime to SAS datetime number
fn to_sas_datetime(dt: NaiveDateTime) -> f64 {
    let epoch = NaiveDateTime::new(
        NaiveDate::from_ymd_opt(1960, 1, 1).unwrap(),
        NaiveTime::from_hms_opt(0, 0, 0).unwrap(),
    );
    (dt - epoch).num_seconds() as f64
}

/// Convert SAS datetime number to NaiveDateTime
fn from_sas_datetime(sas_dt: f64) -> NaiveDateTime {
    let epoch = NaiveDateTime::new(
        NaiveDate::from_ymd_opt(1960, 1, 1).unwrap(),
        NaiveTime::from_hms_opt(0, 0, 0).unwrap(),
    );
    epoch + chrono::Duration::seconds(sas_dt as i64)
}
}

Time Conversions

#![allow(unused)]
fn main() {
use chrono::NaiveTime;

/// Convert NaiveTime to SAS time number
fn to_sas_time(time: NaiveTime) -> f64 {
    time.num_seconds_from_midnight() as f64
}

/// Convert SAS time number to NaiveTime
fn from_sas_time(sas_time: f64) -> NaiveTime {
    let seconds = sas_time as u32;
    NaiveTime::from_num_seconds_from_midnight_opt(seconds, 0).unwrap()
}
}

Date Formats

Common Date Formats

FormatExample OutputDescription
DATE9.15JAN2024Standard SAS date
DATE7.15JAN24Short year
MMDDYY10.01/15/2024US format
DDMMYY10.15/01/2024European format
YYMMDD10.2024-01-15ISO format
E8601DA.2024-01-15ISO 8601

DateTime Formats

FormatExample Output
DATETIME20.15JAN2024:14:30:00
E8601DT.2024-01-15T14:30:00

Time Formats

FormatExample Output
TIME8.14:30:00
TIME5.14:30
HHMM.14:30

Using Dates in xportrs

Storing as Numeric with Format

#![allow(unused)]
fn main() {
use xportrs::{Column, ColumnData, Format};

// Calculate SAS date for 2024-01-15
let sas_date = 23391.0;  // Days since 1960-01-01

Column::new("AESTDT", ColumnData::F64(vec![Some(sas_date)]))
    .with_label("Start Date")
    .with_format_str("DATE9.")?
}

For SDTM submissions, dates are typically stored as ISO 8601 character strings:

#![allow(unused)]
fn main() {
use xportrs::{Column, ColumnData, Format};

// ISO 8601 date string
Column::new("AESTDTC", ColumnData::String(vec![Some("2024-01-15".into())]))
    .with_label("Start Date/Time of Adverse Event")
    .with_format(Format::character(19))
    .with_length(19)
}

[!TIP] SDTM uses --DTC variables (character) for dates/times, while ADaM often uses --DT/--TM (numeric) variables with date formats.

Partial Dates

SDTM allows partial dates in character variables:

PrecisionExampleDescription
Complete2024-01-15Full date
Month2024-01Unknown day
Year2024Unknown month/day
#![allow(unused)]
fn main() {
// Partial date examples
let dates = vec![
    Some("2024-01-15".to_string()),  // Complete
    Some("2024-01".to_string()),     // Month only
    Some("2024".to_string()),        // Year only
    None,                             // Missing
];

Column::new("AESTDTC", ColumnData::String(dates))
    .with_label("Start Date/Time")
    .with_format(Format::character(19))
}

File Timestamps

XPT files contain creation and modification timestamps in the dataset descriptor:

Position 48-63: Creation timestamp (ddMMMyy:hh:mm:ss)
Position 64-79: Modified timestamp (ddMMMyy:hh:mm:ss)

Example: "01JAN24:14:30:00"

Reading File Timestamps

#![allow(unused)]
fn main() {
use xportrs::Xpt;

let info = Xpt::inspect("ae.xpt")?;
if let Some(created) = &info.created {
    println!("Created: {}", created);
}
if let Some(modified) = &info.modified {
    println!("Modified: {}", modified);
}
}

Time Zone Considerations

[!WARNING] XPT files do not store time zone information. All times are assumed to be in the local time zone where the data was collected.

For SDTM submissions:

  • Store times in ISO 8601 format with explicit time zone when known
  • Document time zone assumptions in the Reviewer’s Guide

Best Practices

  1. Use ISO 8601 for SDTM: Store dates as character strings (AESTDTC) rather than numeric
  2. Use numeric for ADaM: ADaM analysis dates (ASTDT) are typically numeric with formats
  3. Document partial dates: Use imputation flags (AESTDTF) to indicate partial date handling
  4. Consider precision: Numeric dates have ~15 digit precision; sub-second precision may be lost

Reference