Skip to content

std/calendar

Civil date-time — the Date / Time / NaiveDateTime / OffsetDateTime / DateTime ladder. DateTime (IANA-zoned, DST-aware) is the safe default for real moments; NaiveDateTime’s prefix marks it as a wall-reading footgun. Components and arithmetic are exposed through the DateParts / TimeParts / Anchored interfaces plus Add impls for typed civil units; civil vs physical arithmetic diverge across DST.

Import with import std/calendar.

enum Error {
    InvalidFormat String
    InvalidValue String
    UnknownZone String
    Nonexistent String
    Ambiguous String
}

Error — what went wrong constructing or projecting a calendar value.

The first two variants (InvalidFormat, InvalidValue) cover parsing and construction across every civil type. The last three (UnknownZone, Nonexistent, Ambiguous) are only surfaced by the anchored projection cluster (OffsetDateTime, DateTime): UnknownZone when the zone name isn’t in the embedded tzdb; Nonexistent when a wall-clock reading falls in a spring-forward gap with mode = Reject; Ambiguous when a wall-clock reading falls in a fall-back fold with mode = Reject. (Other disambiguation modes resolve gaps/folds silently.)

impl Display

fn to_string(e: Error): String

impl Display.to_string

impl Debug

fn inspect(value: Error): String

impl Debug.inspect

interface DateParts {
    fn year(value: self): Int
    fn month(value: self): Int
    fn day(value: self): Int
}

Date-bearing values expose their year/month/day components. Impls: Date, NaiveDateTime, OffsetDateTime, DateTime.

interface TimeParts {
    fn hour(value: self): Int
    fn minute(value: self): Int
    fn second(value: self): Int
    fn nanosecond(value: self): Int
}

Time-of-day values expose their hour/minute/second/ nanosecond components. Impls: Time, NaiveDateTime, OffsetDateTime, DateTime.

interface Anchored {
    fn to_instant(value: self): Instant
    fn offset(value: self): Duration
}

Anchored-to-a-moment values expose their underlying Instant plus the effective UTC offset at that instant. Impls: OffsetDateTime, DateTime. Re-projecting the same instant through a different IANA zone is DateTime.with_zone(dt, zone), a DateTime-specific function — not in this interface because OffsetDateTime has a fixed offset, not a zone, and can’t sensibly re-zone.

type Years Int

Whole calendar years for civil calendar arithmetic on date-bearing values.

type Months Int

Whole calendar months for civil calendar arithmetic on date-bearing values.

type Weeks Int

Whole calendar weeks for civil calendar arithmetic on date-bearing values.

type Days Int

Whole calendar days for civil calendar arithmetic on date-bearing values.

type Hours Int

Whole civil hours for date-time wall-clock arithmetic.

type Minutes Int

Whole civil minutes for date-time wall-clock arithmetic.

type Seconds Int

Whole civil seconds for date-time wall-clock arithmetic.

type Milliseconds Int

Whole civil milliseconds for date-time wall-clock arithmetic.

type Microseconds Int

Whole civil microseconds for date-time wall-clock arithmetic.

type Nanoseconds Int

Whole civil nanoseconds for date-time wall-clock arithmetic.

struct Date

opaque — construction surface is private to its defining module

A calendar date — year/month/day, no time-of-day, no zone. Construct via Date.new(y, m, d) (validating), Date.parse, or the Date"…" typed literal. Components reachable via year/month/day (or the DateParts interface).

fn new(year: Int, month: Int, day: Int): Result<Date, Error>

type function on Date

Construct a Date, validating the combination (no Feb 30, leap-year aware). Returns Err(.InvalidValue(...)) for impossible dates.

fn parse(s: String): Result<Date, Error>

type function on Date

Parse an ISO-8601 date string (YYYY-MM-DD) into a Date. Backs the Date"…" typed literal and is exported for callers parsing from arbitrary string sources.

fn days_between(earlier: Date, later: Date): Int

type function on Date

Number of whole days between two Dates — positive if later is after earlier, negative if before. (Calendar-day count, ignoring any sub-day time of day.)

impl Literal

