Skip to content

std/time

std/time — six-type date/time module, jiff/NodaTime shape. Backs the seven time types registered as built-ins: Instant, Date, Time, DateTime, Zoned, Span, Duration, plus TimeZone. Each is meaning-distinct (docs/STDLIB-DESIGN-RESEARCH.md Rec §4): Instant — point in physical time, UTC, ns precision. Date — civil (year, month, day); no time, no zone. Time — civil wall-clock (h, m, s, ns); no date, no zone. DateTime — pair of (Date, Time); no zone. Zoned — Instant + TimeZone, fully qualified. Span — calendar-flavoured interval (years, months, …). Duration — absolute interval (sec + nsec). Conversions stay explicit: Date → Instant requires a TimeZone. Zoned → Date discards the zone. Span ↔ Duration only round-trips in the no-DST-shift case. Phase 1 (this PR): constants + the simplest constructors. Phase 2: Instant.now() via clock_gettime, basic arithmetic. Phase 3: Hinnant civil-date algorithms (add_days, weekday). Phase 4: RFC 3339 parse + format. Phase 5: TimeZone.iana(name) + Zoned operations. Phase 6: Span vs Duration calendar arithmetic.

pub const NANOS_PER_SECOND: i32

Unit conversion constants — used everywhere; pull them out so callers don’t repeat magic numbers.

pub const NANOS_PER_MILLI: i32
pub const NANOS_PER_MICRO: i32
pub const SECONDS_PER_MINUTE: i32
pub const SECONDS_PER_HOUR: i32
pub const SECONDS_PER_DAY: i32
pub const MINUTES_PER_HOUR: i32
pub const HOURS_PER_DAY: i32
pub const DAYS_PER_WEEK: i32
pub function instant_from_unix(sec: i64): Instant

instant_from_unix(sec) — build an Instant from a Unix epoch second count. Sub-second precision is left at zero; callers needing nanoseconds use the struct literal directly.

Negative sec is accepted (pre-1970 instants) and the nsec stays non-negative — same shape as clock_gettime.

pub function instant_now(): Instant

instant_now() — wall-clock now, UTC. Reads the system real-time clock through now_unix_ms() (which delegates to host-specific syscalls: wasi:clocks/wall-clock on the wasm target, Go’s time.Now() under the interp). Splits the result into (sec, nsec) — nsec only carries millisecond resolution since the underlying primitive caps there; future Phase 2.x can swap to a nanosecond- resolution call when the backends grow it.

NTP-adjustable: subject to backward clock jumps when the host adjusts time. Use monotonic_ns (also in this module’s future surface) for benchmark / elapsed-time measurement.

pub function date_make(year: i32, month: i32, day: i32): Date

date_make(year, month, day) — construct a civil Date. Doesn’t validate that the date is real (Feb 30 builds fine); validation lands with the Hinnant arithmetic in Phase 3.

pub function time_make(hour: i32, minute: i32, second: i32): Time

time_make(hour, minute, second) — construct a Time at second precision. Use the struct literal directly when you need nanosecond precision.

pub function datetime_make(d: Date, t: Time): DateTime

datetime_make(d, t) — pair a Date with a Time, no zone.

pub function timezone_utc(): TimeZone

timezone_utc() — the UTC zone constant. Offset zero. IANA zone lookup (timezone_iana(name)) lands in Phase 5.

pub function duration_seconds(s: i64): Duration

duration_seconds(s) — absolute interval of s seconds. Negative is allowed (start-after-end durations).

pub function duration_millis(ms: i64): Duration

duration_millis(ms) — convert milliseconds to a Duration. Splits whole-seconds + sub-second nsec so the storage matches the (sec, nsec) shape callers see in struct fields.

The 1000000 is NANOS_PER_MILLI inlined — modload’s flat-load path doesn’t yet plumb constants across modules, so references to the prelude-exposed const above resolve only at the qualified-import call site, not from inside the module’s own bodies. Use the literal until the const resolution gets fixed.

pub function span_days(n: i32): Span

span_days(n) — calendar-flavoured interval of n days. Convenience for the common “tomorrow” / “yesterday” use case (today.add_span(span_days(1)) once Phase 3 lands).

pub function span_hours(n: i32): Span

span_hours(n) — calendar-flavoured interval of n hours. Distinct from duration_seconds(n * 3600)span_hours(24) is “same wall-clock time tomorrow,” not “exactly 86400 seconds later,” and the two diverge across DST boundaries (once TimeZone arithmetic lands).

