use "time"
// 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 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
fun clone(): ZonedDateTime iso^ =>
"""
Return a fresh ZonedDateTime with the same state as this one — same
instant, same zone (or offset), same resolved Observation. Skips
the tzdata lookup since the Observation is already known.
Useful for handing the value across actors: the result is `iso^`,
so the caller can keep it as `iso`, recover to `val`, etc.
"""
let sec' = _sec
let nsec' = _nsec
let kind' = _kind
let zone_name' = _zone_name
let obs' = _observation
recover iso
ZonedDateTime._unchecked(sec', nsec', kind', zone_name', obs')
end
// Accessors.
fun to_posix(): (I64, I64) => (_sec, _nsec)
fun kind(): ZonedKind => _kind
fun zone_name(): String val => _zone_name
fun local_date(): Date val => _observation.local_date()
fun local_tod(): TimeOfDay val => _observation.local_tod()
fun offset_sec(): I32 => _observation.offset_sec()
fun abbreviation(): String val => _observation.abbreviation()
fun is_dst(): Bool => _observation.is_dst()
// Formatting.
fun 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