std/time
std/time
Section titled “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.
const NANOS_PER_SECOND
Section titled “const NANOS_PER_SECOND”pub const NANOS_PER_SECOND: i32Unit conversion constants — used everywhere; pull them out so callers don’t repeat magic numbers.
const NANOS_PER_MILLI
Section titled “const NANOS_PER_MILLI”pub const NANOS_PER_MILLI: i32const NANOS_PER_MICRO
Section titled “const NANOS_PER_MICRO”pub const NANOS_PER_MICRO: i32const SECONDS_PER_MINUTE
Section titled “const SECONDS_PER_MINUTE”pub const SECONDS_PER_MINUTE: i32const SECONDS_PER_HOUR
Section titled “const SECONDS_PER_HOUR”pub const SECONDS_PER_HOUR: i32const SECONDS_PER_DAY
Section titled “const SECONDS_PER_DAY”pub const SECONDS_PER_DAY: i32const MINUTES_PER_HOUR
Section titled “const MINUTES_PER_HOUR”pub const MINUTES_PER_HOUR: i32const HOURS_PER_DAY
Section titled “const HOURS_PER_DAY”pub const HOURS_PER_DAY: i32const DAYS_PER_WEEK
Section titled “const DAYS_PER_WEEK”pub const DAYS_PER_WEEK: i32instant_from_unix
Section titled “instant_from_unix”pub function instant_from_unix(sec: i64): Instantinstant_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.
instant_now
Section titled “instant_now”pub function instant_now(): Instantinstant_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.
date_make
Section titled “date_make”pub function date_make(year: i32, month: i32, day: i32): Datedate_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.
time_make
Section titled “time_make”pub function time_make(hour: i32, minute: i32, second: i32): Timetime_make(hour, minute, second) — construct a Time at
second precision. Use the struct literal directly when you
need nanosecond precision.
datetime_make
Section titled “datetime_make”pub function datetime_make(d: Date, t: Time): DateTimedatetime_make(d, t) — pair a Date with a Time, no zone.
timezone_utc
Section titled “timezone_utc”pub function timezone_utc(): TimeZonetimezone_utc() — the UTC zone constant. Offset zero.
IANA zone lookup (timezone_iana(name)) lands in Phase 5.
duration_seconds
Section titled “duration_seconds”pub function duration_seconds(s: i64): Durationduration_seconds(s) — absolute interval of s seconds.
Negative is allowed (start-after-end durations).
duration_millis
Section titled “duration_millis”pub function duration_millis(ms: i64): Durationduration_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.
span_days
Section titled “span_days”pub function span_days(n: i32): Spanspan_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).
span_hours
Section titled “span_hours”pub function span_hours(n: i32): Spanspan_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).
is_leap_year
Section titled “is_leap_year”pub function is_leap_year(y: i32): booleanis_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.
days_in_month
Section titled “days_in_month”pub function days_in_month(y: i32, m: i32): i32days_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.
is_valid
Section titled “is_valid”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).
add_days
Section titled “add_days”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.
days_since
Section titled “days_since”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.
weekday
Section titled “weekday”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).
day_of_year
Section titled “day_of_year”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.
format_iso
Section titled “format_iso”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.
date_parse_iso
Section titled “date_parse_iso”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.
format_rfc3339
Section titled “format_rfc3339”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.
instant_parse_rfc3339
Section titled “instant_parse_rfc3339”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.
timezone_fixed_offset
Section titled “timezone_fixed_offset”pub function timezone_fixed_offset(offset_seconds: i32): TimeZonetimezone_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.
in_zone
Section titled “in_zone”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.
to_datetime
Section titled “to_datetime”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.
format_rfc3339
Section titled “format_rfc3339”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.
instant_zoned_parse_rfc3339
Section titled “instant_zoned_parse_rfc3339”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).
span_seconds
Section titled “span_seconds”pub function span_seconds(n: i32): SpanSpan 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.
span_minutes
Section titled “span_minutes”pub function span_minutes(n: i32): Spanspan_weeks
Section titled “span_weeks”pub function span_weeks(n: i32): Spanspan_months
Section titled “span_months”pub function span_months(n: i32): Spanspan_years
Section titled “span_years”pub function span_years(n: i32): Spanadd_span
Section titled “add_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.
add_duration
Section titled “add_duration”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.
duration_since
Section titled “duration_since”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.
days_until
Section titled “days_until”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).
timezone_iana
Section titled “timezone_iana”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.