pub function is_leap_year(y: i32): boolean

is_leap_year(y) — Gregorian leap-year rule. Divisible by 4, except century years (divisible by 100) which must also be divisible by 400. 2000 yes, 1900 no, 2024 yes, 2100 no.

pub function days_in_month(y: i32, m: i32): i32

days_in_month(y, m) — calendar days for the given month. Handles February’s leap-year branch. Returns 0 for an out-of-range month — callers should .is_valid() first if they care.

pub function (d: Date) is_valid(): boolean

(d).is_valid() — does the (year, month, day) triple name a real Gregorian date? Year is unbounded (negative years are “BCE”, which Hinnant’s algorithm handles); month must be 1..12; day must be 1..days_in_month(year, month).

pub function (d: Date) add_days(n: i32): Date

(d).add_days(n) — calendar-add n days. Negative n walks backward. Round-trips through the serial day number so month / year / leap-year boundaries handle themselves.

pub function (d: Date) days_since(other: Date): i32

(d).days_since(other) — number of whole days from other to d. Positive when d is after other; negative when before. Subtracts serial day numbers — exact, no floating point.

pub function (d: Date) weekday(): i32

(d).weekday() — 0..6 with Sunday=0 (Hinnant convention, matches strftime %w). 1970-01-01 was a Thursday so __days_from_civil returns 0 for that date; we offset by 4 to land Thursday → 4. The ((x % 7) + 7) % 7 trick makes the result non-negative even when __days_from_civil returns a negative i32 (pre-1970 dates).

pub function (d: Date) day_of_year(): i32

(d).day_of_year() — 1..366. Jan 1 returns 1. Computed from Hinnant’s intra-year offset; doesn’t need the era machinery since the value only depends on (month, day) plus the leap-year flag.

Hinnant’s frame anchors at March 1 (so months map to mp = {Mar→0, Apr→1, …, Feb→11}); Jan/Feb of year Y are framed in year Y-1, so their frame-doy values 307..365 (+1 in leap) map to Jan-based 1..59 (+1). Convert with two cases: m ≤ 2: Jan-based = frame_doy - 306 m > 2: Jan-based = frame_doy + 59 + (is_leap_year(y) ? 1 : 0) Mar 1 is day 60 in non-leap, 61 in leap; Dec 31 is 365 / 366.

pub function (d: Date) format_iso(): string

(d: Date).format_iso() — emit YYYY-MM-DD. Doesn’t validate; callers should .is_valid() first if the date came from arithmetic that could produce nonsense.

pub function date_parse_iso(s: string): Option[Date]

date_parse_iso(s) — parse YYYY-MM-DD. Returns None on any deviation from the exact shape: wrong length, missing hyphens at offsets 4 and 7, or non-digit bytes in the year/month/day slices. Doesn’t check that the resulting date is calendar-valid (Feb 30 parses); callers use .is_valid() afterward.

pub function (i: Instant) format_rfc3339(): string

(i: Instant).format_rfc3339() — emit YYYY-MM-DDTHH:MM:SSZ (UTC). When i.nsec > 0, includes a fractional-seconds component .nnnnnnnnn (always 9 digits; callers wanting trimmed trailing zeros can post-process).

Built from the civil-date arithmetic: sec / 86400 → serial days → __civil_from_days → Y/M/D. Remainder gives H/M/S. nsec passes through unchanged.

Negative i.sec (pre-1970) works via the era trick in __civil_from_days; the day-rem-and-sec-rem math below uses floor division on negatives by adjusting when the remainder would otherwise come back negative.

pub function instant_parse_rfc3339(s: string): Option[Instant]

instant_parse_rfc3339(s) — parse the UTC form YYYY-MM-DDTHH:MM:SS[.fraction]Z. Returns None on any deviation. Doesn’t accept the lowercase t / z variants (RFC 3339 §5.6 allows them, but the canonical form dominates in the wild and accepting both invites case-mismatch bugs in callers comparing strings).

Zone offsets +HH:MM / -HH:MM land in Phase 5 with TimeZone.

pub function timezone_fixed_offset(offset_seconds: i32): TimeZone

timezone_fixed_offset(offset_seconds) — construct a fixed- offset TimeZone (no DST). Common use: ±09:00 (Japan), ±05:00 (Eastern US in winter), ±00:00 (UTC alias). The name field carries the canonical UTC+HH:MM / UTC-HH:MM representation for display.

