time: named time zones (design record first) #389

Closed
opened 2026-10-19 22:05:40 +00:00 by orion · 8 comments
Collaborator

Context

Last slice of FIRST-PARTY.md gap 11: named time zones (Europe/Berlin), with daylight saving. Too large and with too many open design questions to implement directly, so this issue's first deliverable is a design record. Depends on the wall-clock slice (DateTime, UtcOffset, OffsetDateTime).

What the design record has to decide

docs/design/TIME.md (new, with pdisc frontmatter, linked from the design index and from FIRST-PARTY.md gap 11), decided by the loop with measurements where there are numbers:

  • Where the tz data comes from: compiled into the toolchain/package (size per zone set, update path, tzdata is public domain), or the system's zoneinfo (needs the files capability and does not exist on Windows or in the playground), or both with a defined order. Numbers: binary size cost of the full database vs. a compact transition table.
  • The capability story: a TimeZone value must stay capability-free if the data is compiled in (capabilityOfImport, RELEASE.md 7.8).
  • Semantics of a local time in a gap (spring forward) and an overlap (fall back): explicit Disambiguation (Earlier, Later, Reject), no silent default except documented.
  • API: TimeZone.named(String): Result<TimeZone, TimeParseError>, ZonedDateTime, conversion to Timestamp/OffsetDateTime, Show as RFC 9557 (2026-10-25T02:30:00+01:00[Europe/Berlin]).
  • Which part is the post-2037 rule representation (POSIX TZ string) and how far the first version goes.

Acceptance criteria

  • docs/design/TIME.md exists with the decisions above, each with reasoning and numbers where measurable, and gap 11 in FIRST-PARTY.md links it
  • Follow-up implementation issues (with label loop, linked here) filed from the record, each small enough for one PR
  • Docs only: no seed impact; torb docs checks green in forge CI

Dependencies

#387 (wall-clock types, offsets).

## Context Last slice of [FIRST-PARTY.md gap 11](https://git.torb.dev/torbscript/language/src/commit/393ef2608e4a60b8ff76d8b6b408951b822d887d/docs/design/FIRST-PARTY.md#L271): named time zones (`Europe/Berlin`), with daylight saving. Too large and with too many open design questions to implement directly, so this issue's first deliverable is a design record. Depends on the wall-clock slice (`DateTime`, `UtcOffset`, `OffsetDateTime`). ## What the design record has to decide `docs/design/TIME.md` (new, with pdisc frontmatter, linked from the design index and from FIRST-PARTY.md gap 11), decided by the loop with measurements where there are numbers: - Where the tz data comes from: compiled into the toolchain/package (size per zone set, update path, tzdata is public domain), or the system's zoneinfo (needs the `files` capability and does not exist on Windows or in the playground), or both with a defined order. Numbers: binary size cost of the full database vs. a compact transition table. - The capability story: a `TimeZone` value must stay capability-free if the data is compiled in (`capabilityOfImport`, RELEASE.md 7.8). - Semantics of a local time in a gap (spring forward) and an overlap (fall back): explicit `Disambiguation` (`Earlier`, `Later`, `Reject`), no silent default except documented. - API: `TimeZone.named(String): Result<TimeZone, TimeParseError>`, `ZonedDateTime`, conversion to `Timestamp`/`OffsetDateTime`, `Show` as RFC 9557 (`2026-10-25T02:30:00+01:00[Europe/Berlin]`). - Which part is the post-2037 rule representation (POSIX TZ string) and how far the first version goes. ## Acceptance criteria - [ ] `docs/design/TIME.md` exists with the decisions above, each with reasoning and numbers where measurable, and gap 11 in FIRST-PARTY.md links it - [ ] Follow-up implementation issues (with label `loop`, linked here) filed from the record, each small enough for one PR - [ ] Docs only: no seed impact; `torb docs` checks green in forge CI ## Dependencies #387 (wall-clock types, offsets).
Author
Collaborator

[loop:torbscript] claim refine by session:loop-torbscript-20261025 until 2026-10-25T00:00:00Z

[loop:torbscript] claim refine by session:loop-torbscript-20261025 until 2026-10-25T00:00:00Z
Author
Collaborator

[loop:torbscript] intake: in scope (open, label loop, author orion). No duplicate: a keyword search of all issues (zone, tz, tzdata, daylight, time zone, zoneinfo) finds only the finished slices #111, #387 and #388 of gap 11. No collision with a design record: docs/design/ has no TIME record yet, FIRST-PARTY.md gap 11 names this issue as the one open slice, and its dependency #387 (DateTime, UtcOffset, OffsetDateTime) is closed. It is a design-record-first item, so the next stage is refine.

