Skip to main content

timezone_data/
zone.rs

1//! The [`Zone`] type and its supporting records.
2//!
3//! Every embedded zone is pre-parsed into static Rust objects by the `xtask`
4//! generator (see `src/generated.rs`), so there is no parsing at runtime: the
5//! accessors return slices that point directly at `&'static` data.
6
7use crate::posix::{year_of, PosixTz};
8
9/// Describes a local time type (e.g. `EST`, `EDT`).
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub struct ZoneType {
12    /// Abbreviated name.
13    pub abbrev: &'static str,
14    /// Seconds east of UTC.
15    pub offset: i32,
16    /// True if this is a daylight-saving time type.
17    pub is_dst: bool,
18}
19
20/// A moment when the timezone rule changes.
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub struct Transition {
23    /// Unix timestamp at which the transition takes effect.
24    pub when: i64,
25    /// Index into the zone's [`types`](Zone::types).
26    pub type_idx: usize,
27    /// True if the transition time is standard (not wall clock).
28    pub is_std: bool,
29    /// True if the transition time is UT (not local).
30    pub is_ut: bool,
31}
32
33/// A leap-second record.
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub struct LeapSecond {
36    /// Unix timestamp of the leap second.
37    pub when: i64,
38    /// Cumulative correction in seconds.
39    pub correction: i32,
40}
41
42/// A transition produced by [`Zone::transitions_for_range`].
43///
44/// Unlike [`Transition`], this carries the resolved [`ZoneType`] directly, since
45/// transitions generated from the POSIX extend rule may name a type that does
46/// not appear in the stored type table.
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub struct RangeTransition {
49    /// Unix timestamp at which the transition takes effect.
50    pub when: i64,
51    /// The zone type in effect after the transition.
52    pub zone_type: ZoneType,
53}
54
55/// A parsed IANA timezone, exposing transitions, zone types, leap seconds, and
56/// the POSIX TZ extend rule.
57///
58/// Instances come from [`load`](crate::load) and reference `&'static` data
59/// generated at build time; `Zone` is `Copy`.
60#[derive(Debug, Clone, Copy)]
61pub struct Zone {
62    pub(crate) name: &'static str,
63    pub(crate) version: u8,
64    pub(crate) types: &'static [ZoneType],
65    pub(crate) transitions: &'static [Transition],
66    pub(crate) leap_seconds: &'static [LeapSecond],
67    pub(crate) extend: Option<PosixTz<'static>>,
68    pub(crate) extend_raw: &'static str,
69}
70
71impl Zone {
72    /// The IANA timezone name.
73    pub fn name(&self) -> &'static str {
74        self.name
75    }
76
77    /// The TZif format version the data was compiled from (1, 2, 3, or 4).
78    pub fn version(&self) -> u8 {
79        self.version
80    }
81
82    /// The zone's local time types.
83    pub fn types(&self) -> &'static [ZoneType] {
84        self.types
85    }
86
87    /// The stored transition records.
88    pub fn transitions(&self) -> &'static [Transition] {
89        self.transitions
90    }
91
92    /// The leap-second records.
93    pub fn leap_seconds(&self) -> &'static [LeapSecond] {
94        self.leap_seconds
95    }
96
97    /// The parsed POSIX TZ rule for computing future transitions, if any.
98    pub fn extend(&self) -> Option<&PosixTz<'static>> {
99        self.extend.as_ref()
100    }
101
102    /// The raw POSIX TZ footer string (empty if none).
103    pub fn extend_raw(&self) -> &'static str {
104        self.extend_raw
105    }
106
107    /// The number of local time types.
108    pub fn type_count(&self) -> usize {
109        self.types.len()
110    }
111
112    /// Returns the `i`-th local time type. Panics if `i >= types().len()`.
113    pub fn type_at(&self, i: usize) -> ZoneType {
114        self.types[i]
115    }
116
117    /// Returns the zone type in effect at the given Unix timestamp.
118    ///
119    /// Searches stored transitions and falls back to the POSIX TZ rule for
120    /// times after the last transition.
121    pub fn lookup(&self, unix: i64) -> ZoneType {
122        let tr = self.transitions;
123        if tr.is_empty() {
124            return self.types.first().copied().unwrap_or(ZoneType {
125                abbrev: "UTC",
126                offset: 0,
127                is_dst: false,
128            });
129        }
130
131        // Number of transitions whose time is <= unix.
132        let lo = tr.partition_point(|t| t.when <= unix);
133
134        if lo == 0 {
135            // Before the first transition: first non-DST type, else type 0.
136            for zt in self.types {
137                if !zt.is_dst {
138                    return *zt;
139                }
140            }
141            return self.types[0];
142        }
143
144        if lo == tr.len() {
145            if let Some(ext) = &self.extend {
146                let (abbrev, offset, is_dst) = ext.lookup(unix);
147                return ZoneType {
148                    abbrev,
149                    offset,
150                    is_dst,
151                };
152            }
153        }
154
155        self.types[tr[lo - 1].type_idx]
156    }
157
158    /// Returns transitions in the half-open interval `[start_unix, end_unix)`,
159    /// combining stored transitions with ones generated from the POSIX TZ
160    /// extend rule. The result is yielded in chronological order.
161    pub fn transitions_for_range(&self, start_unix: i64, end_unix: i64) -> RangeIter {
162        let last_stored = self.transitions.last().map(|t| t.when).unwrap_or(i64::MIN);
163        let generate = self.extend.map(|e| e.has_dst()).unwrap_or(false);
164        RangeIter {
165            zone: *self,
166            start_unix,
167            end_unix,
168            stored_idx: 0,
169            stored_done: false,
170            last_stored,
171            generate,
172            year: year_of(start_unix),
173            end_year: year_of(end_unix),
174            pending: [None, None],
175            pending_i: 0,
176        }
177    }
178}
179
180/// Iterator returned by [`Zone::transitions_for_range`].
181pub struct RangeIter {
182    zone: Zone,
183    start_unix: i64,
184    end_unix: i64,
185    stored_idx: usize,
186    stored_done: bool,
187    last_stored: i64,
188    generate: bool,
189    year: i32,
190    end_year: i32,
191    pending: [Option<RangeTransition>; 2],
192    pending_i: usize,
193}
194
195impl Iterator for RangeIter {
196    type Item = RangeTransition;
197
198    fn next(&mut self) -> Option<RangeTransition> {
199        // Phase 1: stored transitions in range.
200        if !self.stored_done {
201            let tr = self.zone.transitions;
202            while self.stored_idx < tr.len() {
203                let t = tr[self.stored_idx];
204                if t.when >= self.end_unix {
205                    self.stored_done = true;
206                    break;
207                }
208                self.stored_idx += 1;
209                if t.when >= self.start_unix {
210                    return Some(RangeTransition {
211                        when: t.when,
212                        zone_type: self.zone.types[t.type_idx],
213                    });
214                }
215            }
216            self.stored_done = true;
217        }
218
219        // Phase 2: transitions generated from the POSIX extend rule.
220        if !self.generate {
221            return None;
222        }
223        let ext = self.zone.extend.expect("generate implies extend");
224        loop {
225            // Drain any pending transitions buffered for the current year.
226            while self.pending_i < 2 {
227                let item = self.pending[self.pending_i].take();
228                self.pending_i += 1;
229                if let Some(t) = item {
230                    return Some(t);
231                }
232            }
233
234            if self.year > self.end_year {
235                return None;
236            }
237
238            // Compute this year's transitions, filter to range, sort, buffer.
239            let year = self.year;
240            self.year += 1;
241            self.pending = [None, None];
242            self.pending_i = 0;
243
244            if let Some((dst_start, dst_end)) = ext.transitions_for_year(year) {
245                let dst_type = ZoneType {
246                    abbrev: ext.dst_abbrev,
247                    offset: ext.dst_offset,
248                    is_dst: true,
249                };
250                let std_type = ZoneType {
251                    abbrev: ext.std_abbrev,
252                    offset: ext.std_offset,
253                    is_dst: false,
254                };
255                let mut buf: [Option<RangeTransition>; 2] = [None, None];
256                let mut n = 0;
257                if self.in_range(dst_start) {
258                    buf[n] = Some(RangeTransition {
259                        when: dst_start,
260                        zone_type: dst_type,
261                    });
262                    n += 1;
263                }
264                if self.in_range(dst_end) {
265                    buf[n] = Some(RangeTransition {
266                        when: dst_end,
267                        zone_type: std_type,
268                    });
269                    n += 1;
270                }
271                // Sort the (at most two) candidates chronologically.
272                if n == 2 && buf[0].map(|t| t.when) > buf[1].map(|t| t.when) {
273                    buf.swap(0, 1);
274                }
275                self.pending = buf;
276            }
277        }
278    }
279}
280
281impl RangeIter {
282    fn in_range(&self, when: i64) -> bool {
283        when >= self.start_unix && when < self.end_unix && when > self.last_stored
284    }
285}