Skip to main content

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}