Skip to main content

rupg_types/tz/
mod.rs

1//! The time zones of the vendored IANA data (spec/07 section 7.9).
2//!
3//! The names and the rules come from `vendor/tzdata`, so `TimeZone` takes the same zones as the PostgreSQL pin and gives the same offsets. The build of PostgreSQL compiles `tzdata.zi` with its copy of `zic` and installs a file for each zone. Here `zic` is a port that runs when a zone is first loaded, and its result is equal to those files, byte for byte. The reader of that result is a port of `localtime.c`, and [`load`] is a port of `pg_tzset`, which also takes a POSIX zone string such as `EST5EDT` or `<+05>-05`.
4
5mod posix;
6mod zic;
7
8use std::collections::BTreeMap;
9use std::sync::{Arc, Mutex, OnceLock, PoisonError};
10
11use crate::datetime::{AbbrevMeaning, TimeZone, ZoneLookup};
12use crate::generated::tznames::ZONE_NAMES;
13
14/// The limits of `tzfile.h`.
15const TZ_MAX_TIMES: usize = 2000;
16const TZ_MAX_TYPES: usize = 256;
17const TZ_MAX_CHARS: usize = 50;
18/// `TZ_STRLEN_MAX`: the longest name that `pg_tzset` takes.
19const TZ_STRLEN_MAX: usize = 255;
20const YEARS_PER_REPEAT: i64 = 400;
21const AVG_SECS_PER_YEAR: i64 = 31_556_952;
22const SECS_PER_REPEAT: i64 = YEARS_PER_REPEAT * AVG_SECS_PER_YEAR;
23/// 2000-01-01 00:00 UTC in seconds since 1970, the instant of `pg_tz_acceptable`.
24const TIME_2000: i64 = 946_684_800;
25
26/// The name of a zone or a link as the data spells it, found without regard to case, as `pg_tzset` finds it.
27pub fn zone_name(name: &str) -> Option<&'static str> {
28    let key = name.to_ascii_lowercase();
29    ZONE_NAMES
30        .binary_search_by(|n| n.to_ascii_lowercase().as_str().cmp(&key))
31        .ok()
32        .map(|i| ZONE_NAMES[i])
33}
34
35/// Every name of a zone or a link, sorted by the name in lower case.
36pub fn zone_names() -> &'static [&'static str] {
37    &ZONE_NAMES
38}
39
40/// `ttinfo`: a local time type, as the offset east of UTC, the daylight saving flag, and the index of the abbreviation.
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42struct Tti {
43    utoff: i32,
44    isdst: bool,
45    desigidx: usize,
46}
47
48/// `struct state` of `pgtz.h`, without the leap seconds, which the data of PostgreSQL does not have.
49#[derive(Debug, Clone, Default)]
50struct State {
51    ats: Vec<i64>,
52    types: Vec<u8>,
53    ttis: Vec<Tti>,
54    /// The abbreviations, each with a NUL after it.
55    chars: Vec<u8>,
56    goback: bool,
57    goahead: bool,
58    defaulttype: usize,
59}
60
61/// The string at an index of a buffer of strings, up to the next NUL.
62fn string_at(chars: &[u8], index: usize) -> &[u8] {
63    let rest = chars.get(index..).unwrap_or(&[]);
64    rest.split(|&b| b == 0).next().unwrap_or(&[])
65}
66
67impl State {
68    /// `tzloadbody`: the state from the data of a zone file, with the transitions of the TZ string at the end of the file for the 400 years after the last transition.
69    fn from_compiled(compiled: zic::Compiled) -> Option<State> {
70        let mut state = State { chars: compiled.chars, ..State::default() };
71        for (&at, &kind) in compiled.ats.iter().zip(&compiled.types) {
72            if let Some(&last) = state.ats.last()
73                && at <= last
74            {
75                if at < last {
76                    return None;
77                }
78                state.ats.pop();
79                state.types.pop();
80            }
81            state.ats.push(at);
82            state.types.push(kind);
83        }
84        for &(utoff, isdst, desigidx) in &compiled.ttis {
85            state.ttis.push(Tti { utoff, isdst, desigidx: usize::from(desigidx) });
86        }
87        if !compiled.footer.is_empty() && state.ttis.len() + 2 <= TZ_MAX_TYPES {
88            state.extend(compiled.footer.as_bytes());
89        }
90        if state.ttis.is_empty() {
91            return None;
92        }
93        state.repeats();
94        state.defaulttype = state.default_type();
95        Some(state)
96    }
97
98    /// The part of `tzloadbody` that adds the rules of the TZ string, if all of its abbreviations fit.
99    fn extend(&mut self, footer: &[u8]) {
100        let Some(mut ts) = posix::tzparse(footer, false) else { return };
101        let abbrs: Vec<Vec<u8>> =
102            ts.ttis.iter().map(|t| string_at(&ts.chars, t.desigidx).to_vec()).collect();
103        let mut chars = self.chars.clone();
104        let mut got = 0;
105        for (tti, abbr) in ts.ttis.iter_mut().zip(&abbrs) {
106            let found = (0..chars.len()).find(|&j| string_at(&chars, j) == abbr.as_slice());
107            if let Some(j) = found {
108                tti.desigidx = j;
109                got += 1;
110            } else if chars.len() + abbr.len() < TZ_MAX_CHARS {
111                tti.desigidx = chars.len();
112                chars.extend_from_slice(abbr);
113                chars.push(0);
114                got += 1;
115            }
116        }
117        if got != ts.ttis.len() {
118            return;
119        }
120        self.chars = chars;
121        while self.types.len() > 1
122            && self.types[self.types.len() - 1] == self.types[self.types.len() - 2]
123        {
124            self.ats.pop();
125            self.types.pop();
126        }
127        let first = ts.ats.iter().position(|&at| self.ats.last().is_none_or(|&last| last < at));
128        // There are at most 256 types, so the index of a type fits in a byte.
129        #[allow(clippy::cast_possible_truncation)]
130        let typecnt = self.ttis.len() as u8;
131        for (&at, &kind) in ts.ats.iter().zip(&ts.types).skip(first.unwrap_or(ts.ats.len())) {
132            if self.ats.len() >= TZ_MAX_TIMES {
133                break;
134            }
135            self.ats.push(at);
136            self.types.push(typecnt + kind);
137        }
138        self.ttis.append(&mut ts.ttis);
139    }
140
141    /// `typesequiv`.
142    fn types_equiv(&self, a: u8, b: u8) -> bool {
143        let (a, b) = (self.ttis[usize::from(a)], self.ttis[usize::from(b)]);
144        a.utoff == b.utoff
145            && a.isdst == b.isdst
146            && string_at(&self.chars, a.desigidx) == string_at(&self.chars, b.desigidx)
147    }
148
149    /// The part of `tzloadbody` that sets `goback` and `goahead`: the rules repeat every 400 years before the first transition or after the last one.
150    fn repeats(&mut self) {
151        let n = self.ats.len();
152        if n <= 1 {
153            return;
154        }
155        let repeat = |t1: i64, t0: i64| t1.wrapping_sub(t0) == SECS_PER_REPEAT;
156        self.goback = (1..n).any(|i| {
157            self.types_equiv(self.types[i], self.types[0]) && repeat(self.ats[i], self.ats[0])
158        });
159        self.goahead = (0..n - 1).rev().any(|i| {
160            self.types_equiv(self.types[n - 1], self.types[i])
161                && repeat(self.ats[n - 1], self.ats[i])
162        });
163    }
164
165    /// The part of `tzloadbody` that finds the type of the instants before the first transition.
166    fn default_type(&self) -> usize {
167        if !self.types.contains(&0) {
168            return 0;
169        }
170        if let Some(&first) = self.types.first()
171            && self.ttis[usize::from(first)].isdst
172            && let Some(i) = (0..usize::from(first)).rev().find(|&i| !self.ttis[i].isdst)
173        {
174            return i;
175        }
176        self.ttis.iter().position(|t| !t.isdst).unwrap_or(0)
177    }
178
179    /// The instant moved by whole cycles of 400 years into the range of the transitions, and the shift, for an instant out of the range of a zone that repeats.
180    fn in_range(&self, t: i64) -> Option<(i64, i64)> {
181        let (Some(&first), Some(&last)) = (self.ats.first(), self.ats.last()) else { return None };
182        if !((self.goback && t < first) || (self.goahead && t > last)) {
183            return None;
184        }
185        let seconds = if t < first { first - t } else { t - last } - 1;
186        let shift = (seconds / SECS_PER_REPEAT + 1)
187            .checked_mul(YEARS_PER_REPEAT)?
188            .checked_mul(AVG_SECS_PER_YEAR)?;
189        let newt = if t < first { t.checked_add(shift)? } else { t.checked_sub(shift)? };
190        if newt < first || newt > last {
191            return None;
192        }
193        Some((newt, if t < first { -shift } else { shift }))
194    }
195
196    /// `localsub`: the type of the local time at an instant.
197    fn local_type(&self, t: i64) -> usize {
198        // `localsub` fails if the move overflows, which does not occur for the instants of a timestamp. Then the search below gives the type at the end of the range.
199        if let Some((newt, _)) = self.in_range(t) {
200            return self.local_type(newt);
201        }
202        if self.ats.is_empty() || t < self.ats[0] {
203            return self.defaulttype;
204        }
205        let (mut lo, mut hi) = (1, self.ats.len());
206        while lo < hi {
207            let mid = (lo + hi) >> 1;
208            if t < self.ats[mid] {
209                hi = mid;
210            } else {
211                lo = mid + 1;
212            }
213        }
214        usize::from(self.types[lo - 1])
215    }
216
217    /// `pg_next_dst_boundary`: the type at an instant, and the next transition after it with its type.
218    fn next_boundary(&self, t: i64) -> (Tti, Option<(i64, Tti)>) {
219        let tti = |i: u8| self.ttis[usize::from(i)];
220        let n = self.ats.len();
221        if n == 0 {
222            return (self.ttis[self.defaulttype], None);
223        }
224        if let Some((newt, shift)) = self.in_range(t) {
225            let (before, after) = self.next_boundary(newt);
226            return (before, after.map(|(boundary, after)| (boundary + shift, after)));
227        }
228        if t >= self.ats[n - 1] {
229            return (tti(self.types[n - 1]), None);
230        }
231        if t < self.ats[0] {
232            return (self.ttis[self.defaulttype], Some((self.ats[0], tti(self.types[0]))));
233        }
234        let (mut lo, mut hi) = (1, n - 1);
235        while lo < hi {
236            let mid = (lo + hi) >> 1;
237            if t < self.ats[mid] {
238                hi = mid;
239            } else {
240                lo = mid + 1;
241            }
242        }
243        (tti(self.types[lo - 1]), Some((self.ats[lo], tti(self.types[lo]))))
244    }
245
246    /// The index of an abbreviation at the start of a string of the buffer, as `pg_interpret_timezone_abbrev` finds it.
247    fn abbrev_index(&self, abbrev: &str) -> Option<usize> {
248        let mut index = 0;
249        while index < self.chars.len() {
250            let text = string_at(&self.chars, index);
251            if text == abbrev.as_bytes() {
252                return Some(index);
253            }
254            index += text.len() + 1;
255        }
256        None
257    }
258}
259
260/// A zone of the time zone database or of a POSIX zone string, with the name that `pg_get_timezone_name` gives.
261#[derive(Debug)]
262pub struct Zone {
263    name: String,
264    state: State,
265}
266
267impl Zone {
268    /// The canonical name: the name in the data, or the zone string in upper case.
269    pub fn name(&self) -> &str {
270        &self.name
271    }
272
273    /// `pg_localtime`: the offset east of UTC, the daylight saving flag and the abbreviation at an instant in seconds since 1970-01-01 UTC.
274    pub fn local(&self, unix_seconds: i64) -> (i32, bool, &str) {
275        let tti = self.state.ttis[self.state.local_type(unix_seconds)];
276        (tti.utoff, tti.isdst, self.abbrev(tti.desigidx))
277    }
278
279    /// `pg_tz_acceptable`: the local time of 2000-01-01 00:00 UTC is on a whole minute, so the zone does not count leap seconds.
280    pub fn acceptable(&self) -> bool {
281        let (offset, _, _) = self.local(TIME_2000);
282        (TIME_2000 + i64::from(offset)).rem_euclid(60) == 0
283    }
284
285    fn abbrev(&self, index: usize) -> &str {
286        std::str::from_utf8(string_at(&self.state.chars, index)).unwrap_or("")
287    }
288
289    /// The zone of a file of the data, by its name in the data.
290    fn from_file(name: &'static str) -> Option<Zone> {
291        let state = State::from_compiled(zic::compile(name)?)?;
292        Some(Zone { name: name.to_owned(), state })
293    }
294
295    /// `pg_tzset` without the cache.
296    fn make(upper: &str) -> Option<Zone> {
297        if upper == "GMT" {
298            return Some(Zone {
299                name: upper.to_owned(),
300                state: posix::tzparse(upper.as_bytes(), true)?,
301            });
302        }
303        if let Some(name) = zone_name(upper.strip_prefix(':').unwrap_or(upper))
304            && let Some(zone) = Zone::from_file(name)
305        {
306            return Some(zone);
307        }
308        if upper.starts_with(':') {
309            return None;
310        }
311        Some(Zone { name: upper.to_owned(), state: posix::tzparse(upper.as_bytes(), false)? })
312    }
313}
314
315impl TimeZone for Zone {
316    fn at(&self, unix_seconds: i64) -> (i32, &str) {
317        let (offset, _, abbrev) = self.local(unix_seconds);
318        (offset, abbrev)
319    }
320
321    fn next_change(&self, unix_seconds: i64) -> (i32, Option<(i64, i32)>) {
322        let (before, after) = self.state.next_boundary(unix_seconds);
323        (before.utoff, after.map(|(boundary, after)| (boundary, after.utoff)))
324    }
325
326    fn abbrev_meaning(&self, abbrev: &str) -> Option<AbbrevMeaning> {
327        let index = self.state.abbrev_index(abbrev)?;
328        let mut uses = self.state.ttis.iter().filter(|t| t.desigidx == index);
329        let first = uses.next()?;
330        if uses.all(|t| t.utoff == first.utoff && t.isdst == first.isdst) {
331            Some(AbbrevMeaning::Fixed { offset: first.utoff, dst: first.isdst })
332        } else {
333            Some(AbbrevMeaning::Varies)
334        }
335    }
336
337    fn abbrev_at(&self, abbrev: &str, unix_seconds: i64) -> Option<(i32, bool)> {
338        let state = &self.state;
339        let index = state.abbrev_index(abbrev)?;
340        let cutoff = state.ats.partition_point(|&at| at <= unix_seconds);
341        let tti = |i: usize| state.ttis[usize::from(state.types[i])];
342        let found = (0..cutoff)
343            .rev()
344            .map(tti)
345            .chain(std::iter::once(state.ttis[state.defaulttype]))
346            .chain((cutoff..state.ats.len()).map(tti))
347            .find(|t| t.desigidx == index)?;
348        Some((found.utoff, found.isdst))
349    }
350
351    fn fixed_offset(&self) -> Option<i32> {
352        let first = self.state.ttis[0].utoff;
353        self.state.ttis.iter().all(|t| t.utoff == first).then_some(first)
354    }
355
356    fn abbrevs(&self) -> Vec<&str> {
357        let mut out = Vec::new();
358        let mut index = 0;
359        while index < self.state.chars.len() {
360            let abbrev = string_at(&self.state.chars, index);
361            out.push(self.abbrev(index));
362            index += abbrev.len() + 1;
363        }
364        out
365    }
366}
367
368/// `pg_tzset`: the zone of a name of the data without regard to case, or of a POSIX zone string, or `None`. `GMT` is always the zone string, and a name that starts with `:` is only a name of the data.
369pub fn load(name: &str) -> Option<Arc<Zone>> {
370    static CACHE: OnceLock<Mutex<BTreeMap<String, Arc<Zone>>>> = OnceLock::new();
371    if name.len() > TZ_STRLEN_MAX {
372        return None;
373    }
374    let upper = name.to_ascii_uppercase();
375    let cache = CACHE.get_or_init(Mutex::default);
376    if let Some(zone) = cache.lock().unwrap_or_else(PoisonError::into_inner).get(&upper) {
377        return Some(Arc::clone(zone));
378    }
379    let zone = Arc::new(Zone::make(&upper)?);
380    cache.lock().unwrap_or_else(PoisonError::into_inner).insert(upper, Arc::clone(&zone));
381    Some(zone)
382}
383
384/// `pg_tzenumerate_next`: each zone of the data that [`Zone::acceptable`] takes, in the order of [`zone_names`]. The zones do not go into the cache of [`load`].
385pub fn zones() -> impl Iterator<Item = Zone> {
386    ZONE_NAMES.iter().filter_map(|name| Zone::from_file(name)).filter(Zone::acceptable)
387}
388
389/// The [`ZoneLookup`] of the time zone database: [`load`].
390#[derive(Debug, Clone, Copy, Default)]
391pub struct TzDatabase;
392
393impl ZoneLookup for TzDatabase {
394    fn zone(&self, name: &str) -> Option<Arc<dyn TimeZone + Send + Sync>> {
395        load(name).map(|zone| zone as Arc<dyn TimeZone + Send + Sync>)
396    }
397}
398
399#[cfg(test)]
400mod tests {
401    use super::*;
402
403    #[test]
404    fn the_rules_give_the_offsets_and_the_changes() {
405        let zone = load("america/new_york").unwrap();
406        assert_eq!(zone.name(), "America/New_York");
407        // 2024-07-01 12:00 UTC and 2024-01-01 12:00 UTC.
408        assert_eq!(zone.local(1_719_835_200), (-14400, true, "EDT"));
409        assert_eq!(zone.local(1_704_110_400), (-18000, false, "EST"));
410        // The change at 2024-03-10 07:00 UTC.
411        assert_eq!(zone.next_change(1_710_000_000), (-18000, Some((1_710_054_000, -14400))));
412        assert!(zone.acceptable());
413        let posix = load("xyz5abc").unwrap();
414        assert_eq!(posix.name(), "XYZ5ABC");
415        assert_eq!(posix.local(1_719_835_200), (-14400, true, "ABC"));
416        assert!(!load("XYZ+3:00:30").unwrap().acceptable());
417        assert!(load(":XYZ5ABC").is_none());
418        assert_eq!(zones().count(), 598);
419    }
420
421    #[test]
422    fn names_are_found_without_regard_to_case() {
423        assert_eq!(zone_name("europe/paris"), Some("Europe/Paris"));
424        assert_eq!(zone_name("UTC"), Some("UTC"));
425        assert_eq!(zone_name("us/eastern"), Some("US/Eastern"));
426        assert_eq!(zone_name("Mars/Olympus"), None);
427        assert_eq!(zone_names().len(), 598);
428    }
429}