Skip to content

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/instantInstant, the machine timestamp. What hardware records.
  • std/durationDuration, an exact span of time (5 seconds, 30 minutes).

The everyday civil type 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.”

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
}

fn main(): Result<Unit, Error> {
  d = try Date"2026-06-15"
  dbg d

  dt = try DateTime"2026-06-15T14:30:00-04:00[America/New_York]"
  dbg dt

  dbg DateTime.year(dt)
  dbg DateTime.month(dt)
  dbg DateTime.hour(dt)
  Ok(Unit)
}

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
}

fn main(): Result<Unit, Error> {
  start = try Date"2026-05-04"
  jan31 = try Date"2026-01-31"
  leap = try Date"2024-02-29"

  dbg start + Civil.Days(10)
  dbg start + Civil.Weeks(2)
  dbg start - Civil.Weeks(1)
  dbg jan31 + Civil.Months(1)
  dbg jan31 - Civil.Months(1)
  dbg leap + Civil.Years(1)
  Ok(Unit)
}

An Instant is a machine timestamp — an absolute point in time backed by Unix epoch nanoseconds. It’s what Instant.now() returns, and what you store when you need a moment independent of calendar names or time zones. A Duration is an exact span of nanoseconds — the value you add to an instant, compare between instants, or pass to Timer.sleep. Construct durations with unit functions (Duration.seconds(5), Duration.minutes(30)) and combine them with +, -, *, and /:

import {
  std/duration: Duration
  std/instant: Instant
}

fn main() {
  start = Instant.from_seconds(1_700_000_000)
  five_minutes = Duration.minutes(5)

  dbg Duration.as_minutes(five_minutes)

  later = start + five_minutes
  dbg Instant.to_seconds(later)

  // Instant.between returns the Duration between two Instants
  gap = Instant.between(start, later)
  dbg Duration.as_seconds(gap)

  Unit
}

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.

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
}

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]"

  dbg ny == paris
  dbg ny
  dbg paris
  Ok(Unit)
}

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. Adding an exact 24-hour Duration lands an hour later, because one wall-clock hour disappeared overnight:

import {
  std/calendar: Civil, DateTime, Error
  std/duration: Duration
}

fn main(): Result<Unit, Error> {
  start = try DateTime"2026-03-07T12:00:00-05:00[America/New_York]"
  civil = start + Civil.Days(1)
  physical = start + Duration.hours(24)

  dbg start
  dbg civil
  dbg physical
  Ok(Unit)
}

Both answers are correct. + Civil.Days(1) preserves the local calendar reading and re-resolves the UTC offset. + Duration.hours(24) advances the underlying instant by exactly 24 elapsed hours. The split lets the caller spell which intent applies.

std/calendar defines a ladder from less specific to more specific:

  • Date — calendar day
  • Time — time of day
  • NaiveDateTime — wall reading, no zone (not a real moment)
  • OffsetDateTime — anchored at a fixed UTC offset
  • DateTime — anchored in an IANA zone (DST-aware)

DateTime.with_zone(dt, zone) keeps the underlying instant and re-renders the wall reading through a different IANA zone:

import {
  std/calendar: DateTime, Error
}

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")
  dbg tokyo
  dbg ny == tokyo
  Ok(Unit)
}

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 DateTime.in_zone resolves them:

import {
  std/calendar: DateTime, Disambiguation, Error, NaiveDateTime
  std/instant: Instant
}

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.
  dbg earlier
  dbg 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()

  dbg later_sec - earlier_sec

  // Instant-only equality: they are NOT equal.
  dbg 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.

DateTime.now_in(zone) reads the system clock and projects it into the given zone in one call (sugar for Instant.now() |> DateTime.from_instant_in(zone)):

import {
  std/calendar: DateTime, Error
}

fn main(): Result<DateTime, Error> {
  now = try DateTime.now_in("America/New_York")
  dbg now // example: dbg line 6: now = 2026-05-26T17:42:00-04:00[America/New_York]
  Ok(now)
}

The output changes each time because this reads the real wall clock.