fn from_fragments(fragments: List<Fragment<String>>): Result<Date, Error>

impl Literal.from_fragments

impl Display

fn to_string(d: Date): String

impl Display.to_string

ISO-8601 date string (YYYY-MM-DD).

impl Debug

fn inspect(d: Date): String

impl Debug.inspect

impl DateParts

fn year(value: Date): Int

impl DateParts.year

impl DateParts

fn month(value: Date): Int

impl DateParts.month

impl DateParts

fn day(value: Date): Int

impl DateParts.day

impl Add

fn add(lhs: Date, rhs: Civil.Years): Date

impl Add.add

Add whole calendar years, clamping leap days when needed.

impl Subtract

fn subtract(lhs: Date, Civil.Years(n)): Date

impl Subtract.subtract

Subtract whole calendar years, clamping leap days when needed.

impl Equatable

fn equal?(a: Date, b: Date): Bool

impl Equatable.equal?

impl Hashable

fn hash(value: Date): Int

impl Hashable.hash

impl Comparable

fn compare(a: Date, b: Date): Ordering

impl Comparable.compare

struct Time

opaque — construction surface is private to its defining module

A time-of-day — hour/minute/second/nanosecond, no date, no zone. Construct via Time.new(h, m, s, ns) (validating), Time.parse, or the Time"…" typed literal. Components reachable via hour/minute/second/nanosecond (or the TimeParts interface).

fn new(hour: Int, minute: Int, second: Int = 0, nanosecond: Int = 0): Result<Time, Error>

type function on Time

Construct a Time, validating each component (hour 0..=23, minute/second 0..=59, nanosecond 0..=999_999_999).

fn parse(s: String): Result<Time, Error>

type function on Time

Parse a time-of-day string (HH:MM:SS or HH:MM:SS.fff…).

Interactive Tests

assert Time.parse("08:30:00") == Time"08:30:00"
assert Err(Error.InvalidFormat(_msg)) = Time.parse("nope")
fn between(earlier: Time, later: Time): Duration

type function on Time

Interactive Tests

assert Time.between(try Time"08:30:00", try Time"08:33:00") == Duration.minutes(
  3
)

impl Literal

fn from_fragments(fragments: List<Fragment<String>>): Result<Time, Error>

impl Literal.from_fragments

impl Display

fn to_string(t: Time): String

impl Display.to_string

ISO time string. Sub-second precision renders as a trailing .fff… only when nonzero (so 09:30:00, not 09:30:00.000).

impl Debug

fn inspect(t: Time): String

impl Debug.inspect

impl TimeParts

fn hour(value: Time): Int

impl TimeParts.hour

impl TimeParts

fn minute(value: Time): Int

impl TimeParts.minute

impl TimeParts

fn second(value: Time): Int

impl TimeParts.second

impl TimeParts

fn nanosecond(value: Time): Int

impl TimeParts.nanosecond

impl Add

fn add(lhs: Time, rhs: Duration): Time

impl Add.add

Add an exact Duration, wrapping around midnight.

Interactive Tests

assert try Time"08:33:00" + Duration.minutes(3) == try Time"08:36:00"

impl Subtract

fn subtract(lhs: Time, rhs: Duration): Time

impl Subtract.subtract

Subtract an exact Duration, wrapping around midnight.

Interactive Tests

assert try Time"08:36:00" - Duration.minutes(3) == try Time"08:33:00"

impl Equatable

fn equal?(a: Time, b: Time): Bool

impl Equatable.equal?

impl Hashable

fn hash(value: Time): Int

impl Hashable.hash

impl Comparable

fn compare(a: Time, b: Time): Ordering

impl Comparable.compare

struct NaiveDateTime

opaque — construction surface is private to its defining module

A naive date+time — a wall-clock reading like 2026-05-04T14:30:00, with no zone or offset. Looks like a complete timestamp but isn’t: a wall reading without a zone doesn’t name a real moment in history. Reach for DateTime (IANA-zoned) when you need a real instant; reach for NaiveDateTime only when the value is genuinely a wall reading (calendar templates, schedule patterns, dates with a “local time of day” that will be resolved into a zone later).

