Skip to content
heff

std.datetimezone

Time zones: which offset from UTC (std.offset) the clocks of a place are at when — +08:00 in Taipei all year round, -05:00 in New York in winter and -04:00 in summer — and so which instant a date and time on its clocks (std.localdatetime) is. NodaTime’s DateTimeZone is the model. A zone is its id, Asia/Taipei, and its rules: the changes of the tz database, each an instant and the offset after it, and a POSIX TZ rule for the years after the last of them. fromTZif reads a zone from the bytes of a TZif file (RFC 8536), as the tz database is kept on the system, std.tzdb reading them; fromPosix makes one from a POSIX rule alone, fixed from an offset.

An instant has one offset in a zone (offsetAt), but a date and time on its clocks may have none, where the clocks skipped over it, or two, where they went back over it (mapLocal): std.zoneddatetime resolves it, the program saying which instant each of those becomes. A leap second is no second here, as POSIX time has none. Import it by a name, import std.datetimezone as DateTimeZone, as its names are short.

import std.datetimezone as DateTimeZone
import std.instant as Instant
import std.offset as Offset
-- | the offset in New York at an instant, by its rule
-- | >>> inNewYork Instant.epoch
-- | "-05:00"
inNewYork : Instant.Instant -> String
inNewYork t =
maybe
""
(\z -> show (DateTimeZone.offsetAt z t))
(DateTimeZone.fromPosix "EST5EDT,M3.2.0,M11.1.0")

DateTimeZone

data DateTimeZone

A time zone: its id, the offset before its first change, its changes, and the rule after the last of them, if there is one.

Mapping

data Mapping
= Unique Offset.Offset
| Skipped Offset.Offset Offset.Offset Instant.Instant
| Ambiguous Offset.Offset Offset.Offset

What a date and time on a zone’s clocks is there: one instant, at an offset; none, where the clocks skipped over it, the offsets before and after the gap and the instant it ends at; or two, where they went back over it, the offsets of the earlier and the later.

utc

utc : DateTimeZone

UTC’s own, with no change: UTC.

id

id : DateTimeZone -> String

Its id: Asia/Taipei.

fixed

fixed : Offset.Offset -> DateTimeZone

The zone always at the offset, with no change, as NodaTime’s ForOffset: its id UTC and the offset, UTC+08:00, or UTC for none.

>>> List.map (\n -> maybe "" (\o -> id (fixed o)) (Offset.fromSeconds n)) [0, 28800, -34200]
["UTC", "UTC+08:00", "UTC-09:30"]

instanceEqualityDateTimeZone

instance instanceEqualityDateTimeZone : Equality DateTimeZone

The same id, as NodaTime has it.

>>> fixed Offset.utc == utc
True

instanceDebugDateTimeZone

instance instanceDebugDateTimeZone : Debug DateTimeZone

As show writes it: its id.

instanceShowDateTimeZone

instance instanceShowDateTimeZone : Show DateTimeZone

Its id, Asia/Taipei.

>>> show utc
"UTC"

instanceEqualityMapping

instance instanceEqualityMapping : Equality Mapping

The same, at the same offsets.

instanceDebugMapping

instance instanceDebugMapping : Debug Mapping

As show writes it.

instanceShowMapping

instance instanceShowMapping : Show Mapping

Its tag, then its offsets and its instant as they write them: Skipped -05:00 -04:00 2024-03-10T07:00:00Z.

fromPosix

fromPosix : String -> Maybe DateTimeZone

The zone of a POSIX TZ rule, EST5EDT,M3.2.0,M11.1.0, its id the rule: a name of three letters or more (or quoted, <+08>), its offset west of UTC (5 is -05:00, -8 is +08:00), then perhaps a daylight name, its offset (an hour ahead if none), and when each starts, Jn, n or Mm.w.d, perhaps a / and a time (02:00:00 if none), as RFC 8536 has them. With a daylight name and no changes, the United States’, as the tz code has them. Nothing if it writes none, or an offset beyond 18 hours.

