Temporal Types for Datalove Datalit

Status: Research / design proposal.

Design space for date, time, duration, and timestamp types in datalove datalit. All proposed types are Copy, fixed-size, no heap allocation, nanosecond precision, proleptic Gregorian calendar, no leap seconds.

Primary reference library: jiff. Secondary references: TC39 Temporal proposal, Apache Arrow temporal types.

Six types in three categories.

Civil (wall-clock, no timezone)

Type Size Align Description
date 4 2 Calendar date (year/month/day)
time 8 4 Wall-clock time (h/m/s/nanos)
datetime 12 4 Date + time combined

These carry no timezone or offset information. A datetime is a reading on a wall clock, not an absolute moment.

Instant (absolute, UTC)

Type Size Align Description
timestamp 16 8 Exact point in time, epoch-based

Represents an unambiguous instant on the timeline. Internally stored as seconds + nanoseconds since Unix epoch (UTC).

Duration

Type Size Align Description
duration 16 8 Flat signed elapsed nanoseconds
span 32 4 Calendar-aware (years/months/days/h/m/s)

duration is an exact physical elapsed time. span is a calendar-relative offset whose absolute length depends on context (e.g. "1 month" from January is 31 days, from February is 28 or 29).

Cross-Reference with Prior Art

Datalove jiff TC39 Temporal Arrow
date civil::Date PlainDate Date32
time civil::Time PlainTime Time64(ns)
datetime civil::DateTime PlainDateTime Timestamp(ns, None)
timestamp Timestamp Instant Timestamp(ns, "UTC")
duration SignedDuration -- Duration(ns)
span Span Duration Interval(MonthDayNano)

TC39 Temporal has no flat-nanosecond duration; its Duration is calendar-aware like our span. Arrow's Interval(MonthDayNano) stores months, days, and nanoseconds as three separate i32/i64 fields, similar in spirit to span.

In-Memory Layouts

#[repr(C)]
pub struct Date {
    pub year: i16,          // -32768..32767
    pub month: u8,          // 1..12
    pub day: u8,            // 1..31 (validated per month)
}

#[repr(C)]
pub struct Time {
    pub hour: u8,           // 0..23
    pub minute: u8,         // 0..59
    pub second: u8,         // 0..59
    pub _pad: u8,
    pub subsec_nanos: u32,  // 0..999_999_999
}

#[repr(C)]
pub struct DateTime {
    pub date: Date,         // 4 bytes at offset 0
    pub time: Time,         // 8 bytes at offset 4
}

#[repr(C)]
pub struct Timestamp {
    pub unix_seconds: i64,  // seconds since 1970-01-01T00:00:00Z
    pub subsec_nanos: i32,  // 0..999_999_999 (always non-negative)
}

#[repr(C)]
pub struct Duration {
    pub secs: i64,          // signed total seconds
    pub nanos: i32,         // 0..999_999_999 (sign carried by secs)
}

#[repr(C)]
pub struct Span {
    pub years: i32,
    pub months: i32,
    pub days: i32,
    pub hours: i32,
    pub minutes: i32,
    pub seconds: i32,
    pub nanos: i32,         // 0..999_999_999
    pub sign: i8,           // +1 or -1
    pub _pad: [u8; 3],
}

Notes on layout choices:

Literal Syntax

Keyword-prefix form, matching the existing atom/term pattern:

date 2024-06-19
time 14:30:00
time 14:30:00.123456789
datetime 2024-06-19T14:30:00
datetime 2024-06-19T14:30:00.5
timestamp 2024-06-19T14:30:00Z
timestamp 2024-06-19T14:30:00.123456789Z
duration PT1H30M
duration -PT5S
span P1Y2M3DT4H5M6S
span -P1Y

The value portion after each keyword follows ISO 8601 formatting:

Type annotations use the same keywords:

: date
: [timestamp]

Pretty-printing always uses keyword-prefix form. Subsecond digits are trimmed to the minimum needed (e.g. .5 not .500000000).

Timezone Handling

No timezone-aware type is proposed. Rationale:

Timestamp literals accept UTC offsets and normalize on parse:

timestamp 2024-06-19T14:30:00-04:00

parses and normalizes to:

timestamp 2024-06-19T18:30:00Z

The offset is consumed during parsing and not stored. Output always uses Z suffix.

What We're Not Including

Open Design Questions

Duration/Span boundary

Where to draw the line between duration (flat nanoseconds) and span (calendar-aware fields).

Option A: Strict separation. duration only accepts hours/minutes/seconds/nanos. Any literal mentioning days, months, or years requires span. Rationale: days are not always 24 hours (DST transitions), so duration P3D is misleading.

Option B: Permissive duration. duration accepts days (converting 1 day = 86400 seconds) but not months or years. This matches common intuition and jiff's SignedDuration::from_hours(24 * 3) pattern.

Option C: No duration type at all. Use span for everything. Simplifies the type system but loses the semantic distinction between "exact elapsed time" and "calendar offset."

Span total ordering

span values have no natural total order because their absolute length depends on a reference point (P1M could be 28-31 days).

Option A: Not orderable. span does not implement Ord or </>. Comparison is a type error. This is the TC39 Temporal approach. Cleanest semantically but may surprise users.

Option B: Lexicographic ordering. Compare fields top-down: years, then months, then days, then hours, etc. This gives a total order but it is not calendar-aware (P1M < P31D even though 1 month can be 31 days).

Option C: Approximate normalization. Normalize to an approximate nanosecond count using fixed conversion factors (1 month = 30.4375 days, 1 year = 365.25 days) and compare. Gives a "usually right" total order but can produce surprising results at boundary cases.

Span sign representation

How to represent the sign of a span.

Option A: Single overall sign. One sign: i8 field applies to the whole span. All magnitude fields are non-negative. This is the proposed layout above. Matches jiff's Span which has a single sign. Simplest invariant.

Option B: Per-field signs. Each field (years, months, days, etc.) independently signed. Allows mixed-sign spans like "1 year minus 2 months." More expressive but harder to normalize and reason about. TC39 Temporal requires all fields to have the same sign (a "balanced" span), effectively Option A.

Option C: Signed fields, normalized. Per-field signs allowed in construction, but always normalized to a canonical form where all fields share the same sign. Middle ground: accepts mixed input, stores uniform sign.