"""
Zone and Offset (tag primitives), Observation, LocalResolution,
ResolveError, ZonedDateTime.
Per resolutions/4 (2026-05-24), Zone and Offset are tag primitives
with no instance data — they discriminate whether a ZonedDateTime's
internal representation refers to an IANA-named zone (DST rules apply)
or a fixed numeric offset (no DST). The actual data (name string or
offset int) lives on the ZonedDateTime itself.
Per resolutions/1, the IANA-bundled tzdata (via `_TzData`) is the only
data source. No TzProvider abstraction.
Per resolutions/2, ZonedDateTime is the only "moment in time" type —
no separate Instant. Construction via `now()` (UTC), `now_in_zone(name)`,
or `now_at_offset(sec)`. Stdlib bridge via `to_posix(): (I64, I64)`.
ZonedDateTime is REF — designed for in-place mutation in tight loops.
"""
use "time"
// Tag primitives. No instance data; discriminate the ZonedDateTime kind.
primitive Zone
"""
Tag: this ZonedDateTime is in an IANA-named zone (DST rules apply,
abbreviation comes from tzdata).
"""
primitive Offset
"""
Tag: this ZonedDateTime is at a fixed numeric offset, no zone
identity, no DST.
"""
type ZonedKind is (Zone | Offset)
// Observation: the zdump-comparable observable bundle for a (zone, instant)
// pair. Internal type used by ZonedDateTime; rarely seen by consumers
// directly (ZonedDateTime delegates accessors to it).
class val Observation
"""
Local fields + offset + abbreviation + isdst flag for a (zone, instant)
pair. Matches what zdump emits per transition record (used as the
comparison surface for differential testing against zdump).
"""
let _local_date: Date val
let _local_tod: TimeOfDay val
let _offset_sec: I32
let _abbreviation: String val
let _is_dst: Bool
new val create(
local_date': Date val,
local_tod': TimeOfDay val,
offset_sec': I32,
abbreviation': String val,
is_dst': Bool)
=>
_local_date = local_date'
_local_tod = local_tod'
_offset_sec = offset_sec'
_abbreviation = abbreviation'
_is_dst = is_dst'
fun val local_date(): Date val => _local_date
fun val local_tod(): TimeOfDay val => _local_tod
fun val offset_sec(): I32 => _offset_sec
fun val abbreviation(): String val => _abbreviation
fun val is_dst(): Bool => _is_dst
// DST resolution policy and error variants.
primitive ResolveStrict
"""DST gaps and overlaps are errors (LocalGap, LocalAmbiguous)."""
primitive ResolveEarliest
"""On overlap, pick the earlier UTC instant (pre-transition offset)."""
primitive ResolveLatest
"""On overlap, pick the later UTC instant (post-transition offset)."""
primitive ResolveNextValid
"""On gap, advance to the first valid local instant after the gap."""
type LocalResolution is
(ResolveStrict | ResolveEarliest | ResolveLatest | ResolveNextValid)
primitive LocalGap
"""Spring-forward gap and policy refused to advance."""
primitive LocalAmbiguous
"""Fall-back overlap and policy refused to pick."""
type ResolveError is (LocalGap | LocalAmbiguous)
// ZonedDateTime: the primary moment-in-time type.
class ZonedDateTime
"""
A UTC moment plus zone context (an IANA zone OR a fixed offset).
Storage: POSIX (sec, nsec) UTC + zone name (or "" for Offset mode) +
resolved Observation (local fields, offset, abbrev, isdst).
REF capability. Allocate once and reuse via `reset_to`,
`to_timezone_in_place`, etc. For cross-actor sharing, convert via
`to_posix()` + `zone_name()` (or `offset_sec()` when in Offset mode)
and reconstruct in the receiver actor.
## Construction patterns
ZonedDateTime.now() // UTC
ZonedDateTime.now_in_zone("America/Los_Angeles")? // IANA
ZonedDateTime.now_at_offset(-7 * 3600) // fixed offset
ZonedDateTime.from_posix((sec, nsec)) // reconstruct UTC
ZonedDateTime.from_posix_in_zone((sec, nsec), name)?
ZonedDateTime.from_posix_at_offset((sec, nsec), offset_sec)
"""
var _sec: I64
var _nsec: I64
var _kind: ZonedKind
var _zone_name: String val
var _observation: Observation val
new now() =>
"""Wall-clock now, in UTC."""
(_sec, _nsec) = Time.now()
_kind = Zone
_zone_name = "UTC"
_observation = _resolve_utc(_sec, _nsec)
new now_in_zone(name: String val) ? =>
"""
Wall-clock now, in the specified IANA zone. Errors if the zone
isn't in the bundled tzdata (use `try_zone(name)` on an existing
ZDT for richer error info).
"""
(_sec, _nsec) = Time.now()
_kind = Zone
_zone_name = name
_observation =
match _TzData.observation_at(name, _sec, _nsec)
| let o: Observation val => o
| let _: TzLookupError => error
end
new now_at_offset(offset_sec': I32) =>
"""Wall-clock now, at a fixed numeric offset (not an IANA zone)."""
(_sec, _nsec) = Time.now()
_kind = Offset
_zone_name = ""
_observation = _TzData.observation_for_offset(_sec, _nsec, offset_sec')
new from_posix(p: (I64, I64)) =>
"""Reconstruct a UTC ZonedDateTime from a POSIX tuple."""
(_sec, _nsec) = p
_kind = Zone
_zone_name = "UTC"
_observation = _resolve_utc(_sec, _nsec)
new from_posix_in_zone(p: (I64, I64), name: String val) ? =>
"""Construct a ZonedDateTime in an IANA zone from a POSIX tuple."""
(_sec, _nsec) = p
_kind = Zone
_zone_name = name
_observation =
match _TzData.observation_at(name, _sec, _nsec)
| let o: Observation val => o
| let _: TzLookupError => error
end
new from_posix_at_offset(p: (I64, I64), offset_sec': I32) =>
"""Construct a ZonedDateTime at a fixed offset from a POSIX tuple."""
(_sec, _nsec) = p
_kind = Offset
_zone_name = ""
_observation = _TzData.observation_for_offset(_sec, _nsec, offset_sec')
new _unchecked(
sec': I64, nsec': I64, kind': ZonedKind, zone_name': String val,
obs': Observation val)
=>
"""
Package-private constructor that bypasses validation. Used by
methods like `to_timezone` that have already done their own
consistency checks. Not callable from outside the package.
"""
_sec = sec'
_nsec = nsec'
_kind = kind'
_zone_name = zone_name'
_observation = obs'
// Mutation methods (in-place; no allocation).
fun ref reset_to(p: (I64, I64)): (None | TzLookupError) =>
"""
Mutate to represent a different UTC instant in the same zone (or
offset). For Zone mode, re-resolves against the bundled tzdata —
may fail if the new instant falls outside coverage, in which case
self is left unchanged.
"""
let new_sec = p._1
let new_nsec = p._2
match _kind
| Zone =>
match _TzData.observation_at(_zone_name, new_sec, new_nsec)
| let o: Observation val =>
_sec = new_sec
_nsec = new_nsec
_observation = o
None
| let e: TzLookupError => e
end
| Offset =>
_sec = new_sec
_nsec = new_nsec
_observation =
_TzData.observation_for_offset(_sec, _nsec, _observation.offset_sec())
None
end
fun ref to_timezone_in_place(name: String val): (None | TzLookupError) =>
"""
Mutate to represent the same UTC instant in a different IANA zone.
Returns ZoneNotFound or OutOfCoverage on failure; self is left
unchanged on error.
"""
match _TzData.observation_at(name, _sec, _nsec)
| let o: Observation val =>
_zone_name = name
_kind = Zone
_observation = o
None
| let e: TzLookupError => e
end
fun ref reset_at_offset(p: (I64, I64), offset_sec': I32) =>
"""
Mutate to a UTC instant at a fixed offset, switching this ZDT to
Offset mode. Symmetric with `from_posix_at_offset` but reuses
self — no allocation.
"""
_sec = p._1
_nsec = p._2
_kind = Offset
_zone_name = ""
_observation = _TzData.observation_for_offset(_sec, _nsec, offset_sec')
fun ref reset_in_zone(p: (I64, I64), name: String val)
: (None | TzLookupError)
=>
"""
Mutate to a UTC instant in an IANA zone, switching this ZDT to
Zone mode. Symmetric with `from_posix_in_zone`; self is left
unchanged on error.
"""
match _TzData.observation_at(name, p._1, p._2)
| let o: Observation val =>
_sec = p._1
_nsec = p._2
_kind = Zone
_zone_name = name
_observation = o
None
| let e: TzLookupError => e
end
fun box to_timezone(name: String val): (ZonedDateTime iso^ | TzLookupError) =>
"""
Return a NEW ZonedDateTime representing the same UTC instant in a
different IANA zone. Allocates one heap object on success.
"""
match _TzData.observation_at(name, _sec, _nsec)
| let o: Observation val =>
let sec' = _sec
let nsec' = _nsec
recover iso ZonedDateTime._unchecked(sec', nsec', Zone, name, o) end
| let e: TzLookupError => e
end
// Accessors.
fun box to_posix(): (I64, I64) => (_sec, _nsec)
fun box kind(): ZonedKind => _kind
fun box zone_name(): String val => _zone_name
fun box local_date(): Date val => _observation.local_date()
fun box local_tod(): TimeOfDay val => _observation.local_tod()
fun box offset_sec(): I32 => _observation.offset_sec()
fun box abbreviation(): String val => _observation.abbreviation()
fun box is_dst(): Bool => _observation.is_dst()
// Formatting.
fun box string(): String iso^ =>
"""
RFC 3339 / ISO 8601 representation of this ZonedDateTime.
Format: `YYYY-MM-DDTHH:MM:SS[.nnnnnnnnn][Z|±HH:MM]`
- Date part comes from `local_date().string()` (YYYY-MM-DD,
negative years prefixed with '-').
- Time part comes from `local_tod().string()` (HH:MM:SS, or
HH:MM:SS.nnnnnnnnn when sub-second precision is non-zero).
- Offset is `Z` for zero offset, or `±HH:MM` otherwise (sub-minute
offsets — LMT-era — are rounded for the suffix; precise value
remains available via `offset_sec()`).
The IANA zone name is NOT in the output — RFC 3339 carries an
offset, not a zone. Use `zone_name()` separately if you need it.
"""
let date_str = local_date().string()
let tod_str = local_tod().string()
let off_str = _TzData._offset_string(offset_sec())
recover iso
let buf = String(40)
buf.append(consume date_str)
buf.push('T')
buf.append(consume tod_str)
buf.append(consume off_str)
buf
end
// UTC is the one zone we know always resolves cleanly — extract the
// try/else for the (currently impossible) error path so the public
// UTC constructors stay non-partial.
fun tag _resolve_utc(sec: I64, nsec: I64): Observation val =>
match _TzData.observation_at("UTC", sec, nsec)
| let o: Observation val => o
| let _: TzLookupError =>
// Unreachable: _TzData hard-codes UTC.
Observation(Date.epoch(), TimeOfDay.midnight(), 0, "UTC", false)
end
// fun _observation_for_offset(sec: I64, nsec: I64, offset_sec: I32): Observation val =>
// """Compute a local Observation for a fixed-offset case (no tzdata
// lookup needed). Used by now_at_offset and reset_to (Offset case)."""
// // TODO: split (sec + offset_sec) back into Y/M/D H:M:S, format
// // abbreviation as the offset string ("-07:00" etc.)
// Observation(Date(1970,1,1)?, TimeOfDay(0,0,0)?, offset_sec, "", false)