Time types
On this page
Store a moment as an Instant. The other types are for what an instant
isn't: a date with no time, a moment seen in a time zone, a length of
time, a zone, or a day of the week.
| Type | What it is | Example | On the wire |
|---|---|---|---|
Instant |
One point on the timeline, in UTC, to the millisecond | when a todo was created | "2026-01-15T12:00:00Z", "2026-01-15T12:00:00.250Z" |
CalendarDate |
A date with no time or zone | a birthday, an invoice date | "2026-10-25" |
ZonedTime |
An Instant seen in a Zone, for showing or reasoning about local time |
"09:30 in London" | { "instant": "2026-10-25T08:30:00Z", "zone": "Europe/London" } |
Duration |
Elapsed time | a timeout, a retention period | "PT5M" |
Zone |
An IANA time zone, a built-in enum | Zone.europe_london |
"Europe/London" |
Weekday |
A built-in enum | Weekday.monday |
"monday" |
Use CalendarDate only for facts that really are dates. These types have
no time of day on its own and no date and time without a zone: a
ZonedTime holds an instant and the zone it is seen in. For what has no
zone, such as a shop's opening time, use LocalTime and LocalDateTime
(Time and calendar additions).
They are never a moment until a zone is chosen.
Time types never turn into each other or into Text by themselves. A
function's parameters and results use named types built on them, such as
type DueAt = Instant {}
(Signatures use named types).
Literals
- An
Instantliteral is ISO 8601 with aZor an offset, to the millisecond:"2026-10-25T08:30:00Z". The compiler checks it. - A
CalendarDateliteral is a real date,"2026-10-25", also checked by the compiler. - A
Durationis written only with a unit,15m.min:andmax:on aDurationtype are enforced. - A
ZonedTimehas no literal.
Literals compare as times, whatever offset they are written with:
/// When an invoice falls due.type DueAt = Instant {}/// The day an invoice is dated.type InvoiceDate = CalendarDate {}test "time literals are checked while compiling and compare as time" { assert DueAt("2026-10-25T13:00:00+01:00") == DueAt("2026-10-25T12:00:00Z") assert InvoiceDate("2026-10-25") < InvoiceDate("2026-10-26")}
Precision and range
An Instant keeps milliseconds, the precision JavaScript clients, SQLite
and PostgreSQL all share. A literal with microseconds is refused, and writes
to PostgreSQL's microsecond column are always whole milliseconds.
An Instant holds the years 0000 to 9999, up to
9999-12-30T22:00:00.999Z, the last instant whose local date is in year
9999 in every zone. A CalendarDate holds the same years. A literal past
them is refused when the code compiles, and arithmetic that goes past them
is the fault time_out_of_range.
Zone, Weekday and Locale
These are built-in enums. Weekday has a variant for each day of the
week, such as Weekday.monday. Zone has a variant for each zone in the
IANA time zone database's zone.tab (2026d), plus utc, named in
lower_snake_case: Zone.america_argentina_buenos_aires.
Locale has a variant for each locale Unicode CLDR 48.2.3 covers fully,
plus, for each language without a region, the language with its likely
region (Locale.fr and Locale.fr_fr, Locale.en_gb). That is 533 in
all, and Locale.en_xa, the pseudo-locale
(Tooling for translations). Each is named in
lower_snake_case from its BCP 47 tag. Both tables change only in a change
of their own.
Comparing and storing a ZonedTime
Two ZonedTimes are equal when both their instants and their zones are.
They have no order of their own, since two in different zones have none.
Their instants do, through temporal.instant(...).
A ZonedTime field of an entity is stored in two columns: its instant as
timestamptz(3) and its zone's IANA name as text.
Where time can be read
clock.now in a function, a library function or an inline function is
type.clock_context (The current time).
clock.advance(...) outside a test is type.test_clock_context
(Controlling time). config.<name> is a
setting.
Markdown for agents: /docs/time-types.md.Built from Jadpo main at a707efb.