>>> List.map (\s -> maybe "" id (fromPosix s)) ["CST-8", "<+0545>-5:45", "AEST-10AEDT,M10.1.0,M4.1.0/3"]
["CST-8", "<+0545>-5:45", "AEST-10AEDT,M10.1.0,M4.1.0/3"]
>>> (List.map (\s -> maybe True (\_ -> False) (fromPosix s)) ["", "CS-8", "CST", "CST-8CDT,M3.2.0", "CST-8CDT,M13.2.0,M11.1.0", "CST-8CDT,J0,J365", "CST19", "<+08-8"] : List Bool)
[True, True, True, True, True, True, True, True]

offsetAt

offsetAt : DateTimeZone -> Instant.Instant -> Offset.Offset

The offset of the zone at the instant.

>>> maybe "" (\z -> joinWith " " (List.map (\s -> show (offsetAt z (Instant.fromUnixNanoseconds (s * 1000000000)))) [1710054000, 1710053999, 1730613600, 1730613599])) (fromPosix "EST5EDT,M3.2.0,M11.1.0")
"-04:00 -05:00 -05:00 -04:00"
>>> List.map (\s -> maybe "" (\z -> show (offsetAt z Instant.epoch)) (fromPosix s)) ["CST-8", "<+0545>-5:45", "EST5EDT", "AEST-10AEDT,M10.1.0,M4.1.0/3", "<-03>3<-02>,M3.5.0/-2,M10.5.0/-1"]
["+08:00", "+05:45", "-05:00", "+11:00", "-03:00"]

mapLocal

mapLocal : DateTimeZone -> LocalDateTime.LocalDateTime -> Mapping

What the date and time on the zone’s clocks is: at one offset, the one instant (Unique); at none, where the clocks skipped over it (Skipped, the offsets before and after the gap, and the instant the gap ends at); at two, where the clocks went back over it (Ambiguous, the offset of the earlier instant, then the later’s).

>>> maybe [] (\z -> List.map (\s -> maybe "" (\t -> show (mapLocal z t)) (LocalDateTime.parse s)) ["2024-03-10T01:59:59", "2024-03-10T02:30:00", "2024-03-10T03:00:00", "2024-11-03T01:30:00", "2024-11-03T02:00:00"]) (fromPosix "EST5EDT,M3.2.0,M11.1.0")
["Unique -05:00", "Skipped -05:00 -04:00 2024-03-10T07:00:00Z", "Unique -04:00", "Ambiguous -04:00 -05:00", "Unique -05:00"]

fromTZif

fromTZif : String -> Bytes -> Either String DateTimeZone

The zone the bytes of a TZif file are (RFC 8536), as the tz database is kept on a system, its id name: of version 1, by its changes of 4 bytes; of version 2 and on, by its changes of 8 bytes and its footer’s POSIX rule for after them. Its leap seconds are not read. Why not if the bytes are none, or an offset is beyond 18 hours.

>>> either (\why -> why) (\z -> joinWith " " (List.map (\n -> show (offsetAt z (Instant.fromUnixNanoseconds n))) [-1, 0, 4102444800000000000])) (fromTZif "Etc/Example" b["TZif2", 0::64, 0::56, 0::32, 0::32, 0::32, 1::32, 2::32, 8::32, 0::32, 1, 29160::32, 0, 0, 28800::32, 0, 4, "LMT", 0, "CST", 0, "TZif2", 0::64, 0::56, 0::32, 0::32, 0::32, 1::32, 2::32, 8::32, 0::64, 1, 29160::32, 0, 0, 28800::32, 0, 4, "LMT", 0, "CST", 0, "\nCST-8\n"])
"+08:06 +08:00 +08:00"
>>> List.map (\b -> either (\why -> why) id (fromTZif "x" b)) [Bytes.fromString "TZ", b["TZif", 0::64, 0::64, 0::64, 0::64, 0::64]]
["no TZif header", "TZif with no local time type"]