fn at(d: Date, t: Time): NaiveDateTime

type function on NaiveDateTime

Combine a Date and a Time. Both parts are already valid, so this is infallible.

fn at_midnight(d: Date): NaiveDateTime

type function on NaiveDateTime

A NaiveDateTime at midnight (00:00:00) on the given date.

fn to_date(dt: NaiveDateTime): Date

type function on NaiveDateTime

The date component.

fn to_time(dt: NaiveDateTime): Time

type function on NaiveDateTime

The time-of-day component.

fn new(year: Int, month: Int, day: Int, hour: Int, minute: Int, second: Int = 0, nanosecond: Int = 0): Result<NaiveDateTime, Error>

type function on NaiveDateTime

Construct from seven integers, validating each component AND the year/month/day combination.

fn parse(s: String): Result<NaiveDateTime, Error>

type function on NaiveDateTime

Parse an ISO naive datetime string (YYYY-MM-DDTHH:MM:SS[.fff…]). Allows a single space instead of T between the date and time.

fn between(earlier: NaiveDateTime, later: NaiveDateTime): Duration

type function on NaiveDateTime

Difference (later − earlier), computed as if both were UTC wall-readings.

impl Literal

fn from_fragments(fragments: List<Fragment<String>>): Result<NaiveDateTime, Error>

impl Literal.from_fragments

impl Display

fn to_string(dt: NaiveDateTime): String

impl Display.to_string

ISO datetime string (YYYY-MM-DDTHH:MM:SS[.fff…]).

impl Debug

fn inspect(dt: NaiveDateTime): String

impl Debug.inspect

impl DateParts

fn year(value: NaiveDateTime): Int

impl DateParts.year

impl DateParts

fn month(value: NaiveDateTime): Int

impl DateParts.month

impl DateParts

fn day(value: NaiveDateTime): Int

impl DateParts.day

impl TimeParts

fn hour(value: NaiveDateTime): Int

impl TimeParts.hour

impl TimeParts

fn minute(value: NaiveDateTime): Int

impl TimeParts.minute

impl TimeParts

fn second(value: NaiveDateTime): Int

impl TimeParts.second

impl TimeParts

fn nanosecond(value: NaiveDateTime): Int

impl TimeParts.nanosecond

impl Add

fn add(lhs: NaiveDateTime, rhs: Civil.Years): NaiveDateTime

impl Add.add

Add whole calendar years, clamping leap days when needed.

impl Subtract

fn subtract(lhs: NaiveDateTime, Civil.Years(n)): NaiveDateTime

impl Subtract.subtract

Subtract whole calendar years, clamping leap days when needed.

impl Equatable

fn equal?(a: NaiveDateTime, b: NaiveDateTime): Bool

impl Equatable.equal?

impl Hashable

fn hash(value: NaiveDateTime): Int

impl Hashable.hash

impl Comparable

fn compare(a: NaiveDateTime, b: NaiveDateTime): Ordering

impl Comparable.compare

struct OffsetDateTime

opaque — construction surface is private to its defining module

A date+time anchored at a fixed UTC offset (-05:00), with no DST awareness. The right type for ISO-8601 timestamps from APIs that include an offset but not a zone (2026-05-04T14:30:00-05:00). For DST-aware semantics, reach for DateTime instead — DateTime is the recommended default for real moments-in-time; an OffsetDateTime deliberately can’t tell you which zone you were in, so converting back to DateTime requires re-naming the zone.

Equality and ordering are instant-only: two OffsetDateTime values with the same underlying nanos but different offsets are equal (they’re the same moment in real history; the offset is display metadata). Construct via OffsetDateTime.with_offset(naive, offset), OffsetDateTime.from_instant(instant, offset), OffsetDateTime.parse, or the OffsetDateTime"…" typed literal.

fn with_offset(dt: NaiveDateTime, offset: Duration): Result<OffsetDateTime, Error>

type function on OffsetDateTime

Combine a NaiveDateTime wall-reading with a fixed UTC offset. Returns Err(.InvalidValue(...)) if the offset is outside the IANA-acceptable range (±18:00).

fn from_instant(i: Instant, offset: Duration): OffsetDateTime

