Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.