time/tzdb.rs
1//! The embedded IANA time zone database.
2//!
3//! The database is compiled into a *pre-parsed* static form by
4//! `tools/gen_tzdb.rs`, from the IANA `tzdata.zi` source run through
5//! `zic -b slim`. Lookups are allocation-free binary searches over static
6//! arrays, so named zones work without `std` and without `alloc`, and the
7//! work per lookup is far smaller than parsing a TZif header each time.
8//!
9//! The generated file (included below when the `timezone-db` feature is
10//! enabled) defines everything documented here:
11//!
12//! * [`TZDB_VERSION`] — the IANA release the data was compiled from,
13//! * [`ZONES`] — one [`ZoneEntry`] per IANA zone name (canonical names and
14//! historic aliases alike, with aliases sharing one [`ZoneData`]), sorted
15//! case-insensitively for [`lookup`],
16//! * `ABBREVIATIONS` — interned abbreviation strings (`"LMT"`, `"EST"`,
17//! `"EET"`, `"-05"`, …) referenced from records by a `u8` index,
18//! * one `ZONE_n` static per unique set of rules, and — only when the crate is
19//! compiled for tests — `ZONES_LEGACY` and the raw `tzdb.bin` blob, kept so
20//! the test builds can assert that the pre-parsed engine and the TZif reader
21//! agree.
22//!
23//! # Encoding (keep in sync with `tools/gen_tzdb.rs`)
24//!
25//! [`ZoneData`] describes a zone as an `initial` record — the local mean
26//! time before the first explicit transition — the ascending transition
27//! `starts`, a small `record_table` of the distinct [`Record`]s used by this
28//! zone plus one `records` index per transition, and a [`Future`] rule that
29//! governs from the last transition onward. The split table keeps long
30//! transition lists compact without repeating the record for every entry.
31//! Together these express exactly what a TZif file encodes plus its POSIX
32//! `TZ` footer. The generator also materializes the footer rule as explicit
33//! transitions for a rolling horizon (the release year plus 50), so lookups
34//! in the near future are a plain binary search; the footer still governs
35//! beyond it. For a zone without a usable footer, the record of the last
36//! transition is stored as the fixed future, which matches the fallback the
37//! runtime TZif reader applies.
38//!
39//! # Future rules (keep in sync with `tools/gen_tzdb.rs`)
40//!
41//! [`Future::DaylightSaving`] is the pre-parsed POSIX footer: standard and
42//! daylight records plus `(packed day, local seconds)` pairs for entering
43//! and leaving daylight saving time. The packed encoding is documented on
44//! `crate::posix::Rule`, which is the same encoding the generator emits.
45
46#[cfg(feature = "timezone-db")]
47use crate::posix::{DstRule, daylight_rule_from_packed_day, evaluate_daylight};
48#[cfg(feature = "timezone-db")]
49use crate::tzif::ZoneInfo;
50
51#[cfg(feature = "timezone-db")]
52include!("tzdb_data.rs");
53
54/// Without the embedded database there is no release to report.
55#[cfg(not(feature = "timezone-db"))]
56pub static TZDB_VERSION: &str = "unknown";
57
58/// The rules of one pre-parsed IANA zone.
59#[allow(dead_code)]
60#[derive(Clone, Copy)]
61pub struct ZoneData {
62 /// The record in effect before the first explicit transition.
63 pub(crate) initial: Record,
64 /// Explicit transition instants (Unix seconds) in ascending order. Each
65 /// record applies from its start until the next one, and
66 /// [`ZoneData::future`] applies from the last transition onward.
67 pub(crate) starts: &'static [i64],
68 /// For each transition, the index into [`ZoneData::record_table`] of the
69 /// record that takes effect there. Stored separately from the instants so
70 /// that the (few) distinct records are not repeated for every transition.
71 pub(crate) records: &'static [u8],
72 /// The distinct records used by this zone's transitions.
73 pub(crate) record_table: &'static [Record],
74 /// The rule that governs from the last transition onward, or from the
75 /// beginning for zones without explicit transitions.
76 pub(crate) future: Future,
77}
78
79/// One entry of the embedded database: a zone name and its compiled rules.
80#[allow(dead_code)]
81pub struct ZoneEntry {
82 /// The zone name; historic aliases keep their own name and share the
83 /// rules of the canonical zone.
84 pub(crate) name: &'static str,
85 /// The compiled rules; identical zones share one static.
86 pub(crate) rules: ZoneRules,
87}
88
89/// Backing store of a zone's rules: either the embedded database or a TZif
90/// image provided at runtime (the system zone or a caller-supplied image).
91#[allow(dead_code)]
92pub enum ZoneRules {
93 /// Rules pre-parsed into the embedded database.
94 Embedded(&'static ZoneData),
95 /// Rules kept as a parsed TZif image. The image is validated once and then
96 /// kept for the process (system detection leaks the parsed value), so time
97 /// zone values stay `Copy`.
98 Runtime(&'static crate::tzif::Tzif<'static>),
99}
100
101/// One stretch of constant UTC offset and daylight saving state.
102#[allow(dead_code)]
103#[derive(Clone, Copy)]
104pub struct Record {
105 /// Offset from UTC in seconds, east of Greenwich.
106 pub(crate) offset: i32,
107 /// Whether daylight saving time is in effect during this stretch.
108 pub(crate) dst: bool,
109 /// Index into the generated abbreviation table.
110 pub(crate) abbrev: u8,
111}
112
113/// The rules that govern from the last explicit transition onward, mirroring
114/// the POSIX `TZ` footer of a TZif file.
115#[allow(dead_code)]
116#[derive(Clone, Copy)]
117pub enum Future {
118 /// The same record applies without end.
119 Fixed { record: Record },
120 /// Daylight saving time, evaluated per POSIX transition rules.
121 DaylightSaving {
122 /// The standard-time record.
123 standard: Record,
124 /// The daylight-time record.
125 daylight: Record,
126 /// `(packed day, local seconds)` at which daylight saving starts.
127 start: (u16, i32),
128 /// `(packed day, local seconds)` at which daylight saving ends.
129 end: (u16, i32),
130 },
131}
132
133/// Resolves an abbreviation index into the shared string table.
134#[cfg(feature = "timezone-db")]
135pub(crate) fn abbreviation(index: u8) -> Option<&'static str> {
136 ABBREVIATIONS.get(usize::from(index)).copied()
137}
138
139#[cfg(feature = "timezone-db")]
140pub(crate) fn record_info(record: Record) -> ZoneInfo<'static> {
141 ZoneInfo {
142 offset: record.offset,
143 is_dst: record.dst,
144 abbrev: abbreviation(record.abbrev).unwrap_or(""),
145 }
146}
147
148impl ZoneData {
149 /// Returns the record that takes effect at the transition `index`.
150 ///
151 /// Kept outside the `timezone-db` gate because the ambiguity classifier
152 /// needs it even in builds that exclude the embedded database (where no
153 /// `ZoneData` value exists, but the code must still compile).
154 pub(crate) fn record_at(self, index: usize) -> Record {
155 self.record_table[usize::from(self.records[index])]
156 }
157}
158
159#[cfg(feature = "timezone-db")]
160impl ZoneData {
161 /// Returns the local time in effect at `seconds`: the UTC offset, the
162 /// daylight saving flag, and the abbreviation in effect.
163 pub fn lookup(self, seconds: i64) -> ZoneInfo<'static> {
164 let starts = self.starts;
165 if starts.is_empty() {
166 return self.future_info(seconds);
167 }
168 let past = starts.partition_point(|&start| start <= seconds);
169 if past == 0 {
170 // Before the first explicit transition: the initial record, per
171 // RFC 8536 §3.2.
172 return record_info(self.initial);
173 }
174 if past == starts.len() {
175 // From the last transition onward the future rule applies.
176 return self.future_info(seconds);
177 }
178 record_info(self.record_at(past - 1))
179 }
180
181 /// The local time implied by the future rule at `seconds`.
182 fn future_info(self, seconds: i64) -> ZoneInfo<'static> {
183 match self.future {
184 Future::Fixed {
185 record,
186 } => record_info(record),
187 Future::DaylightSaving {
188 standard,
189 daylight,
190 start,
191 end,
192 } => evaluate_daylight(
193 DstRule {
194 name: abbreviation(daylight.abbrev).unwrap_or(""),
195 east: daylight.offset,
196 start: (daylight_rule_from_packed_day(start.0), start.1),
197 end: (daylight_rule_from_packed_day(end.0), end.1),
198 },
199 standard.offset,
200 abbreviation(standard.abbrev).unwrap_or(""),
201 seconds,
202 ),
203 }
204 }
205}
206
207/// Looks up the entry for the given zone name.
208///
209/// Matching is case-insensitive, mirroring the IANA database conventions;
210/// the table is sorted lowercased by the generator for this purpose.
211#[cfg(feature = "timezone-db")]
212pub(crate) fn lookup(name: &str) -> Option<&'static ZoneEntry> {
213 let index = ZONES.binary_search_by(|zone| compare_lowercase(zone.name, name)).ok()?;
214 Some(&ZONES[index])
215}
216
217/// Legacy blob-based lookup, kept while the pre-parsed engine is verified in
218/// tests; test builds assert that both agree (see `timezone.rs`).
219#[cfg(all(test, feature = "timezone-db"))]
220pub(crate) fn lookup_legacy(name: &str) -> Option<(&'static str, &'static [u8])> {
221 if let Ok(index) = ZONES_LEGACY.binary_search_by(|entry| entry.0.cmp(name)) {
222 let (canonical, offset, len) = ZONES_LEGACY[index];
223 return Some((canonical, &BLOB[offset as usize..(offset + len) as usize]));
224 }
225 for &(candidate, offset, len) in ZONES_LEGACY {
226 if candidate.eq_ignore_ascii_case(name) {
227 return Some((candidate, &BLOB[offset as usize..(offset + len) as usize]));
228 }
229 }
230 None
231}
232
233/// Returns the names of every embedded time zone, sorted.
234#[cfg(feature = "timezone-db")]
235pub fn all_timezones() -> impl Iterator<Item = &'static str> {
236 ZONES.iter().map(|zone| zone.name)
237}
238
239/// Stub for builds without the embedded database: no named zones exist.
240#[cfg(not(feature = "timezone-db"))]
241pub(crate) fn lookup(_name: &str) -> Option<&'static ZoneEntry> {
242 None
243}
244
245#[cfg(feature = "timezone-db")]
246fn compare_lowercase(left: &str, right: &str) -> core::cmp::Ordering {
247 let left = left.as_bytes();
248 let right = right.as_bytes();
249 let mut index = 0;
250 while index < left.len() && index < right.len() {
251 let a = left[index].to_ascii_lowercase();
252 let b = right[index].to_ascii_lowercase();
253 if a != b {
254 return a.cmp(&b);
255 }
256 index += 1;
257 }
258 left.len().cmp(&right.len())
259}