type function on OffsetDateTime

Attach a fixed offset to an absolute Instant. Always total — every instant has a representation at every fixed offset.

fn parse(s: String): Result<OffsetDateTime, Error>

type function on OffsetDateTime

Parse an ISO-8601 timestamp with offset suffix (YYYY-MM-DDTHH:MM:SS[.fff…][±HH:MM|Z]).

fn between(earlier: OffsetDateTime, later: OffsetDateTime): Duration

type function on OffsetDateTime

impl Literal

fn from_fragments(fragments: List<Fragment<String>>): Result<OffsetDateTime, Error>

impl Literal.from_fragments

impl Display

fn to_string(odt: OffsetDateTime): String

impl Display.to_string

ISO-8601 datetime + offset string (YYYY-MM-DDTHH:MM:SS[.fff…]±HH:MM).

impl Debug

fn inspect(odt: OffsetDateTime): String

impl Debug.inspect

impl DateParts

fn year(value: OffsetDateTime): Int

impl DateParts.year

impl DateParts

fn month(value: OffsetDateTime): Int

impl DateParts.month

impl DateParts

fn day(value: OffsetDateTime): Int

impl DateParts.day

impl TimeParts

fn hour(value: OffsetDateTime): Int

impl TimeParts.hour

impl TimeParts

fn minute(value: OffsetDateTime): Int

impl TimeParts.minute

impl TimeParts

fn second(value: OffsetDateTime): Int

impl TimeParts.second

impl TimeParts

fn nanosecond(value: OffsetDateTime): Int

impl TimeParts.nanosecond

impl Anchored

fn to_instant(value: OffsetDateTime): Instant

impl Anchored.to_instant

impl Anchored

fn offset(value: OffsetDateTime): Duration

impl Anchored.offset

impl Add

fn add(lhs: OffsetDateTime, rhs: Duration): OffsetDateTime

impl Add.add

Add an exact Duration.

impl Subtract

fn subtract(lhs: OffsetDateTime, rhs: Duration): OffsetDateTime

impl Subtract.subtract

Subtract an exact Duration.

impl Equatable

fn equal?(a: OffsetDateTime, b: OffsetDateTime): Bool

impl Equatable.equal?

impl Hashable

fn hash(value: OffsetDateTime): Int

impl Hashable.hash

impl Comparable

fn compare(a: OffsetDateTime, b: OffsetDateTime): Ordering

impl Comparable.compare

struct DateTime

opaque — construction surface is private to its defining module

A date+time anchored to an IANA time zone (America/New_York), fully DST-aware. The default type for real moments in history — short name, attractive-by-design, the right choice for almost every timestamp a program handles. Backed by time/tzdata embedded in the runtime, so IANA-zone lookups work on every host without external tzdata.

Equality and ordering are instant-only: same moment in two different zones compares equal. The zone is display metadata, not identity.

Construct via DateTime.in_zone(naive, zone, mode) (the fallible wall→instant projection — see Disambiguation for the gap/fold modes), DateTime.from_instant_in(instant, zone) (always total), DateTime.now_in(zone) (the system clock projected into a zone), DateTime.parse, or the DateTime"…" typed literal (RFC 9557 bracket form).

fn in_zone(dt: NaiveDateTime, zone: String, mode: Disambiguation): Result<DateTime, Error>

type function on DateTime

Project a naive wall-reading into an IANA zone, returning a DateTime anchored at the resolved instant. Fails on UnknownZone always, and — only when mode = Reject — on Nonexistent (gap) or Ambiguous (fold). The three resolving modes (Compatible/Earlier/Later) always land on an instant.

fn from_instant_in(i: Instant, zone: String): Result<DateTime, Error>

type function on DateTime

Project an absolute Instant into an IANA zone. Always total once the zone is known. Fails only on UnknownZone.

fn now_in(zone: String): Result<DateTime, Error>

type function on DateTime

Read the current wall clock and project into a zone. Sugar for Instant.now() |> DateTime.from_instant_in(zone).

fn to_offset(d: DateTime): OffsetDateTime

type function on DateTime

