This is the named-law index: every obscure zic behaviour that can change emitted output is
named here, tied to a reference source, and marked with its enforcement status (a test, a fail-closed
bucket, or a declared future campaign). The standard is deliberately strict —
Every obscure
zicsemantic that can change emitted behaviour must be named, tested, bucketed, and tied to an admitted zone, a fail-closed zone, or a future campaign.
— because that is what makes a timezone compiler boring and reliable rather than "works on the
easy zones." Most of these are not in any tutorial; they are learned by being burned in production.
The per-zone evidence for each fix lives in reference-zic-semantics.md
(the behaviour ledger); this file is the map over those traps. The companion
zic-v-hazards.md tracks the zic -v verbose-warning hazards.
IANA tzdb source → reference zic / zdump → RFC 9636 (TZif validity) → zic-rs + oracle evidence
Wikipedia and articles are orientation only. Every subtle behaviour below is pinned by experiment
against reference zic/zdump (tzcode 2026b) over a declared horizon, never inferred from prose.
Normative sources: zic(8) / zic.c (grammar + semantics), RFC 9636 (TZif binary), RFC 6557 /
BCP 175 (governance). See standards.md and tzdb-governance.md.
A Zone is not a DST table; it is a state machine over eras. Almost every behaviour bug in
this project came from collapsing three distinct things into one. They are tracked separately and
must stay that way:
| Concept | What it is | Bounded by |
|---|---|---|
| era state | which Zone continuation line is in force: its STDOFF, RULES column, FORMAT, UNTIL-context |
era boundaries |
| rule state | the prevailing (save, is_dst_semantics, letter, source_rule) of a named rule set, evolving along that rule set's own timeline |
rule activations |
| local-time type | the emitted TZif ttinfo: (utoff = stdoff + save, is_dst, abbreviation) — a function of era+rule state, never a substitute |
a transition |
Two local-time types can share a utoff yet differ in is_dst/abbreviation (e.g. MSK-std vs
EEST-dst, both +3; EDT vs AST, both −4) — a real type change, never deduplicated.
- compile-clean ≠ behaviour-match. A valid TZif is a compile claim;
zdump-equivalence over a declared horizon is the behaviour claim. They are reported separately even when the counts coincide. As oftzdata.zi2026b: 341/341 compile-clean; 341/341 behaviour-match referencezicover1900..2040; 0 fail-closed — the entire canonical-zone behaviour frontier is closed. - Not full
zic. No leap-second modes, CLI policy,right//posix/, backzone/rearguard, warning parity, or slim emission heuristics. zic-rs is a reference-verified producer for the supported canonical-zone subset over a declared horizon. - Pre-1970 humility. A passing historical horizon proves agreement with reference
zic/tzdb for that named zone over that horizon — not regional civil-time truth (law 17).
Each law: the trap, its Reference, zic-rs status, guard Tests, Known zones, and any Open bucket (a fail-closed reason or future campaign).
A zone is a sequence of eras (continuation lines), each compiled with state carried from the previous
era — not a line-by-line table conversion.
Reference: zic(8) zone/continuation grammar. zic-rs status: ✅ compile_multi_era
(src/compile/transitions.rs). Tests: the whole tests/multi_era.rs suite. Known zones: every
multi-era zone (London, New_York, …). Open bucket: —.
A Zone UNTIL is interpreted using the rules in effect just before the transition — the
ending era's STDOFF and the save prevailing at the UNTIL, plus its w/s/u reference. A wall
UNTIL during active DST lands one save earlier than naive.
Reference: zic(8) ("the UNTIL … is interpreted using the rules in effect just before the
transition"). zic-rs status: ✅ convert_at(ul, stdoff, run.save, uref) at the era-end.
Tests: until_wall_time_uses_prevailing_save_when_dst_active (Test/MidDst). Open bucket: —.
- = wall, s = standard, u/g/z = universal. The line describes the instant a clock set to
that reference would show. Never compare two as raw local seconds without normalizing to a common
frame.
Reference: zic(8) (AT/UNTIL suffixes). zic-rs status: ✅ T5 #5 — the era-end break compares
resolved UT; boundary-coincidence normalizes refs under the prevailing save (wall ≡ standard at
save 0). Tests: central_asia_offset_drop_boundaries_normalize_clock_reference,
europe_mixed_reference_era_end_breaks_match_reference_zic,
era_boundary_normalizes_wall_standard_clock_reference. Known zones: Tashkent, Ashgabat, Anadyr,
Simferopol, Warsaw. Open bucket: —.
When a continuation changes the UT offset and a rule would otherwise fire in the next N seconds,
zic reads the boundary and the activation as simultaneous — one transition, not two. Absorption
uses exact clock-frame equality, never a proximity tolerance.
Reference: zic(8) (the America/Menominee example). zic-rs status: ✅ T5 #2/#3/#5 —
boundary_until + exact normalized clock-frame equality. Tests:
russia_boundary_offset_drop_does_not_create_spurious_standard_hour,
europe_lisbon_final_era_boundary_coincident_dst_matches_reference_zic. Known zones: Moscow,
Volgograd, Novosibirsk, Lisbon, St_Johns. Open bucket: —.
A rule set's (save, is_dst, letter) persists until the rule itself changes it. An intervening era
that uses - or a different rule does not reset it; a later era reusing the rule seeds from its
most-recent activation at the era start (even one years earlier, outside the local window).
Reference: zic(8) rule semantics; pinned by experiment. zic-rs status: ✅ T5 #4 — seed scans
the rule's actual FROM range. Tests: america_phoenix_1944_inherits_war_time_matches_reference_zic,
atlantic_bermuda_1930_inherits_standard_letter_matches_reference_zic,
asia_manila_and_europe_brussels_inherit_prior_dst_match_reference_zic. Known zones: Phoenix (War
from 1942), Bermuda (letter S from 1918), Manila, Brussels, Macau. Open bucket: —.
%s renders the active rule's LETTER/S; - means the variable part is null. A SAVE=0 rule
can change it (a standard-letter change). Timestamps before the earliest rule use the earliest
standard-time rule's letter.
Reference: zic(8) (LETTER/S; pre-first-rule letter). zic-rs status: ✅ T5 #1 — a
boundary-coincident SAVE=0 activation seeds the boundary's letter. Tests:
pacific_auckland_1946_changes_nzmt_to_nzst_and_matches_reference_zic. Known zones: Auckland
(NZMT→NZST). Open bucket: —.
effective_offset = STDOFF + SAVE; the suffix carries standard/daylight semantics; negative SAVE
is valid (Ireland is the canonical example). zic does not distinguish equivalent sums
(10:30 + 0:30 ≡ 10:00 + 1:00).
Reference: zic(8) (SAVE field; negative saves). zic-rs status: ✅ done — inline SAVE is
signed; a negative inline save renders the signed effective offset with is_dst = (save ≠ 0),
exactly as reference zic does (implemented as first-class signed SAVE — negative-save Rules
already worked, e.g. Morocco; the inline guard was lifted). %s/slash inline still fail closed.
Tests: inline_save_negative_renders_signed_effective_offset,
negative_inline_save_is_signed_state_matches_reference_zic. Known zones: Europe/Prague
(1 -1 GMT → GMT, isdst=1, gmtoff 0). Open bucket: — (cleared; was the Prague fail-closed bucket).
Inline SAVE constructs a fixed local-time type from STDOFF + SAVE (its own path) — not a lesser
rule set, and no named-rule timeline is consulted.
Reference: zic(8) ("RULES … can be a field in the same format as a rule SAVE"). zic-rs status:
✅ T3.2b inline-save. Tests: inline_save_percent_z_uses_effective_offset (Test/InlineZ),
Test/InlineLit. Open bucket: inline-save %s / slash (law 9).
%z renders the effective UT offset (STDOFF + SAVE) in shortest lossless form (±hh/±hhmm/
±hhmmss); %s substitutes the active LETTER; slash chooses between two explicit names. Do not
infer one from another.
Reference: zic(8) (FORMAT); RFC 9636. zic-rs status: ✅ %z (incl. no-rules era + inline
save) and %s and slash on ruled eras; inline-save %s/slash fail closed. Tests:
inline_save_percent_z_uses_effective_offset. Open bucket: inline-save %s/slash (future).
Note: the %z unlock took compile-clean 162→338 — but a later sweep proved compile-clean after
%z is not behaviour-verified (law: scope honesty). Both numbers are reported separately.
Sun>=31 may resolve into the next month, and Sat<=30 is a weekday <= N form. zic expresses
these in a recurring footer by re-anchoring onto a clean nth-weekday and folding the skipped days
into the transition time — which can exceed 24h and needs the v3 footer extension (RFC 9636).
E.g. Sat<=30 02:00 → M3.4.4/50 (4th Thursday + 50h). The exact zic rule (verified): for
W <= N (not last-day), wdayoff = N mod 7, d = W − wdayoff, week = N/7, +wdayoff days;
>= N is symmetric (wdayoff = (N−1) mod 7, week = 1+(N−1)/7), reducing to the plain form at
wdayoff = 0. Reference: zic(8) / zic.c stringrule; RFC 9636 (v3). zic-rs status: ✅
done — posix_footer::date_rule + the content-driven v3 (recurring emits b'3' for an
out-of-0..24h time); the explicit horizon also extends to the last finite (one-shot) rule year so
Ramadan-dated rows are emitted explicitly while the footer covers the perpetual tail. Tests:
recurring_sat_leq_30_uses_v3_extended_time_footer, recurring_sun_leq_25_uses_extended_v3_footer,
neighboring_month_on_form_matches_reference_zic. Known zones: Asia/Gaza, Asia/Hebron (now
compile v3 and zdump-match). Open bucket: — (cleared → 341/341).
AT may be 24:00, >24:00 (260:00), negative (-2:30), or fractional (rounded to the nearest
second, ties to even). These interact with calendar carry across day/month/year and with the
TZif-version footer decision (law 12).
Reference: zic(8) (AT range; fractional rounding). zic-rs status: parsed; compile path for
24:00/negative not yet pinned (fails closed). Open bucket: extended-AT + content-driven v3
(future campaign).
Emit the lowest version that correctly represents the compiled behaviour. v3 only when the footer needs the extension (e.g. signed transition hours −167…167) — never because source syntax "looked weird." Reference: RFC 9636 (version selection; v3 footer extensions). zic-rs status: ✅ content-driven v2 today (no v3 content in scope). Tests: covered by the byte-pinned fixtures + the writer. Open bucket: v3 trigger arrives with law 11.
For TZif v2+, the footer computes local time after the last explicit transition and must be
consistent with that last transition. Not decorative.
Reference: RFC 9636 (footer). zic-rs status: ✅ exact-or-fail recurring + fixed footers;
final-era anchoring; projection verified past RECUR_HI. Tests:
footer_drives_post_tail_behavior (Test/Eastern), the ecosystem-tests tz-rs bench. Known
zones: every recurring zone (London EU footer, US footer). Open bucket: —.
Reference zic (slim) may emit fewer explicit transitions and lean on the footer; zic-rs emits a fat
set. If zdump behaviour + footer match, they are equivalent. Byte parity is claimed only where a
reference blob is pinned.
Reference: zic(8) (-b slim/fat, -R). zic-rs status: ✅ accepted, documented. Tests:
Test/Mixed (zic 39 vs zic-rs 122 explicit transitions, zdump-equivalent). Open bucket: optional
--emit-style slim/byte-parity (future).
Link targets may be zones or other links; links may precede their targets; chains name the same
zone. Production systems use aliases, not only canonical names. Accounted separately.
Reference: zic(8) (Link); jiff#258 alias-duplication. zic-rs status: ✅ resolve/cycle/missing
diagnostics; --alias-map; support-report counts canonical/links/total/chains/cycles separately.
Tests: link cycle/missing tests; tests/support_report.rs. Open bucket: —.
Main zone source is Rule/Zone/Link; leap-second input is Leap + an expiration line. The
R/Z/L zishrink prefixes are zone-source dispatch; Leap/Expires must not be accepted in
normal zone-source mode.
Reference: zic(8) (input forms). zic-rs status: ✅ zone-source dispatch only; leap modes out of
scope (fail closed / unknown). Open bucket: leap-file mode (future, out of scope).
tzdb partitions the world so clocks agree after 1970; pre-1970 data is included where useful but is
not universal historical truth. Phrase every claim as "matches reference zic/tzdb for this zone
over this horizon."
Reference: tz theory.html. zic-rs status: ✅ wording enforced in docs (compatibility/ladder).
Known zones: London 1830..2045 (stated as reference-agreement, not regional truth). Open
bucket: —.
zic-rs compiles tzdb per reference zic behaviour; it does not set timezone policy or "correct"
history. tzdb is public-domain, maintained by the TZ Coordinator via the RFC 6557 / BCP 175 process.
Reference: RFC 6557; tz theory.html. zic-rs status: ✅ doctrine in
tzdb-governance.md; credit + no-endorsement in ACKNOWLEDGEMENTS.md. Open
bucket: —.
support-report --explain-buckets and this file share one map, so they cannot drift:
| Bucket (support-report label) | Zones (2026b) | Deep law |
|---|---|---|
recurring footer: non-POSIX day form |
✅ none — cleared (law 10 + v3 footer; Asia/Gaza/Asia/Hebron now compile) |
law 10 — neighbouring-month ON / footer expressibility |
inline-save: negative SAVE |
✅ none — cleared (law 7; Europe/Prague now compiles) |
law 7 — signed SAVE (negative valid) |
no-rules era: %s or STD/DST slash FORMAT |
(none in 2026b) | law 9 — FORMAT paths |
No canonical zone fails closed in 2026b → 341/341 compile-clean and behaviour-match. The map
retains the law-7/law-10 rows (the deep_semantic mapping is kept as documentation and would fire
again if a future tzdb release reintroduced such a construct) even though both buckets are now empty.
Any fail-closed remains a deliberate refusal (no approximate output), surfaced as a ZIC001
diagnostic and bucketed honestly.