Diese Seite gibt es noch nicht auf Deutsch, deshalb steht sie hier auf Englisch. Zur englischen Seite
Named Time Zones#
Status: designed, not built (2026-10-25). — std/time has the wall clock without a zone database: Date,
TimeOfDay, DateTime, UtcOffset and OffsetDateTime (FIRST-PARTY.md gap 11, slices 1 to 3,
#111, #387, #388). What is missing is the last slice of that gap: a place on the map, Europe/Berlin, that knows its
own offset on every day, daylight saving included. This record decides how the data gets into a program, what
TimeZone is allowed to reach, what a local time in a gap or an overlap means, the shape of the API and how far the
first version reaches into the future, so that the slices of section 9 build one specification. It is a design inside
std/time: nothing named here exists yet, so no published contract changes.
A TimeZone is a value with its rules inside it, and a local time that is not one instant is never guessed.
TimeZone.named "Europe/Berlin" reads no file and asks no machine: the data of one pinned tzdata release is part of the
toolchain, so the same program answers the same on Linux, Windows and in the playground. And the two days of the year
on which a wall clock is ambiguous - 2026-03-29 02:30 does not exist in Berlin, 2026-10-25 02:30 happens twice -
are decided by an argument the caller has to write, Disambiguation, and by no default.
- 1. What is decided
- 2. Where the data comes from
- 3. Size
- 4. Capability
- 5. Gaps and overlaps
- 6. The API
- 7. Years after the last transition: POSIX rules
- 8. What stays out
- 9. Slices
1. What is decided#
- The data is compiled into
std/time, generated as TorbScript from one pinned tzdata release by a generator intools/; the system's zoneinfo is not read, bystdat least (section 2). - The full history is kept, in a compact transition table of an estimated 76 KB for 447 zones (to be re-measured in
slice 1), and only the module that
TimeZoneimports carries it (section 3). TimeZoneandZonedDateTimeare capability-free values likeTimestampandDuration; only reading the clock isclock(section 4).DisambiguationisEarlier,LaterorReject, and it is a required argument wherever a local time becomes an instant (section 5).TimeZone.named,ZonedDateTime,toTimestamp,toOffsetDateTimeand RFC 9557 text are the surface (section 6).- The first version includes the POSIX TZ rule that continues each zone after its last stored transition: 127 of 447 zones end in one and without it every date after the last stored transition would be wrong (section 7).
- Seed impact: none. The data is TorbScript constants and the generator is a
.trbprogram; no syntax, native or driver flag changes (CLAUDE.md, "Seed and breaking changes").
2. Where the data comes from#
Three sources were possible: compiled into the toolchain, the zoneinfo of the machine, or both with a defined order.
| Source | For | Against |
|---|---|---|
| Compiled in | same answer everywhere; pure; works in the playground | a tzdata release is a std one |
| System zoneinfo | always the machine's newest rules | absent on Windows and in the playground; needs files |
| Both, system first | a fresh rule without a std release | depends on the machine again; named would need files |
Decided: compiled in, and only that. The reasons are the two against the system's database: a language whose
Date is the same on every target (RANDOM.md pins the same promise for its generator) cannot have a zone
that is not, and a capability that a value constructor needs would put files on every package that parses a
timestamp with a zone. tzdata is in the public domain.
Open points of this record. Two statements below are not yet settled facts. (1) The decision to build zic from the
pinned tzcode is proposed in this record; the refinement of #389 did not decide it. It is open to a better way of
getting TZif files (a pinned prebuilt zic, or reading the sources directly), to be confirmed in slice 1. (2) The size
figures of section 3 are the refinement's; whether they include abbreviations is unconfirmed (section 3).
- How it is generated. Like the Unicode tables (
tools/fetch-unicode.shpins every input by SHA-256 so the generated output is reproducible from them),tools/fetch-tzdata.shdownloads one tzdata release and itstzcodeand checks both againsttools/tzdata/SHA256SUMS. Proposed, not a refinement outcome:zic -b slim, built from the pinnedtzcodewith the C compiler every checkout already needs, compiles the sources to TZif files (RFC 9636), andtools/tzdata/generate.trbreads those- the transitions, the types and the POSIX footer - and writes the data module. The generated file is committed, a gate regenerates it and compares, as for the other generated tables.
- The update path. A new tzdata release is new lines in
SHA256SUMS, a regenerated data file and astdpatch release.TimeZone.databaseVersion()answers the pinned release (2026c), so a program can print which rules it was built with. A runtime override from the system's zoneinfo is not part ofstd: it can come later as a package with thefilescapability that builds aTimeZonefrom a TZif file, without changingTimeZone. - Names.
TimeZone.namedknows the 598 names of the pinned release (447 distinct zones, 151 of the names are links and one name table points them at the zone index, they are not copies). A name that a later release removes stays, as tzdata keeps itsbackwardlinks. Names are case-sensitive, as IANA writes them.
3. Size#
The TZif figures were measured on tzdata 2026c compiled with zic -b slim; the compact-table figures are estimates
from the refinement comment of #389, not measurements of a built table (the numbers come from the refinement of #389).
| Form | Size |
|---|---|
| TZif, slim, full history, 447 zones | 235 395 bytes (52 623 deflated), 18 657 transitions |
| Compact table, full history | about 76 KB (estimate) |
| Compact table, from 1970 on | about 51 KB (estimate) |
| TZif, fat (expanded to 2037), for comparison | 27 184 transitions, 46 % more |
The compact table stores each transition as the delta to the one before in minutes as a varint plus a one-byte index
into the zone's type table (offset, daylight-saving flag). 525 of the 18 657 transitions are not on a whole minute (the
local mean times before standard time), so a flag marks the exceptions and seconds stay in the format; UtcOffset
already holds whole seconds. The estimates count transitions and a type table of offset and daylight-saving flag.
Unconfirmed: whether the 76 KB include abbreviations (CET, CEST); section 8 leaves them out of the first
version. Slice 1 re-measures the real table, with and without abbreviations, and this section is corrected to the
result.
Decided: the full history. From 1970 on is 25 KB smaller, and costs a cliff: a birth date in 1960 in a named zone
would answer with the offset of 1970. Timestamp itself reaches from 1677 to 2262, so a smaller history has no natural
edge to stop at.
Whether a program that imports only Timestamp pays for the data. It does not pay in the program: torb build
seeds the entry function alone and emits nothing unreachable (compiler/src/ir/lower/lower.trb), and a module's
top-level const is a static value the const evaluator makes where it is read, with no module initialization ever
(docs/language/modules-and-packages/top-level-code.md, rule 5). Data that is only read through TimeZone.named is
therefore in the binary and the VM's bytecode only of a program that reaches it. What every program that imports the
module does pay is the parse and the check of the generated source. So the data goes to its own file, zone-data.trb,
that only TimeZone imports, as functions over text (the pattern of std/html/src/entities.trb: one function with a
match over the region, each arm one text of code points), because a function is only lowered where something calls it.
The first slice measures torb check of a program that imports only Timestamp, before and after, in forge CI; if the
check cost is noticeable, the data moves into a package of its own that std/time re-exports lazily, which changes no
name of the API.
4. Capability#
TimeZone and ZonedDateTime are values and reach no capability, as Duration, Instant and Timestamp do
(RELEASE.md section 7.8, "every imported name decides"). TimeZone.named is pure: the data is in the
program. Reading the clock stays Clock.timestamp() and clock, so Clock.timestamp().inZone zone is where the
capability is visible, at the one call that needs it.
The two names are added to the values of std/time that capabilityOfImport answers None for
(compiler/src/package/archive.trb) and to the table of docs/tooling/package-capabilities.md; the registry test
that compares the two keeps them agreeing. Disambiguation and TimeZoneKind (below) are values too.
5. Gaps and overlaps#
A local time is not always one instant. Where the clocks go forward (a gap) a local time does not exist, where they go
back (an overlap) it happens twice. Disambiguation is an enum of Earlier, Later and Reject, and every
function that turns a local time into an instant takes one, with no default value: the caller writes what a booking
system, a scheduler or a log reader means.
Earlier: in an overlap the first of the two instants (the offset before the clocks went back); in a gap the local time read with the offset that holds after the gap, which is the earlier instant.Later: in an overlap the second instant; in a gap the local time read with the offset that holds before the gap, which is the later instant.Reject: a failure in both cases, aTimeParseErrorthat names the zone and the local time and says whether it is a gap or an overlap.- Outside a gap or an overlap all three answer the same, and
Rejectnever fails.
Europe/Berlin, to be copied by the conformance programs (every result is written with the suffix [Europe/Berlin],
left out of the cells):
| Local time | Case | Earlier | Later | Reject |
|---|---|---|---|---|
| 2026-10-25 02:30 | overlap, +02:00 then +01:00 | 2026-10-25T02:30:00+02:00 | 2026-10-25T02:30:00+01:00 | fails |
| 2026-03-29 02:30 | gap, +01:00 then +02:00 | 2026-03-29T01:30:00+01:00 | 2026-03-29T03:30:00+02:00 | fails |
| 2026-07-01 12:00 | neither | 2026-07-01T12:00:00+02:00 | the same | the same |
In the gap both answers move the local time by the length of the gap, one hour, and none of them keeps 02:30.
const zone = TimeZone.named("Europe/Berlin").expect("a zone")
const local = DateTime.parse("2026-10-25T02:30:00").expect("a date and time")
print local.inZone(zone, .Earlier)
print local.inZone(zone, .Later)
// prints Ok(2026-10-25T02:30:00+02:00[Europe/Berlin])
// prints Ok(2026-10-25T02:30:00+01:00[Europe/Berlin])The infallible direction needs no argument: an instant has exactly one local time in a zone, Timestamp.inZone zone.
6. The API#
The names follow what std/time has: methods on the types, Result<_, TimeParseError> where a value can be refused,
and nothing in the prelude (use TimeZone, ZonedDateTime, Disambiguation from "std/time").
TimeZone(Compare,Equals,Hash,Show):TimeZone.named(name: String): Result<TimeZone, TimeParseError>for the IANA names;TimeZone.utc()andTimeZone.fixed(offset: UtcOffset)for the two that are no database entry;offsetAt(instant: Timestamp): UtcOffset;name(): String; andTimeZone.databaseVersion(): String. Equality is by the name it was made with, so a zone and a link that points at it are two values that give the same offsets, andshowround-trips the name that was written.Disambiguation(Equals,Hash,Show):Earlier,Later,Reject(section 5).ZonedDateTime(Compare,Equals,Hash,Show): anOffsetDateTimeand theTimeZoneit was made in, and the invariant that the offset is the zone's offset at that instant.Compareorders by the instant likeOffsetDateTime;Equalsalso compares the zone, so two values that are the same instant in two zones are ordered equal and are not equal, which is whyZonedDateTimeis not the key of an ordered set by instant alone. Members:zone(),offset(),dateTime(),date(),timeOfDay(),toTimestamp(): Result<Timestamp, TimeParseError>andtoOffsetDateTime(): OffsetDateTime. It holds anOffsetDateTimeand not aTimestamp, so its range is the one ofOffsetDateTime(the years 0000 to 9999) and onlytoTimestampcan fail on the range of 1677 to 2262.- Making one:
DateTime.inZone(zone, disambiguation): Result<ZonedDateTime, TimeParseError>(fails forRejectin a gap or overlap) andTimestamp.inZone(zone): ZonedDateTime(cannot fail inside the range ofTimestamp). - Arithmetic has two meanings and two names.
plus(duration: Duration)counts exact seconds on the instant, so it steps over a gap or an overlap and the local time jumps;plusDays(days: Int64, disambiguation: Disambiguation)keeps the wall-clock time and moves the calendar, which is the one that can land in a gap and so takes the argument. Months and years wait for aDate.plusMonthsthat does not exist yet; the zone adds nothing to it but the argument. - Text is RFC 9557:
Showwrites2026-10-25T02:30:00+01:00[Europe/Berlin](anOffsetDateTime, then the name in brackets), andZonedDateTime.parsereads the same. The offset in the text has to be valid in the zone at that local time: in an overlap it is what chooses the instant, anywhere else a text whose offset the zone does not have at that time fails with aTimeParseErrorand the two offsets in the message. Critical flags ([!Europe/Berlin]) and other suffix tags are rejected until something needs them.
7. Years after the last transition: POSIX rules#
Slim TZif files list transitions only until a rule takes over, which is stored as the POSIX TZ string of the file's
footer (CET-1CEST,M3.5.0,M10.5.0/3 for Berlin). Without evaluating it every date after the last stored transition
(1996 for Berlin) is wrong, and expanding to 2037 instead is the fat form, which stores 46 % more transitions and is
still wrong from 2038. The first version includes the rule.
- The numbers. Of the 447 zones 127 end in a rule, with 31 distinct rule strings; the other 320 are fixed. Every
rule of the data is in the
Mm.w.d[/time]form (month, week 1 to 5 where 5 is the last, day of week), so the parser knows only that form; theJnandnday-of-year forms are rejected with a clear error and are never reached by the data. - The time of a rule is not a clock time of a day. It may be outside 0 to 23 (
/24,/-1,/2:45), up to 167 hours in either direction as RFC 9636 allows; the transition is at that many hours after midnight of the day found. - Negative daylight saving exists in the data (Europe/Dublin writes standard time as the summer offset): the rule's second name does not mean "later in the year the offset is larger". The evaluator works with the two offsets and the two transition instants and never with the words standard and daylight.
- The southern hemisphere has its start after its end in the calendar year; the evaluator orders the two transitions of a year by instant.
- One evaluation, two uses.
offsetAt(instant)and the local-to-instant search of section 5 both ask the rule for the two transitions of a year, so the gap and the overlap of a future date are found by the same code that finds those of a stored transition. - Tests. The conformance programs compare the rule's answer with the stored transitions for every year where a zone has both (the last stored transitions of every ruled zone are generated by the same rule), plus the worked examples of section 5.
8. What stays out#
- Leap seconds.
Timestampis Unix time, and the data is the non-"right" TZif that ignores them. - Abbreviations (
CET,CEST) are not stored and not in the API of the first version: they are not unique and not parseable, RFC 9557 does not use them, and a later slice that needs them (astrftime-like format) adds them to the type table with its own measurement. - Reading the system's zoneinfo, per-user zone settings and a
TimeZone.local()that asks the machine for its zone: each needs a capability (filesorenvironment) and would be a package or a name of its own, never a change ofTimeZone.named. - A calendar other than the Gregorian and
Date.plusMonths: not zone questions; the latter is the next thingstd/timeneeds for calendar arithmetic in a zone.
9. Slices#
One slice is one pull request that lands as one or two Conventional Commits. Each is filed as an issue when the one before it starts, not up front.
| # | Slice | Seed | Needs |
|---|---|---|---|
| 1 | fetch-tzdata.sh, pinned SHA256SUMS, build of zic, TZif reader generate.trb, size re-measured | none | - |
| 2 | Generated zone-data.trb (section 3), regeneration gate, check cost of a Timestamp-only program | none | 1 |
| 3 | TimeZone: named, utc, fixed, offsetAt, databaseVersion, POSIX rules (section 7) | none | 2 |
| 4 | Disambiguation, ZonedDateTime, both inZone, conversions, plus, plusDays | none | 3 |
| 5 | RFC 9557 Show and ZonedDateTime.parse | none | 4 |
| 6 | The capability table entries, docs/standard-library/time.md, and gap 11 of FIRST-PARTY.md closed | none | 5 |
No slice changes the syntax, adds a native or a driver flag, or renames a name the compiler looks up by string, so none needs the two-commit seed refresh.