Range: ±14:00:00 covers every real fixed offset (the IANA extreme is +14:00 Kiribati / -12:00 Baker Island). Out-of- range offsets get the same canonical-name treatment; we don’t reject them here.

pub function (i: Instant) in_zone(z: TimeZone): Zoned

(i: Instant).in_zone(z) — pair this UTC instant with a zone. Doesn’t change the absolute time; the Zoned shows the wall-clock time at the zone when subsequently formatted or decomposed.

pub function (z: Zoned) to_datetime(): DateTime

(z: Zoned).to_datetime() — split into the wall-clock DateTime at the zone. Drops the zone — callers needing the round-trip should hold onto the Zoned.

pub function (z: Zoned) format_rfc3339(): string

(z: Zoned).format_rfc3339() — emit YYYY-MM-DDTHH:MM:SS[.fraction]±HH:MM. UTC zones (offset zero) emit Z instead of +00:00, matching the canonical form. Other zones emit the signed offset; mirrors the shape instant_zoned_parse_rfc3339 round-trips against.

pub function instant_zoned_parse_rfc3339(s: string): Option[Zoned]

instant_zoned_parse_rfc3339(s) — parse the full RFC 3339 surface: YYYY-MM-DDTHH:MM:SS[.fraction](Z|±HH:MM). Returns the parsed Zoned (instant in UTC; zone holds the original offset). Z is a fixed-offset alias for +00:00 — the returned Zoned’s zone.name is "UTC" for that case, or "UTC±HH:MM" for explicit offsets.

Lowercase t/z still rejected (same strictness as instant_parse_rfc3339).

pub function span_seconds(n: i32): Span

Span constructors for the remaining unit grains. span_days and span_hours already exist (Phase 1). span_seconds, span_minutes, span_weeks, span_months, span_years round out the surface.

pub function span_minutes(n: i32): Span
pub function span_weeks(n: i32): Span
pub function span_months(n: i32): Span
pub function span_years(n: i32): Span
pub function (d: Date) add_span(s: Span): Date

(d: Date).add_span(s) — calendar-add. Years and months shift the year/month fields with month-overflow rollover, then the day clamps to days_in_month(new_year, new_month) to handle “Jan 31 + 1 month = Feb 28/29” and similar month-end-clamping cases. Weeks + days add as serial-day- number offsets (no calendar clamping; March 30 + 7 days = April 6).

Time fields (hours/minutes/seconds/nanos) on a Date are ignored — Date has no time component. Use the Phase 6 Instant.add_duration path for sub-day shifts.

pub function (i: Instant) add_duration(d: Duration): Instant

(i: Instant).add_duration(d) — absolute-add. Sums the (sec, nsec) pairs with nsec-overflow carry into sec. Negative durations (Duration { sec: -10, nsec: 0 }) walk backward.

pub function (i: Instant) duration_since(other: Instant): Duration

(i: Instant).duration_since(other) — absolute interval between two instants. Returns i - other. Positive when i is after other; negative otherwise. The nsec field is kept non-negative by borrow / carry, even for negative total durations.

pub function (d: Date) days_until(other: Date): Span

(d: Date).days_until(other) — days from d to other. The Span counterpart to days_since; the returned Span carries the result in the days field with all other fields zero. Year/month decomposition is a separate follow-up (it’s ambiguous — “1 month minus 3 days” could represent the same 28-day delta two ways).

pub function timezone_iana(name: string): Option[TimeZone]

timezone_iana(name) — lookup a known IANA zone by name. Returns a fixed-offset TimeZone for the zone’s standard- time offset. DST transitions are NOT modeled today — that’s the IANA tzdb integration follow-up (Phase 5.x.x). Zones where the local-time-in-summer matters (Europe / North America / Australia / NZ) will surface a wrong wall-clock during DST months; UTC-based handler code that just stamps timestamps doesn’t notice.

Coverage in this PR — the ~80% of wall-clock-relevant zones for edge-handler workloads: “UTC”, “GMT”, “Z” — UTC US: America/{New_York, Chicago, Denver, Los_Angeles, Anchorage, Honolulu, Phoenix} Europe: London, Dublin, Paris, Berlin, Madrid, Rome, Amsterdam, Moscow Asia: Tokyo, Shanghai, Hong_Kong, Singapore, Kolkata, Dubai, Bangkok Pacific: Auckland, Sydney, Melbourne, Honolulu Other: Africa/Cairo, America/Sao_Paulo, America/Toronto

Returns None for any name not in this table. Future Phase 5.x.x will load the IANA database (or a bundled subset) from disk with proper DST handling.