[loop:torbscript] intake: in scope (open, label `loop`, author `orion`). No duplicate: a keyword search of all issues (zone, tz, tzdata, daylight, time zone, zoneinfo) finds only the finished slices #111, #387 and #388 of gap 11. No collision with a design record: `docs/design/` has no TIME record yet, `FIRST-PARTY.md` gap 11 names this issue as the one open slice, and its dependency #387 (`DateTime`, `UtcOffset`, `OffsetDateTime`) is closed. It is a design-record-first item, so the next stage is refine.
Author
Collaborator

[loop:torbscript] decided by the loop: the design questions of this issue, so that docs/design/TIME.md records them and does not reopen them. No owner question applies (all of it is technical design inside std/time; nothing is public policy, cost, or a break of a published contract). Numbers are measured on tzdata 2026c compiled with zic -b slim (447 distinct zone files behind 598 names).

  1. Data source: compiled into std/time, no system zoneinfo. A generator tools/tzdata/ reads the pinned tzdata release and emits TorbScript constants, exactly like the Unicode tables (tools/fetch-unicode.sh pins every input file by SHA-256, the generated output is reproducible from them). Why: the system database is absent on Windows and in the playground, differs per machine (the same program would answer differently), and would need the files capability. tzdata is public domain. Update path: a new tzdata release is a regenerated data file and a std patch release; the module exposes the pinned version as a constant. A runtime override from the system's zoneinfo is not in std; it can come later as a separate files-capability package without changing TimeZone.
  2. Size. Full history, slim form: 235 395 bytes of TZif for the 447 distinct zones (52 623 bytes deflated); 18 657 transitions in total. A compact transition table (delta in minutes as a varint plus a one-byte type index, a type table per zone) is about 76 KB for the full history and about 51 KB from 1970 on; 525 of the 18 657 transitions are not on a whole minute, so seconds stay in the format (a flag, not a second encoding). Decision: the full history, because the 25 KB difference is small against a cliff at 1970 where a 1960 birth date in a named zone would silently answer wrong. Aliases (151 of the 598 names) are one name table that points at the zone index, not copies. The record has to say, by reading the compiler's handling of unused module constants, whether a program that imports only Timestamp pays for the data; if it does, the data goes to its own module that only TimeZone imports.
  3. Capability: TimeZone and ZonedDateTime are capability-free values, like Duration, Instant and Timestamp (RELEASE.md 7.8, "every imported name decides"). TimeZone.named is pure: the data is in the program. Only Timestamp.now() and its kin stay clock. The record adds the two names to the list in capabilityOfImport and docs/tooling/package-capabilities.md as a slice (the registry test compares both).
  4. Gap and overlap: Disambiguation is an enum of Earlier, Later and Reject, and it is a required argument, never a default. DateTime.inZone(zone, disambiguation): Result<ZonedDateTime, TimeParseError>. Worked example for Europe/Berlin: 2026-10-25 02:30 happens twice (+02:00, then +01:00): Earlier is the +02:00 instant, Later the +01:00 one. 2026-03-29 02:30 does not exist: Earlier reads it with the offset after the gap and gives 01:30+01:00, Later reads it with the offset before and gives 03:30+02:00 (both move by the gap's length); Reject fails in both cases. The infallible direction is Timestamp.inZone(zone) (an instant has exactly one local time).
  5. API shape as the issue lists it: TimeZone.named(String): Result<TimeZone, TimeParseError> (case-sensitive IANA names, UTC and fixed offsets via TimeZone.utc() and TimeZone.fixed(UtcOffset)), ZonedDateTime (instant, zone, the offset valid at that instant, derived DateTime), toTimestamp(), toOffsetDateTime(), Show as RFC 9557 (2026-10-25T02:30:00+01:00[Europe/Berlin]) and parse of the same, where the offset must be valid in the zone at that local time (it picks the instant in an overlap; otherwise the parse fails). Calendar arithmetic (days, months) works on the local time and takes a Disambiguation; Duration arithmetic works on the instant. Details and names of the remaining members belong to the record.
  6. Future years: the first version includes the POSIX TZ rule representation. The slim data lists transitions only until the rule takes over; without the rule every date after the last stored transition would be wrong. Numbers: 127 of the 447 zones end in a rule, 31 distinct rule strings, 320 zones are fixed. All rules use the Mm.w.d[/time] form; the time can be outside 0 to 23 (/24, /-1, /2:45) and some zones have a negative DST. This is cheaper than the alternative of expanding to 2037 (the fat form stores 27 184 transitions, 46 % more, and is still wrong from 2038). No Jn or n day forms are needed by the data; the record says so and the parser rejects them.

Seed impact: none for the record (docs only). The record's slices table must flag it per slice; the expected answer is none, since the data is TorbScript constants and the generator is a .trb program.

[loop:torbscript] decided by the loop: the design questions of this issue, so that `docs/design/TIME.md` records them and does not reopen them. No owner question applies (all of it is technical design inside `std/time`; nothing is public policy, cost, or a break of a published contract). Numbers are measured on tzdata 2026c compiled with `zic -b slim` (447 distinct zone files behind 598 names). 1. **Data source: compiled into `std/time`, no system zoneinfo.** A generator `tools/tzdata/` reads the pinned tzdata release and emits TorbScript constants, exactly like the Unicode tables (`tools/fetch-unicode.sh` pins every input file by SHA-256, the generated output is reproducible from them). Why: the system database is absent on Windows and in the playground, differs per machine (the same program would answer differently), and would need the `files` capability. tzdata is public domain. Update path: a new tzdata release is a regenerated data file and a `std` patch release; the module exposes the pinned version as a constant. A runtime override from the system's zoneinfo is not in `std`; it can come later as a separate `files`-capability package without changing `TimeZone`. 2. **Size.** Full history, slim form: 235 395 bytes of TZif for the 447 distinct zones (52 623 bytes deflated); 18 657 transitions in total. A compact transition table (delta in minutes as a varint plus a one-byte type index, a type table per zone) is about 76 KB for the full history and about 51 KB from 1970 on; 525 of the 18 657 transitions are not on a whole minute, so seconds stay in the format (a flag, not a second encoding). Decision: the full history, because the 25 KB difference is small against a cliff at 1970 where a 1960 birth date in a named zone would silently answer wrong. Aliases (151 of the 598 names) are one name table that points at the zone index, not copies. The record has to say, by reading the compiler's handling of unused module constants, whether a program that imports only `Timestamp` pays for the data; if it does, the data goes to its own module that only `TimeZone` imports. 3. **Capability: `TimeZone` and `ZonedDateTime` are capability-free values**, like `Duration`, `Instant` and `Timestamp` (RELEASE.md 7.8, "every imported name decides"). `TimeZone.named` is pure: the data is in the program. Only `Timestamp.now()` and its kin stay `clock`. The record adds the two names to the list in `capabilityOfImport` and `docs/tooling/package-capabilities.md` as a slice (the registry test compares both). 4. **Gap and overlap: `Disambiguation` is an enum of `Earlier`, `Later` and `Reject`, and it is a required argument, never a default.** `DateTime.inZone(zone, disambiguation): Result<ZonedDateTime, TimeParseError>`. Worked example for Europe/Berlin: 2026-10-25 02:30 happens twice (+02:00, then +01:00): `Earlier` is the +02:00 instant, `Later` the +01:00 one. 2026-03-29 02:30 does not exist: `Earlier` reads it with the offset after the gap and gives 01:30+01:00, `Later` reads it with the offset before and gives 03:30+02:00 (both move by the gap's length); `Reject` fails in both cases. The infallible direction is `Timestamp.inZone(zone)` (an instant has exactly one local time). 5. **API shape** as the issue lists it: `TimeZone.named(String): Result<TimeZone, TimeParseError>` (case-sensitive IANA names, `UTC` and fixed offsets via `TimeZone.utc()` and `TimeZone.fixed(UtcOffset)`), `ZonedDateTime` (instant, zone, the offset valid at that instant, derived `DateTime`), `toTimestamp()`, `toOffsetDateTime()`, `Show` as RFC 9557 (`2026-10-25T02:30:00+01:00[Europe/Berlin]`) and `parse` of the same, where the offset must be valid in the zone at that local time (it picks the instant in an overlap; otherwise the parse fails). Calendar arithmetic (days, months) works on the local time and takes a `Disambiguation`; `Duration` arithmetic works on the instant. Details and names of the remaining members belong to the record. 6. **Future years: the first version includes the POSIX TZ rule representation.** The slim data lists transitions only until the rule takes over; without the rule every date after the last stored transition would be wrong. Numbers: 127 of the 447 zones end in a rule, 31 distinct rule strings, 320 zones are fixed. All rules use the `Mm.w.d[/time]` form; the time can be outside 0 to 23 (`/24`, `/-1`, `/2:45`) and some zones have a negative DST. This is cheaper than the alternative of expanding to 2037 (the fat form stores 27 184 transitions, 46 % more, and is still wrong from 2038). No `Jn` or `n` day forms are needed by the data; the record says so and the parser rejects them. Seed impact: none for the record (docs only). The record's slices table must flag it per slice; the expected answer is none, since the data is TorbScript constants and the generator is a `.trb` program.
Author
Collaborator

[loop:torbscript] decided by the loop: three corrections to the issue text so that it fits the contract and the repository, each reasoned.

  • Acceptance criterion 2 is replaced. "Follow-up implementation issues filed from the record" would file steps up front. Instead: TIME.md ends with a numbered slices table (each slice one PR landing as one or two Conventional Commits, with its seed impact and its dependency); the loop files one slice as an issue at a time, just in time, when the one before it starts, after a duplicate search. Suggested slice order for the record to confirm or fix: (1) generator, pinned tzdata and the data constants; (2) TimeZone, named, offsetAt with the POSIX rule evaluation; (3) ZonedDateTime, Disambiguation, conversions; (4) RFC 9557 Show and parse; (5) capability table entries and the guide page.
  • No frontmatter on TIME.md. Every record in docs/design/ opens with # Title and a **Status: ...** line, none carries frontmatter. The new record does the same, gets its entry in docs/design/index.md like the others, and the docs command regenerates the list.
  • This issue is the record only, one PR, Closes #389 (the issue is not an epic any more: the slices table of the record is the checklist, and the slices become their own issues as above).

Definition of ready, as it stands now:

  • Goal: docs/design/TIME.md decides named time zones for std/time, with the six decisions of the comment above as its backbone (each with reasoning and the numbers given there; more measurement only where the record needs it).
  • Acceptance: (a) docs/design/TIME.md exists, status line designed, not built, with data source, size, capability, disambiguation, API, POSIX rules, and a slices table; (b) gap 11 in FIRST-PARTY.md links the record and no longer calls named zones open without a pointer; (c) docs/design/index.md lists it; (d) any TorbScript in the record follows the canon (no semicolons, command calls, fn area(): Int style) and the torbscript skill; (e) torb docs checks and torb docs skill docs skills --check green in forge CI (regenerate skills/ if the docs change reaches it).
  • Design record point: gap 11 of docs/design/FIRST-PARTY.md, depends on #387 (done); the approach follows the Unicode tables (tools/fetch-unicode.sh) and RELEASE.md 7.8.
  • Seed impact: none (docs only).
  • Size: one PR, one Conventional Commit, for example docs(time): design record for named time zones.
[loop:torbscript] decided by the loop: three corrections to the issue text so that it fits the contract and the repository, each reasoned. - **Acceptance criterion 2 is replaced.** "Follow-up implementation issues filed from the record" would file steps up front. Instead: `TIME.md` ends with a numbered slices table (each slice one PR landing as one or two Conventional Commits, with its seed impact and its dependency); the loop files one slice as an issue at a time, just in time, when the one before it starts, after a duplicate search. Suggested slice order for the record to confirm or fix: (1) generator, pinned tzdata and the data constants; (2) `TimeZone`, `named`, `offsetAt` with the POSIX rule evaluation; (3) `ZonedDateTime`, `Disambiguation`, conversions; (4) RFC 9557 `Show` and `parse`; (5) capability table entries and the guide page. - **No frontmatter on `TIME.md`.** Every record in `docs/design/` opens with `# Title` and a `**Status: ...**` line, none carries frontmatter. The new record does the same, gets its entry in `docs/design/index.md` like the others, and the docs command regenerates the list. - **This issue is the record only, one PR, `Closes #389`** (the issue is not an epic any more: the slices table of the record is the checklist, and the slices become their own issues as above). Definition of ready, as it stands now: - Goal: `docs/design/TIME.md` decides named time zones for `std/time`, with the six decisions of the comment above as its backbone (each with reasoning and the numbers given there; more measurement only where the record needs it). - Acceptance: (a) `docs/design/TIME.md` exists, status line `designed, not built`, with data source, size, capability, disambiguation, API, POSIX rules, and a slices table; (b) gap 11 in `FIRST-PARTY.md` links the record and no longer calls named zones open without a pointer; (c) `docs/design/index.md` lists it; (d) any TorbScript in the record follows the canon (no semicolons, command calls, `fn area(): Int` style) and the `torbscript` skill; (e) `torb docs` checks and `torb docs skill docs skills --check` green in forge CI (regenerate `skills/` if the docs change reaches it). - Design record point: gap 11 of `docs/design/FIRST-PARTY.md`, depends on #387 (done); the approach follows the Unicode tables (`tools/fetch-unicode.sh`) and RELEASE.md 7.8. - Seed impact: none (docs only). - Size: one PR, one Conventional Commit, for example `docs(time): design record for named time zones`.
Author
Collaborator

[loop:torbscript] refine: triaged (in scope, no duplicate, no design-record collision), decided the open design questions of the issue by the loop with measured tzdata 2026c numbers (data compiled in as generated TorbScript constants, ~76 KB compact for full history; capability-free values; required Disambiguation; POSIX rules in the first version), corrected the acceptance criteria to the contract and repo conventions (slices table instead of up-front issues, no frontmatter on design records, Closes #389), set label ready. Seed impact: none. Size: one PR, one commit. No owner question. Next: implement (docs-only PR on loop/389-*). Tokens: about 120k (RE worker, estimate).

[loop:torbscript] refine: triaged (in scope, no duplicate, no design-record collision), decided the open design questions of the issue by the loop with measured tzdata 2026c numbers (data compiled in as generated TorbScript constants, ~76 KB compact for full history; capability-free values; required `Disambiguation`; POSIX rules in the first version), corrected the acceptance criteria to the contract and repo conventions (slices table instead of up-front issues, no frontmatter on design records, `Closes #389`), set label `ready`. Seed impact: none. Size: one PR, one commit. No owner question. Next: implement (docs-only PR on `loop/389-*`). Tokens: about 120k (RE worker, estimate).
Author
Collaborator

[loop:torbscript] claim implement by session:loop-torbscript-20261025 until 2026-10-25T04:15:00Z

[loop:torbscript] claim implement by session:loop-torbscript-20261025 until 2026-10-25T04:15:00Z
Author
Collaborator

[loop:torbscript] implement: checkpoint, context limit reached before the commit. Done: worktree torbscript-language-389-time-zones-record (branch loop/389-time-zones-record, from main) holds, uncommitted, the new docs/design/TIME.md (status line designed, not built, sections 1-9: data compiled in via pinned tzdata+zic slim+.trb generator, 76 KB full history, capability-free values, required Disambiguation with the Berlin table, API, POSIX rules, 6-slice table, all seed impact none), the gap 11 link in FIRST-PARTY.md, and the entry in docs/design/index.md (front matter list, generated list, scope paragraph). Verified against the code: torb build emits nothing unreachable and module consts have no initialization, so the data costs only parse/check time, hence its own zone-data.trb. Still to do: check lines over 120 columns in TIME.md (19 lines, tables may be fine), commit docs(design): add the named time zones record with Closes #389, push, open the PR, ci-wait.sh, fix torb docs / skills --check diffs if CI shows any (no skills mention design records, so likely none). Next: implement (continue). Tokens: about 110k (loop worker).

[loop:torbscript] implement: checkpoint, context limit reached before the commit. Done: worktree `torbscript-language-389-time-zones-record` (branch `loop/389-time-zones-record`, from main) holds, uncommitted, the new `docs/design/TIME.md` (status line `designed, not built`, sections 1-9: data compiled in via pinned tzdata+zic slim+`.trb` generator, 76 KB full history, capability-free values, required `Disambiguation` with the Berlin table, API, POSIX rules, 6-slice table, all seed impact none), the gap 11 link in `FIRST-PARTY.md`, and the entry in `docs/design/index.md` (front matter list, generated list, scope paragraph). Verified against the code: `torb build` emits nothing unreachable and module consts have no initialization, so the data costs only parse/check time, hence its own `zone-data.trb`. Still to do: check lines over 120 columns in TIME.md (19 lines, tables may be fine), commit `docs(design): add the named time zones record` with `Closes #389`, push, open the PR, `ci-wait.sh`, fix `torb docs` / `skills --check` diffs if CI shows any (no skills mention design records, so likely none). Next: implement (continue). Tokens: about 110k (loop worker).
Author
Collaborator

[loop:torbscript] implement: PR #476 is open (head 2f8c36ef, bootstrap linux-x64 green, tier A and gates still queued); the finishing worker timed out while waiting on CI, claim released. next: waiting (forge CI on PR 476; green: hand over to review, red: fix round) until 2026-10-25T01:30:00Z

[loop:torbscript] implement: PR https://git.torb.dev/torbscript/language/pulls/476 is open (head 2f8c36ef, bootstrap linux-x64 green, tier A and gates still queued); the finishing worker timed out while waiting on CI, claim released. next: waiting (forge CI on PR 476; green: hand over to review, red: fix round) until 2026-10-25T01:30:00Z
argus closed this issue 2026-10-24 23:56:28 +00:00
Sign in to join this conversation.
No labels
loop
ready
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
torbscript/language#389
No description provided.