Freeze the zone’s current offset at this instant — produces an OffsetDateTime carrying the offset effective at the DateTime’s instant. Lossy: the result no longer remembers the IANA zone.

fn with_zone(d: DateTime, zone: String): Result<DateTime, Error>

type function on DateTime

Same instant, different IANA zone — re-project the wall reading through a new zone. Fails only on UnknownZone.

fn parse(s: String): Result<DateTime, Error>

type function on DateTime

Parse an RFC 9557 datetime-with-zone string (YYYY-MM-DDTHH:MM:SS[.fff…]±HH:MM[Zone/Name]). The IANA-zone bracket suffix is required — bare offset forms parse via OffsetDateTime.parse instead.

fn between(earlier: DateTime, later: DateTime): Duration

type function on DateTime

impl Literal

fn from_fragments(fragments: List<Fragment<String>>): Result<DateTime, Error>

impl Literal.from_fragments

impl Display

fn to_string(d: DateTime): String

impl Display.to_string

RFC 9557 datetime+zone string.

impl Debug

fn inspect(d: DateTime): String

impl Debug.inspect

impl DateParts

fn year(value: DateTime): Int

impl DateParts.year

impl DateParts

fn month(value: DateTime): Int

impl DateParts.month

impl DateParts

fn day(value: DateTime): Int

impl DateParts.day

impl TimeParts

fn hour(value: DateTime): Int

impl TimeParts.hour

impl TimeParts

fn minute(value: DateTime): Int

impl TimeParts.minute

impl TimeParts

fn second(value: DateTime): Int

impl TimeParts.second

impl TimeParts

fn nanosecond(value: DateTime): Int

impl TimeParts.nanosecond

impl Anchored

fn to_instant(value: DateTime): Instant

impl Anchored.to_instant

impl Anchored

fn offset(value: DateTime): Duration

impl Anchored.offset

impl Add

fn add(lhs: DateTime, rhs: Duration): DateTime

impl Add.add

Add an exact Duration.

impl Subtract

fn subtract(lhs: DateTime, rhs: Duration): DateTime

impl Subtract.subtract

Subtract an exact Duration.

impl Equatable

fn equal?(a: DateTime, b: DateTime): Bool

impl Equatable.equal?

impl Hashable

fn hash(value: DateTime): Int

impl Hashable.hash

impl Comparable

fn compare(a: DateTime, b: DateTime): Ordering

impl Comparable.compare

enum Disambiguation {
    Compatible
    Earlier
    Later
    Reject
}

How DateTime.in_zone resolves wall-clock readings that fall in a DST spring-forward gap (the wall time never exists — clocks jumped over it) or a fall-back fold (the wall time exists twice — clocks rolled back). Modeled on Temporal’s disambiguation option.

The four modes:

  • Compatible (the default — pass no mode argument and you get this) — “do the obvious thing”, always returning an instant. In a fold, picks the earlier instant (the pre-transition occurrence). In a gap, advances forward through the gap so 02:30 becomes 03:30 post-transition. Matches the intuition that “30 minutes after the gap started” means “30 minutes of wall time after the gap’s start”. Reach for this unless you have a specific reason not to.

  • Earlier — pick the earlier instant in both transitions. In a fold, same as Compatible (the pre-transition occurrence). In a gap, the instant just before the transition: 02:30 in a spring-forward gap resolves to 01:30 pre-transition. Useful when “earlier is safer” — e.g. scheduling a deadline or notification where the earlier interpretation prevents a missed event.

  • Later — pick the later instant in both transitions. In a fold, the post-transition occurrence (01:30 EST after the fall-back). In a gap, the post-transition instant (same as Compatible for gaps). Useful when “later is safer” — e.g. expirations and grace periods, where the later interpretation gives the user the benefit of the doubt.

  • Reject — surface DST anomalies as Err instead of resolving silently: Err(Ambiguous ...) for folds, Err(Nonexistent ...) for gaps. The strict mode for code that should never silently disambiguate — typically when the wall reading came from a user (UI form, API request) and the right answer is to bounce the ambiguity back to them.

impl Debug

fn inspect(value: Disambiguation): String

impl Debug.inspect