1mod 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
14const TZ_MAX_TIMES: usize = 2000;
16const TZ_MAX_TYPES: usize = 256;
17const TZ_MAX_CHARS: usize = 50;
18const 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;
23const TIME_2000: i64 = 946_684_800;
25
26pub 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
35pub fn zone_names() -> &'static [&'static str] {
37 &ZONE_NAMES
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42struct Tti {
43 utoff: i32,
44 isdst: bool,
45 desigidx: usize,
46}
47
48#[derive(Debug, Clone, Default)]
50struct State {
51 ats: Vec<i64>,
52 types: Vec<u8>,
53 ttis: Vec<Tti>,
54 chars: Vec<u8>,
56 goback: bool,
57 goahead: bool,
58 defaulttype: usize,
59}
60
61fn 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 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 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 #[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 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 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 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 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 fn local_type(&self, t: i64) -> usize {
198 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 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 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#[derive(Debug)]
262pub struct Zone {
263 name: String,
264 state: State,
265}
266
267impl Zone {
268 pub fn name(&self) -> &str {
270 &self.name
271 }
272
273 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 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 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 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
368pub 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
384pub fn zones() -> impl Iterator<Item = Zone> {
386 ZONE_NAMES.iter().filter_map(|name| Zone::from_file(name)).filter(Zone::acceptable)
387}
388
389#[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 assert_eq!(zone.local(1_719_835_200), (-14400, true, "EDT"));
409 assert_eq!(zone.local(1_704_110_400), (-18000, false, "EST"));
410 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}