Dates & Times
Three stdlib files cover dates and times, each handling a different layer:
std/calendar— civil types:Date,Time,NaiveDateTime,OffsetDateTime,DateTime. What humans read off a calendar or clock face.std/instant—Instant, the machine timestamp. What hardware records.std/duration—Duration, an exact span of time (5 seconds,30 minutes).
The bare/attractive name in std/calendar is DateTime: when you say
“10:30 AM on June 15 in New York,” you mean a real moment in history, and a
DateTime is what models it (IANA-zoned, DST-aware). NaiveDateTime carries
the “Naive” prefix as a footgun marker — it’s a wall-clock reading without
a zone, useful for templates and unzoned data, not for “the meeting happens
at.”
Civil construction
Section titled “Civil construction”Each civil type has a typed-literal form. DateTime uses RFC 9557’s bracket
suffix to name the IANA zone:
import {
std/calendar: Date, DateTime, Error
std/io: IO
}
fn main(): Result<Unit, Error> {
d = try Date"2026-06-15"
IO.print(d)
dt = try DateTime"2026-06-15T14:30:00-04:00[America/New_York]"
IO.print(dt)
IO.print(DateTime.year(dt))
IO.print(DateTime.month(dt))
IO.print(DateTime.hour(dt))
Ok(Unit)
}
Civil periods
Section titled “Civil periods”Date shifts by civil calendar periods with + and -. Days and weeks move
by whole dates; months and years clamp when the target month has fewer days:
import {
std/calendar: Civil, Date, Error
std/io: IO
}
fn main(): Result<Unit, Error> {
start = try Date"2026-05-04"
jan31 = try Date"2026-01-31"
leap = try Date"2024-02-29"
IO.print(start + Civil.Days(10))
IO.print(start + Civil.Weeks(2))
IO.print(start - Civil.Weeks(1))
IO.print(jan31 + Civil.Months(1))
IO.print(jan31 - Civil.Months(1))
IO.print(leap + Civil.Years(1))
Ok(Unit)
}
Machine clock and durations
Section titled “Machine clock and durations”An Instant is a machine timestamp — what Instant.now() returns, what
Timer.sleep operates on. A Duration is an exact span of nanoseconds,
constructed with unit functions (Duration.seconds(5),
Duration.minutes(30)) and combined with +, -, *, and /:
import {
std/duration: Duration
std/instant: Instant
std/io: IO
}
fn main() {
start = Instant.from_seconds(1_700_000_000)
five_minutes = Duration.minutes(5)
IO.print(Duration.as_minutes(five_minutes))
later = start + five_minutes
IO.print(Instant.to_seconds(later))
// Instant.between returns the Duration between two Instants
gap = Instant.between(start, later)
IO.print(Duration.as_seconds(gap))
}
Instant.now() and Timer.sleep aren’t shown here because their output isn’t
deterministic — they read the real wall clock and pause for real time. See
the Concurrency chapter for Timer.sleep in practice.
Instant-only equality
Section titled “Instant-only equality”A DateTime holds a moment plus a display zone. Equality is instant-only
— same moment in two different zones compares equal, even though their
local readings differ:
import {
std/calendar: DateTime, Error
std/io: IO
}
fn main(): Result<Unit, Error> {
ny = try DateTime"2026-06-15T12:00:00-04:00[America/New_York]"
paris = try DateTime"2026-06-15T18:00:00+02:00[Europe/Paris]"
IO.print(ny == paris)
IO.print(ny)
IO.print(paris)
Ok(Unit)
}
DST is real
Section titled “DST is real”The 2026 spring-forward in America/New_York lands the small hours of March 8th. Adding one calendar day to noon March 7 lands at noon March 8 — the civil reading you’d want — even though the elapsed wall-clock time between the two moments is 23 hours, not 24:
import {
std/calendar: Civil, DateTime, Error
std/io: IO
}
fn main(): Result<Unit, Error> {
start = try DateTime"2026-03-07T12:00:00-05:00[America/New_York]"
next = start + Civil.Days(1)
IO.print(start)
IO.print(next)
Ok(Unit)
}
+ Civil.Days(1) does civil arithmetic — the local clock reads the same value
the next day, and the UTC offset shifts (-05:00 → -04:00) to account for
the DST transition. For exact-elapsed-time arithmetic (+24 real hours, which
lands at 1pm here), add a Duration instead.
Going deeper
Section titled “Going deeper”Five civil types
Section titled “Five civil types”std/calendar defines a ladder from less specific to more specific:
Date— calendar dayTime— time of dayNaiveDateTime— wall reading, no zone (not a real moment)OffsetDateTime— anchored at a fixed UTC offsetDateTime— anchored in an IANA zone (DST-aware)
Reading components
Section titled “Reading components”DateParts and TimeParts give uniform accessors across every type with
those fields:
import {
std/calendar: DateTime, Error
std/io: IO
}
fn main(): Result<Unit, Error> {
dt = try DateTime"2026-06-15T14:30:00-04:00[America/New_York]"
DateTime.year(dt) |> IO.print()
DateTime.month(dt) |> IO.print()
DateTime.day(dt) |> IO.print()
DateTime.hour(dt) |> IO.print()
DateTime.minute(dt) |> IO.print()
Ok(Unit)
}
Re-projecting a moment into another zone
Section titled “Re-projecting a moment into another zone”with_zone(dt, zone) keeps the underlying instant and re-renders the wall
reading through a different IANA zone:
import {
std/calendar: DateTime, Error
std/io: IO
}
fn main(): Result<Unit, Error> {
ny = try DateTime"2026-06-15T12:00:00-04:00[America/New_York]"
tokyo = try DateTime.with_zone(ny, "Asia/Tokyo")
IO.print(tokyo)
IO.print(ny == tokyo)
Ok(Unit)
}
DST: civil vs physical arithmetic
Section titled “DST: civil vs physical arithmetic”Civil arithmetic (+ Civil.Hours(24), - Civil.Hours(24)) adjusts the
wall reading and re-resolves through the zone. Physical arithmetic
(+ Duration, - Duration) advances the instant by exact elapsed time.
Around DST, they diverge:
import {
std/calendar: Civil, DateTime, Error
std/duration: Duration
std/io: IO
}
fn main(): Result<Unit, Error> {
// Saturday March 7 at noon in New York. DST springs forward overnight.
start = try DateTime"2026-03-07T12:00:00-05:00[America/New_York]"
// Civil: "+24 wall-clock hours" — stays at noon EDT. Only 23h physically.
civil = start + Civil.Hours(24)
IO.print(civil)
// Physical: "+24 hours of real time" — lands at 1pm EDT.
physical = start + Duration.hours(24)
IO.print(physical)
Ok(Unit)
}
Both are correct; the split lets the caller spell which intent applies.
DST: gaps and folds
Section titled “DST: gaps and folds”Projecting a wall reading into an IANA zone can hit a gap (the wall
reading never exists — clocks jumped over it) or a fold (the wall
reading exists twice — clocks rolled back). The Disambiguation enum
controls how in_zone resolves them:
import {
std/calendar: DateTime, Disambiguation, Error, NaiveDateTime
std/instant: Instant
std/io: IO
}
fn main(): Result<Unit, Error> {
// 2026-11-01 01:30 NY happens twice (2am EDT rolls back to 1am EST).
naive = try NaiveDateTime"2026-11-01T01:30:00"
earlier = try DateTime.in_zone(
naive,
"America/New_York",
Disambiguation.Earlier,
)
later = try DateTime.in_zone(naive, "America/New_York", Disambiguation.Later)
// Same wall reading, different offsets, different instants.
IO.print(earlier)
IO.print(later)
// One hour apart in the underlying instant.
earlier_sec = DateTime.to_instant(earlier) |> Instant.to_seconds()
later_sec = DateTime.to_instant(later) |> Instant.to_seconds()
IO.print(later_sec - earlier_sec)
// Instant-only equality: they are NOT equal.
IO.print(earlier == later)
Ok(Unit)
}
The default mode is Disambiguation.Compatible (total resolution matching
naive intuition — earlier in folds, advances forward through gaps).
Disambiguation.Reject is the strict mode for when the wall reading came
from user input and the right answer is to bounce the ambiguity back via
Err.
Reading the current time
Section titled “Reading the current time”DateTime.now_in(zone) reads the system clock and projects it into the
given zone in one call (sugar for Instant.now() |> from_instant_in(zone)):
import { std/calendar: Error std/io: IO } fn main(): Result<Unit, Error> { now = try DateTime.now_in("America/New_York") IO.print(now) // example: 2026-05-26T17:42:00-04:00[America/New_York] Ok(Unit) }
This snippet is static-only because the actual output varies with the wall clock.