Tzdb
Deterministic timezone arithmetic over a pinned, curated zone table.
The operating system’s timezone database is an ambient input, so consulting it would put a hidden mutable table between a program and its output. This package takes the other route: the zone data is ordinary Prism source, versioned with the package, so a conversion is a pure function of the instant, the zone value, and the package root. The table is a curated subset of the IANA database (tzdata_version names the release the rules were transcribed from), carrying each zone’s standard offset and, where one is observed, its daylight-saving rule in effect at that release. Historical transitions before the current rules are out of scope, and so is the full zone census; a program that needs either needs the real database, not a frozen copy of it.
Offsets are computed, not stored per instant: a Dst rule names its switch days the way legislation does (the second Sunday of March at 02:00 standard time), and the calendar arithmetic turns that into an exact UTC instant for any year. All arithmetic is over Time.Wall, nanoseconds since the Unix epoch, and is valid for instants from 1970 through 9999.
fn main() =
let noon = wall_of_nanos(1782907200 * 1000000000)
match find_zone("America/New_York") of
Some(z) => println(format_in_zone(noon, z))
None => println("unknown zone")
2026-07-01T08:00:00-04:00
Types
Basis
type Basis = Utc | Standard | Daylight deriving (Eq, Show)
The clock a switch’s at_minutes is read against: Utc (the European convention, every zone switching at one shared instant), Standard (the North American spring convention), or Daylight (the autumn convention, naming the wall clock that is actually showing when the switch happens).
Switch
type Switch = Switch {
month: Int,
week: Int,
weekday: Int,
at_minutes: Int,
basis: Basis
} deriving (Eq, Show)
One annual switch day, stated the way legislation states it: the weekth weekday of month (weeks 1 through 4, or 5 for the last), at at_minutes past midnight read against basis.
Dst
type Dst = Dst {
delta_minutes: Int,
starts: Switch,
ends: Switch
} deriving (Eq, Show)
A daylight-saving rule: the offset moves by delta_minutes between the starts and ends switches. A southern-hemisphere rule has starts later in the year than ends and spans the new year.
Zone
type Zone = Zone {
name: String,
abbrev_std: String,
abbrev_dst: String,
std_minutes: Int,
dst: Option(Dst)
} deriving (Eq, Show)
A timezone: an IANA-style name, standard and daylight abbreviations, the standard offset from UTC in minutes, and the daylight rule if one is observed.
Functions and Values
is_leap_year
is_leap_year : (Int) -> Bool
Whether year is a Gregorian leap year.
fn main() =
println(show(is_leap_year(2024)))
println(show(is_leap_year(2100)))
true
false
days_in_month
days_in_month : (Int, Int) -> Int
The number of days in month of year.
days_from_civil
days_from_civil : (Int, Int, Int) -> Int
Days since 1970-01-01 for a civil date. The inverse of civil_from_days.
fn main() = println(show(days_from_civil(1970, 1, 2)))
1
civil_from_days
civil_from_days : (Int) -> (Int, Int, Int)
The civil date (year, month, day) of a day count since 1970-01-01.
fn main() = println(show(civil_from_days(20270)))
(2025, 7, 1)
day_of_week
day_of_week : (Int) -> Int
The day of the week for a day count since 1970-01-01: 0 is Sunday through 6 is Saturday.
zone_name
zone_name : (Zone) -> String
A zone’s IANA-style name.
std_offset_minutes
std_offset_minutes : (Zone) -> Int
A zone’s standard (winter) offset from UTC in minutes.
offset_minutes_at
offset_minutes_at : (Zone, Time.Wall) -> Int
The zone’s offset from UTC, in minutes, at a UTC instant. This is the package’s central question; everything else is a projection of it.
New York is four hours behind UTC in July and five in January:
fn main() =
println(show(offset_minutes_at(america_new_york(), wall_of_nanos(1782907200 * 1000000000))))
println(show(offset_minutes_at(america_new_york(), wall_of_nanos(1768478400 * 1000000000))))
-240
-300
The switch is exact. United States daylight time in 2026 begins at 2026-03-08T07:00:00Z (02:00 standard time in the Eastern zone), and one second earlier is still winter:
fn main() =
println(show(offset_minutes_at(america_new_york(), wall_of_nanos(1772953199 * 1000000000))))
println(show(offset_minutes_at(america_new_york(), wall_of_nanos(1772953200 * 1000000000))))
-300
-240
is_dst
is_dst : (Zone, Time.Wall) -> Bool
Whether daylight-saving time is in effect in the zone at a UTC instant.
Sydney observes daylight time across the new year:
fn main() =
println(show(is_dst(australia_sydney(), wall_of_nanos(1768478400 * 1000000000))))
true
offset_at
offset_at : (Zone, Time.Wall) -> Time.Duration
The zone’s offset at an instant, as a Time.Duration.
abbrev_at
abbrev_at : (Zone, Time.Wall) -> String
The abbreviation showing on the zone’s clocks at an instant.
fn main() =
println(abbrev_at(america_new_york(), wall_of_nanos(1782907200 * 1000000000)))
EDT
to_local
to_local : (Zone, Time.Wall) -> Time.Wall
The local wall reading of a UTC instant in the zone: the same Wall shifted by the zone’s offset at that instant. Formatting it as UTC yields the zone’s local clock; it is a reading, not a new instant.
format_in_zone
format_in_zone : (Time.Wall, Zone) -> String
A UTC instant rendered as the zone’s local RFC 3339 timestamp, offset suffix included.
fn main() =
let noon = wall_of_nanos(1782907200 * 1000000000)
println(format_in_zone(noon, america_new_york()))
println(format_in_zone(noon, asia_tokyo()))
println(format_in_zone(noon, utc()))
2026-07-01T08:00:00-04:00
2026-07-01T21:00:00+09:00
2026-07-01T12:00:00+00:00
next_transition
next_transition : (Zone, Time.Wall) -> Option(Time.Wall)
The next offset switch strictly after an instant, or None for a zone that observes no daylight time.
From the middle of 2026, New York next switches when daylight time ends at 06:00 UTC on the first Sunday of November:
fn main() =
match next_transition(america_new_york(), wall_of_nanos(1782907200 * 1000000000)) of
Some(t) => println(show(wall_unix_secs(t)))
None => println("fixed")
1793512800
tzdata_version
tzdata_version : () -> String
The IANA tzdata release the table’s rules were transcribed from.
utc
utc : () -> Zone
Coordinated Universal Time itself, offset zero, no daylight rule.
europe_london
europe_london : () -> Zone
europe_lisbon
europe_lisbon : () -> Zone
europe_paris
europe_paris : () -> Zone
europe_berlin
europe_berlin : () -> Zone
europe_madrid
europe_madrid : () -> Zone
europe_rome
europe_rome : () -> Zone
europe_athens
europe_athens : () -> Zone
america_new_york
america_new_york : () -> Zone
america_chicago
america_chicago : () -> Zone
america_denver
america_denver : () -> Zone
america_phoenix
america_phoenix : () -> Zone
Arizona observes no daylight time, unlike the rest of the Mountain zone.
america_los_angeles
america_los_angeles : () -> Zone
america_anchorage
america_anchorage : () -> Zone
america_sao_paulo
america_sao_paulo : () -> Zone
Brazil abolished daylight time in 2019.
america_mexico_city
america_mexico_city : () -> Zone
Mexico abolished daylight time in 2022.
asia_tokyo
asia_tokyo : () -> Zone
asia_shanghai
asia_shanghai : () -> Zone
asia_kolkata
asia_kolkata : () -> Zone
India runs on a half-hour offset, five hours thirty minutes ahead of UTC.
asia_dubai
asia_dubai : () -> Zone
asia_singapore
asia_singapore : () -> Zone
australia_sydney
australia_sydney : () -> Zone
australia_brisbane
australia_brisbane : () -> Zone
Queensland observes no daylight time, unlike New South Wales.
pacific_auckland
pacific_auckland : () -> Zone
zones
zones : () -> List(Zone)
Every zone in the table, in name order.
find_zone
find_zone : (String) -> Option(Zone)
Look a zone up by its IANA-style name.
fn main() =
match find_zone("Asia/Kolkata") of
Some(z) => println(show(std_offset_minutes(z)))
None => println("unknown")
330
zone_names
zone_names : () -> List(String)
The names of every zone in the table, in order.