tz/timezone/
mod.rs

1//! Types related to a time zone.
2
3mod rule;
4
5pub use rule::*;
6
7use crate::datetime::{days_since_unix_epoch, is_leap_year};
8use crate::error::*;
9use crate::utils::*;
10use crate::UtcDateTime;
11
12use core::cmp::Ordering;
13use core::fmt;
14use core::str;
15
16#[cfg(feature = "alloc")]
17use alloc::{vec, vec::Vec};
18
19/// Transition of a TZif file
20#[derive(Debug, Copy, Clone, Eq, PartialEq)]
21pub struct Transition {
22    /// Unix leap time
23    unix_leap_time: i64,
24    /// Index specifying the local time type of the transition
25    local_time_type_index: usize,
26}
27
28impl Transition {
29    /// Construct a TZif file transition
30    #[inline]
31    #[cfg_attr(feature = "const", const_fn::const_fn)]
32    pub fn new(unix_leap_time: i64, local_time_type_index: usize) -> Self {
33        Self { unix_leap_time, local_time_type_index }
34    }
35
36    /// Returns Unix leap time
37    #[inline]
38    #[cfg_attr(feature = "const", const_fn::const_fn)]
39    pub fn unix_leap_time(&self) -> i64 {
40        self.unix_leap_time
41    }
42
43    /// Returns local time type index
44    #[inline]
45    #[cfg_attr(feature = "const", const_fn::const_fn)]
46    pub fn local_time_type_index(&self) -> usize {
47        self.local_time_type_index
48    }
49}
50
51/// Leap second of a TZif file
52#[derive(Debug, Copy, Clone, Eq, PartialEq)]
53pub struct LeapSecond {
54    /// Unix leap time
55    unix_leap_time: i64,
56    /// Leap second correction
57    correction: i32,
58}
59
60impl LeapSecond {
61    /// Construct a TZif file leap second
62    #[inline]
63    #[cfg_attr(feature = "const", const_fn::const_fn)]
64    pub fn new(unix_leap_time: i64, correction: i32) -> Self {
65        Self { unix_leap_time, correction }
66    }
67
68    /// Returns Unix leap time
69    #[inline]
70    #[cfg_attr(feature = "const", const_fn::const_fn)]
71    pub fn unix_leap_time(&self) -> i64 {
72        self.unix_leap_time
73    }
74
75    /// Returns leap second correction
76    #[inline]
77    #[cfg_attr(feature = "const", const_fn::const_fn)]
78    pub fn correction(&self) -> i32 {
79        self.correction
80    }
81}
82
83/// ASCII-encoded fixed-capacity string, used for storing time zone designations
84#[derive(Copy, Clone, Eq, PartialEq)]
85struct TzAsciiStr {
86    /// Length-prefixed string buffer
87    bytes: [u8; 8],
88}
89
90impl TzAsciiStr {
91    /// Construct a time zone designation string
92    #[cfg_attr(feature = "const", const_fn::const_fn)]
93    fn new(input: &[u8]) -> Result<Self, LocalTimeTypeError> {
94        let len = input.len();
95
96        if !(3 <= len && len <= 7) {
97            return Err(LocalTimeTypeError("time zone designation must have between 3 and 7 characters"));
98        }
99
100        let mut bytes = [0; 8];
101        bytes[0] = input.len() as u8;
102
103        let mut i = 0;
104        while i < len {
105            let b = input[i];
106
107            if !matches!(b, b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z' | b'+' | b'-') {
108                return Err(LocalTimeTypeError("invalid characters in time zone designation"));
109            }
110
111            bytes[i + 1] = b;
112
113            i += 1;
114        }
115
116        Ok(Self { bytes })
117    }
118
119    /// Returns time zone designation as a byte slice
120    #[inline]
121    #[cfg_attr(feature = "const", const_fn::const_fn)]
122    fn as_bytes(&self) -> &[u8] {
123        match &self.bytes {
124            [3, head @ .., _, _, _, _] => head,
125            [4, head @ .., _, _, _] => head,
126            [5, head @ .., _, _] => head,
127            [6, head @ .., _] => head,
128            [7, head @ ..] => head,
129            _ => const_panic!(), // unreachable
130        }
131    }
132
133    /// Returns time zone designation as a string
134    #[inline]
135    #[cfg_attr(feature = "const", const_fn::const_fn)]
136    fn as_str(&self) -> &str {
137        // SAFETY: ASCII is valid UTF-8
138        unsafe { str::from_utf8_unchecked(self.as_bytes()) }
139    }
140
141    /// Check if two time zone designations are equal
142    #[inline]
143    #[cfg_attr(feature = "const", const_fn::const_fn)]
144    fn equal(&self, other: &Self) -> bool {
145        u64::from_ne_bytes(self.bytes) == u64::from_ne_bytes(other.bytes)
146    }
147}
148
149impl fmt::Debug for TzAsciiStr {
150    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
151        self.as_str().fmt(f)
152    }
153}
154
155/// Local time type associated to a time zone
156#[derive(Debug, Copy, Clone, Eq, PartialEq)]
157pub struct LocalTimeType {
158    /// Offset from UTC in seconds
159    ut_offset: i32,
160    /// Daylight Saving Time indicator
161    is_dst: bool,
162    /// Time zone designation
163    time_zone_designation: Option<TzAsciiStr>,
164}
165
166impl LocalTimeType {
167    /// Construct a local time type
168    #[cfg_attr(feature = "const", const_fn::const_fn)]
169    pub fn new(ut_offset: i32, is_dst: bool, time_zone_designation: Option<&[u8]>) -> Result<Self, LocalTimeTypeError> {
170        if ut_offset == i32::MIN {
171            return Err(LocalTimeTypeError("invalid UTC offset"));
172        }
173
174        let time_zone_designation = match time_zone_designation {
175            None => None,
176            Some(time_zone_designation) => match TzAsciiStr::new(time_zone_designation) {
177                Err(error) => return Err(error),
178                Ok(time_zone_designation) => Some(time_zone_designation),
179            },
180        };
181
182        Ok(Self { ut_offset, is_dst, time_zone_designation })
183    }
184
185    /// Construct the local time type associated to UTC
186    #[inline]
187    pub const fn utc() -> Self {
188        Self { ut_offset: 0, is_dst: false, time_zone_designation: None }
189    }
190
191    /// Construct a local time type with the specified UTC offset in seconds
192    #[inline]
193    #[cfg_attr(feature = "const", const_fn::const_fn)]
194    pub fn with_ut_offset(ut_offset: i32) -> Result<Self, LocalTimeTypeError> {
195        if ut_offset == i32::MIN {
196            return Err(LocalTimeTypeError("invalid UTC offset"));
197        }
198
199        Ok(Self { ut_offset, is_dst: false, time_zone_designation: None })
200    }
201
202    /// Returns offset from UTC in seconds
203    #[inline]
204    #[cfg_attr(feature = "const", const_fn::const_fn)]
205    pub fn ut_offset(&self) -> i32 {
206        self.ut_offset
207    }
208
209    /// Returns daylight saving time indicator
210    #[inline]
211    #[cfg_attr(feature = "const", const_fn::const_fn)]
212    pub fn is_dst(&self) -> bool {
213        self.is_dst
214    }
215
216    /// Returns time zone designation
217    #[inline]
218    #[cfg_attr(feature = "const", const_fn::const_fn)]
219    pub fn time_zone_designation(&self) -> &str {
220        match &self.time_zone_designation {
221            Some(s) => s.as_str(),
222            None => "",
223        }
224    }
225
226    /// Check if two local time types are equal
227    #[inline]
228    #[cfg_attr(feature = "const", const_fn::const_fn)]
229    fn equal(&self, other: &Self) -> bool {
230        self.ut_offset == other.ut_offset
231            && self.is_dst == other.is_dst
232            && match (&self.time_zone_designation, &other.time_zone_designation) {
233                (Some(x), Some(y)) => x.equal(y),
234                (None, None) => true,
235                _ => false,
236            }
237    }
238}
239
240/// Time zone
241#[cfg(feature = "alloc")]
242#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
243#[derive(Debug, Clone, Eq, PartialEq)]
244pub struct TimeZone {
245    /// List of transitions
246    transitions: Vec<Transition>,
247    /// List of local time types (cannot be empty)
248    local_time_types: Vec<LocalTimeType>,
249    /// List of leap seconds
250    leap_seconds: Vec<LeapSecond>,
251    /// Extra transition rule applicable after the last transition
252    extra_rule: Option<TransitionRule>,
253}
254
255/// Reference to a time zone
256#[derive(Debug, Copy, Clone, Eq, PartialEq)]
257pub struct TimeZoneRef<'a> {
258    /// List of transitions
259    transitions: &'a [Transition],
260    /// List of local time types (cannot be empty)
261    local_time_types: &'a [LocalTimeType],
262    /// List of leap seconds
263    leap_seconds: &'a [LeapSecond],
264    /// Extra transition rule applicable after the last transition
265    extra_rule: &'a Option<TransitionRule>,
266}
267
268impl<'a> TimeZoneRef<'a> {
269    /// Construct a time zone reference
270    #[cfg_attr(feature = "const", const_fn::const_fn)]
271    pub fn new(
272        transitions: &'a [Transition],
273        local_time_types: &'a [LocalTimeType],
274        leap_seconds: &'a [LeapSecond],
275        extra_rule: &'a Option<TransitionRule>,
276    ) -> Result<Self, TimeZoneError> {
277        let time_zone_ref = Self::new_unchecked(transitions, local_time_types, leap_seconds, extra_rule);
278
279        if let Err(error) = time_zone_ref.check_inputs() {
280            return Err(error);
281        }
282
283        Ok(time_zone_ref)
284    }
285
286    /// Construct the time zone reference associated to UTC
287    #[inline]
288    #[cfg_attr(feature = "const", const_fn::const_fn)]
289    pub fn utc() -> Self {
290        const UTC: LocalTimeType = LocalTimeType::utc();
291        Self { transitions: &[], local_time_types: &[UTC], leap_seconds: &[], extra_rule: &None }
292    }
293
294    /// Returns list of transitions
295    #[inline]
296    #[cfg_attr(feature = "const", const_fn::const_fn)]
297    pub fn transitions(&self) -> &'a [Transition] {
298        self.transitions
299    }
300
301    /// Returns list of local time types
302    #[inline]
303    #[cfg_attr(feature = "const", const_fn::const_fn)]
304    pub fn local_time_types(&self) -> &'a [LocalTimeType] {
305        self.local_time_types
306    }
307
308    /// Returns list of leap seconds
309    #[inline]
310    #[cfg_attr(feature = "const", const_fn::const_fn)]
311    pub fn leap_seconds(&self) -> &'a [LeapSecond] {
312        self.leap_seconds
313    }
314
315    /// Returns extra transition rule applicable after the last transition
316    #[inline]
317    #[cfg_attr(feature = "const", const_fn::const_fn)]
318    pub fn extra_rule(&self) -> &'a Option<TransitionRule> {
319        self.extra_rule
320    }
321
322    /// Find the local time type associated to the time zone at the specified Unix time in seconds
323    #[cfg_attr(feature = "const", const_fn::const_fn)]
324    pub fn find_local_time_type(&self, unix_time: i64) -> Result<&'a LocalTimeType, FindLocalTimeTypeError> {
325        let extra_rule = match self.transitions {
326            [] => match self.extra_rule {
327                Some(extra_rule) => extra_rule,
328                None => return Ok(&self.local_time_types[0]),
329            },
330            [.., last_transition] => {
331                let unix_leap_time = match self.unix_time_to_unix_leap_time(unix_time) {
332                    Ok(unix_leap_time) => unix_leap_time,
333                    Err(OutOfRangeError(error)) => return Err(FindLocalTimeTypeError(error)),
334                };
335
336                if unix_leap_time >= last_transition.unix_leap_time {
337                    match self.extra_rule {
338                        Some(extra_rule) => extra_rule,
339                        None => return Err(FindLocalTimeTypeError("no local time type is available for the specified timestamp")),
340                    }
341                } else {
342                    let index = match binary_search_transitions(self.transitions, unix_leap_time) {
343                        Ok(x) => x + 1,
344                        Err(x) => x,
345                    };
346
347                    let local_time_type_index = if index > 0 { self.transitions[index - 1].local_time_type_index } else { 0 };
348                    return Ok(&self.local_time_types[local_time_type_index]);
349                }
350            }
351        };
352
353        match extra_rule.find_local_time_type(unix_time) {
354            Ok(local_time_type) => Ok(local_time_type),
355            Err(OutOfRangeError(error)) => Err(FindLocalTimeTypeError(error)),
356        }
357    }
358
359    /// Construct a reference to a time zone
360    #[inline]
361    #[cfg_attr(feature = "const", const_fn::const_fn)]
362    fn new_unchecked(
363        transitions: &'a [Transition],
364        local_time_types: &'a [LocalTimeType],
365        leap_seconds: &'a [LeapSecond],
366        extra_rule: &'a Option<TransitionRule>,
367    ) -> Self {
368        Self { transitions, local_time_types, leap_seconds, extra_rule }
369    }
370
371    /// Check time zone inputs
372    #[cfg_attr(feature = "const", const_fn::const_fn)]
373    fn check_inputs(&self) -> Result<(), TimeZoneError> {
374        use crate::constants::*;
375
376        // Check local time types
377        let local_time_types_size = self.local_time_types.len();
378        if local_time_types_size == 0 {
379            return Err(TimeZoneError("list of local time types must not be empty"));
380        }
381
382        // Check transitions
383        let mut i_transition = 0;
384        while i_transition < self.transitions.len() {
385            if self.transitions[i_transition].local_time_type_index >= local_time_types_size {
386                return Err(TimeZoneError("invalid local time type index"));
387            }
388
389            if i_transition + 1 < self.transitions.len() && self.transitions[i_transition].unix_leap_time >= self.transitions[i_transition + 1].unix_leap_time {
390                return Err(TimeZoneError("invalid transition"));
391            }
392
393            i_transition += 1;
394        }
395
396        // Check leap seconds
397        if !(self.leap_seconds.is_empty() || self.leap_seconds[0].unix_leap_time >= 0 && self.leap_seconds[0].correction.saturating_abs() == 1) {
398            return Err(TimeZoneError("invalid leap second"));
399        }
400
401        let min_interval = SECONDS_PER_28_DAYS - 1;
402
403        let mut i_leap_second = 0;
404        while i_leap_second < self.leap_seconds.len() {
405            if i_leap_second + 1 < self.leap_seconds.len() {
406                let x0 = &self.leap_seconds[i_leap_second];
407                let x1 = &self.leap_seconds[i_leap_second + 1];
408
409                let diff_unix_leap_time = x1.unix_leap_time.saturating_sub(x0.unix_leap_time);
410                let abs_diff_correction = x1.correction.saturating_sub(x0.correction).saturating_abs();
411
412                if !(diff_unix_leap_time >= min_interval && abs_diff_correction == 1) {
413                    return Err(TimeZoneError("invalid leap second"));
414                }
415            }
416            i_leap_second += 1;
417        }
418
419        // Check extra rule
420        if let (Some(extra_rule), [.., last_transition]) = (&self.extra_rule, self.transitions) {
421            let last_local_time_type = &self.local_time_types[last_transition.local_time_type_index];
422
423            let unix_time = match self.unix_leap_time_to_unix_time(last_transition.unix_leap_time) {
424                Ok(unix_time) => unix_time,
425                Err(OutOfRangeError(error)) => return Err(TimeZoneError(error)),
426            };
427
428            let rule_local_time_type = match extra_rule.find_local_time_type(unix_time) {
429                Ok(rule_local_time_type) => rule_local_time_type,
430                Err(OutOfRangeError(error)) => return Err(TimeZoneError(error)),
431            };
432
433            if !last_local_time_type.equal(rule_local_time_type) {
434                return Err(TimeZoneError("extra transition rule is inconsistent with the last transition"));
435            }
436        }
437
438        Ok(())
439    }
440
441    /// Convert Unix time to Unix leap time, from the list of leap seconds in a time zone
442    #[cfg_attr(feature = "const", const_fn::const_fn)]
443    pub(crate) fn unix_time_to_unix_leap_time(&self, unix_time: i64) -> Result<i64, OutOfRangeError> {
444        let mut unix_leap_time = unix_time;
445
446        let mut i = 0;
447        while i < self.leap_seconds.len() {
448            let leap_second = &self.leap_seconds[i];
449
450            if unix_leap_time < leap_second.unix_leap_time {
451                break;
452            }
453
454            unix_leap_time = match unix_time.checked_add(leap_second.correction as i64) {
455                Some(unix_leap_time) => unix_leap_time,
456                None => return Err(OutOfRangeError("out of range operation")),
457            };
458
459            i += 1;
460        }
461
462        Ok(unix_leap_time)
463    }
464
465    /// Convert Unix leap time to Unix time, from the list of leap seconds in a time zone
466    #[cfg_attr(feature = "const", const_fn::const_fn)]
467    pub(crate) fn unix_leap_time_to_unix_time(&self, unix_leap_time: i64) -> Result<i64, OutOfRangeError> {
468        if unix_leap_time == i64::MIN {
469            return Err(OutOfRangeError("out of range operation"));
470        }
471
472        let index = match binary_search_leap_seconds(self.leap_seconds, unix_leap_time - 1) {
473            Ok(x) => x + 1,
474            Err(x) => x,
475        };
476
477        let correction = if index > 0 { self.leap_seconds[index - 1].correction } else { 0 };
478
479        match unix_leap_time.checked_sub(correction as i64) {
480            Some(unix_time) => Ok(unix_time),
481            None => Err(OutOfRangeError("out of range operation")),
482        }
483    }
484}
485
486#[cfg(feature = "alloc")]
487impl TimeZone {
488    /// Construct a time zone
489    pub fn new(
490        transitions: Vec<Transition>,
491        local_time_types: Vec<LocalTimeType>,
492        leap_seconds: Vec<LeapSecond>,
493        extra_rule: Option<TransitionRule>,
494    ) -> Result<Self, TimeZoneError> {
495        TimeZoneRef::new_unchecked(&transitions, &local_time_types, &leap_seconds, &extra_rule).check_inputs()?;
496        Ok(Self { transitions, local_time_types, leap_seconds, extra_rule })
497    }
498
499    /// Returns a reference to the time zone
500    #[inline]
501    pub fn as_ref(&self) -> TimeZoneRef {
502        TimeZoneRef::new_unchecked(&self.transitions, &self.local_time_types, &self.leap_seconds, &self.extra_rule)
503    }
504
505    /// Construct the time zone associated to UTC
506    #[inline]
507    pub fn utc() -> Self {
508        Self { transitions: Vec::new(), local_time_types: vec![LocalTimeType::utc()], leap_seconds: Vec::new(), extra_rule: None }
509    }
510
511    /// Construct a time zone with the specified UTC offset in seconds
512    #[inline]
513    pub fn fixed(ut_offset: i32) -> Result<Self, LocalTimeTypeError> {
514        Ok(Self { transitions: Vec::new(), local_time_types: vec![LocalTimeType::with_ut_offset(ut_offset)?], leap_seconds: Vec::new(), extra_rule: None })
515    }
516
517    /// Returns local time zone.
518    ///
519    /// This method in not supported on non-UNIX platforms, and returns the UTC time zone instead.
520    ///
521    #[cfg(feature = "std")]
522    #[cfg_attr(docsrs, doc(cfg(feature = "std")))]
523    pub fn local() -> Result<Self, TzError> {
524        #[cfg(not(unix))]
525        let local_time_zone = Self::utc();
526
527        #[cfg(unix)]
528        let local_time_zone = Self::from_posix_tz("localtime")?;
529
530        Ok(local_time_zone)
531    }
532
533    /// Construct a time zone from the contents of a time zone file
534    #[cfg(feature = "std")]
535    #[cfg_attr(docsrs, doc(cfg(feature = "std")))]
536    pub fn from_tz_data(bytes: &[u8]) -> Result<Self, TzError> {
537        crate::parse::parse_tz_file(bytes)
538    }
539
540    /// Construct a time zone from a POSIX TZ string, as described in [the POSIX documentation of the `TZ` environment variable](https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html).
541    #[cfg(feature = "std")]
542    #[cfg_attr(docsrs, doc(cfg(feature = "std")))]
543    pub fn from_posix_tz(tz_string: &str) -> Result<Self, TzError> {
544        use crate::parse::*;
545
546        use std::fs::{self, File};
547        use std::io::{self, Read};
548
549        if tz_string.is_empty() {
550            return Err(TzError::TzStringError(TzStringError::InvalidTzString("empty TZ string")));
551        }
552
553        if tz_string == "localtime" {
554            return parse_tz_file(&fs::read("/etc/localtime")?);
555        }
556
557        let read = |mut file: File| -> io::Result<_> {
558            let mut bytes = Vec::new();
559            file.read_to_end(&mut bytes)?;
560            Ok(bytes)
561        };
562
563        let mut chars = tz_string.chars();
564        if chars.next() == Some(':') {
565            return parse_tz_file(&read(get_tz_file(chars.as_str())?)?);
566        }
567
568        match get_tz_file(tz_string) {
569            Ok(file) => parse_tz_file(&read(file)?),
570            Err(_) => {
571                let tz_string = tz_string.trim_matches(|c: char| c.is_ascii_whitespace());
572
573                // TZ string extensions are not allowed
574                let rule = parse_posix_tz(tz_string.as_bytes(), false)?;
575
576                let local_time_types = match rule {
577                    TransitionRule::Fixed(local_time_type) => vec![local_time_type],
578                    TransitionRule::Alternate(alternate_time) => vec![*alternate_time.std(), *alternate_time.dst()],
579                };
580
581                Ok(TimeZone::new(vec![], local_time_types, vec![], Some(rule))?)
582            }
583        }
584    }
585
586    /// Find the current local time type associated to the time zone
587    #[cfg(feature = "std")]
588    #[cfg_attr(docsrs, doc(cfg(feature = "std")))]
589    pub fn find_current_local_time_type(&self) -> Result<&LocalTimeType, TzError> {
590        use core::convert::TryInto;
591        use std::time::SystemTime;
592
593        Ok(self.find_local_time_type(SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)?.as_secs().try_into()?)?)
594    }
595
596    /// Find the local time type associated to the time zone at the specified Unix time in seconds
597    pub fn find_local_time_type(&self, unix_time: i64) -> Result<&LocalTimeType, FindLocalTimeTypeError> {
598        self.as_ref().find_local_time_type(unix_time)
599    }
600}
601
602#[cfg(test)]
603mod test {
604    use super::*;
605    use crate::Result;
606
607    #[test]
608    fn test_tz_ascii_str() -> Result<()> {
609        assert!(matches!(TzAsciiStr::new(b""), Err(LocalTimeTypeError(_))));
610        assert!(matches!(TzAsciiStr::new(b"1"), Err(LocalTimeTypeError(_))));
611        assert!(matches!(TzAsciiStr::new(b"12"), Err(LocalTimeTypeError(_))));
612        assert_eq!(TzAsciiStr::new(b"123")?.as_bytes(), b"123");
613        assert_eq!(TzAsciiStr::new(b"1234")?.as_bytes(), b"1234");
614        assert_eq!(TzAsciiStr::new(b"12345")?.as_bytes(), b"12345");
615        assert_eq!(TzAsciiStr::new(b"123456")?.as_bytes(), b"123456");
616        assert_eq!(TzAsciiStr::new(b"1234567")?.as_bytes(), b"1234567");
617        assert!(matches!(TzAsciiStr::new(b"12345678"), Err(LocalTimeTypeError(_))));
618        assert!(matches!(TzAsciiStr::new(b"123456789"), Err(LocalTimeTypeError(_))));
619        assert!(matches!(TzAsciiStr::new(b"1234567890"), Err(LocalTimeTypeError(_))));
620
621        assert!(matches!(TzAsciiStr::new(b"123\0\0\0"), Err(LocalTimeTypeError(_))));
622
623        Ok(())
624    }
625
626    #[cfg(feature = "alloc")]
627    #[test]
628    fn test_time_zone() -> Result<()> {
629        let utc = LocalTimeType::utc();
630        let cet = LocalTimeType::with_ut_offset(3600)?;
631
632        let utc_local_time_types = vec![utc];
633        let fixed_extra_rule = TransitionRule::Fixed(cet);
634
635        let time_zone_1 = TimeZone::new(vec![], utc_local_time_types.clone(), vec![], None)?;
636        let time_zone_2 = TimeZone::new(vec![], utc_local_time_types.clone(), vec![], Some(fixed_extra_rule))?;
637        let time_zone_3 = TimeZone::new(vec![Transition::new(0, 0)], utc_local_time_types.clone(), vec![], None)?;
638        let time_zone_4 = TimeZone::new(vec![Transition::new(i32::MIN.into(), 0), Transition::new(0, 1)], vec![utc, cet], vec![], Some(fixed_extra_rule))?;
639
640        assert_eq!(*time_zone_1.find_local_time_type(0)?, utc);
641        assert_eq!(*time_zone_2.find_local_time_type(0)?, cet);
642
643        assert_eq!(*time_zone_3.find_local_time_type(-1)?, utc);
644        assert!(matches!(time_zone_3.find_local_time_type(0), Err(FindLocalTimeTypeError(_))));
645
646        assert_eq!(*time_zone_4.find_local_time_type(-1)?, utc);
647        assert_eq!(*time_zone_4.find_local_time_type(0)?, cet);
648
649        let time_zone_err = TimeZone::new(vec![Transition::new(0, 0)], utc_local_time_types, vec![], Some(fixed_extra_rule));
650        assert!(time_zone_err.is_err());
651
652        Ok(())
653    }
654
655    #[cfg(feature = "std")]
656    #[test]
657    fn test_time_zone_from_posix_tz() -> Result<()> {
658        #[cfg(unix)]
659        {
660            let time_zone_local = TimeZone::local()?;
661            let time_zone_local_1 = TimeZone::from_posix_tz("localtime")?;
662            let time_zone_local_2 = TimeZone::from_posix_tz("/etc/localtime")?;
663            let time_zone_local_3 = TimeZone::from_posix_tz(":/etc/localtime")?;
664
665            assert_eq!(time_zone_local, time_zone_local_1);
666            assert_eq!(time_zone_local, time_zone_local_2);
667            assert_eq!(time_zone_local, time_zone_local_3);
668
669            assert!(matches!(time_zone_local.find_current_local_time_type(), Ok(_) | Err(TzError::FindLocalTimeTypeError(_))));
670
671            let time_zone_utc = TimeZone::from_posix_tz("UTC")?;
672            assert_eq!(time_zone_utc.find_local_time_type(0)?.ut_offset(), 0);
673        }
674
675        assert!(TimeZone::from_posix_tz("EST5EDT,0/0,J365/25").is_err());
676        assert!(TimeZone::from_posix_tz("").is_err());
677
678        Ok(())
679    }
680
681    #[cfg(feature = "alloc")]
682    #[test]
683    fn test_leap_seconds() -> Result<()> {
684        let time_zone = TimeZone::new(
685            vec![],
686            vec![LocalTimeType::new(0, false, Some(b"UTC"))?],
687            vec![
688                LeapSecond::new(78796800, 1),
689                LeapSecond::new(94694401, 2),
690                LeapSecond::new(126230402, 3),
691                LeapSecond::new(157766403, 4),
692                LeapSecond::new(189302404, 5),
693                LeapSecond::new(220924805, 6),
694                LeapSecond::new(252460806, 7),
695                LeapSecond::new(283996807, 8),
696                LeapSecond::new(315532808, 9),
697                LeapSecond::new(362793609, 10),
698                LeapSecond::new(394329610, 11),
699                LeapSecond::new(425865611, 12),
700                LeapSecond::new(489024012, 13),
701                LeapSecond::new(567993613, 14),
702                LeapSecond::new(631152014, 15),
703                LeapSecond::new(662688015, 16),
704                LeapSecond::new(709948816, 17),
705                LeapSecond::new(741484817, 18),
706                LeapSecond::new(773020818, 19),
707                LeapSecond::new(820454419, 20),
708                LeapSecond::new(867715220, 21),
709                LeapSecond::new(915148821, 22),
710                LeapSecond::new(1136073622, 23),
711                LeapSecond::new(1230768023, 24),
712                LeapSecond::new(1341100824, 25),
713                LeapSecond::new(1435708825, 26),
714                LeapSecond::new(1483228826, 27),
715            ],
716            None,
717        )?;
718
719        let time_zone_ref = time_zone.as_ref();
720
721        assert!(matches!(time_zone_ref.unix_leap_time_to_unix_time(1136073621), Ok(1136073599)));
722        assert!(matches!(time_zone_ref.unix_leap_time_to_unix_time(1136073622), Ok(1136073600)));
723        assert!(matches!(time_zone_ref.unix_leap_time_to_unix_time(1136073623), Ok(1136073600)));
724        assert!(matches!(time_zone_ref.unix_leap_time_to_unix_time(1136073624), Ok(1136073601)));
725
726        assert!(matches!(time_zone_ref.unix_time_to_unix_leap_time(1136073599), Ok(1136073621)));
727        assert!(matches!(time_zone_ref.unix_time_to_unix_leap_time(1136073600), Ok(1136073623)));
728        assert!(matches!(time_zone_ref.unix_time_to_unix_leap_time(1136073601), Ok(1136073624)));
729
730        Ok(())
731    }
732
733    #[cfg(feature = "alloc")]
734    #[test]
735    fn test_leap_seconds_overflow() -> Result<()> {
736        let time_zone_err = TimeZone::new(
737            vec![Transition::new(i64::MIN, 0)],
738            vec![LocalTimeType::utc()],
739            vec![LeapSecond::new(0, 1)],
740            Some(TransitionRule::Fixed(LocalTimeType::utc())),
741        );
742        assert!(time_zone_err.is_err());
743
744        let time_zone = TimeZone::new(vec![Transition::new(i64::MAX, 0)], vec![LocalTimeType::utc()], vec![LeapSecond::new(0, 1)], None)?;
745        assert!(matches!(time_zone.find_local_time_type(i64::MAX), Err(FindLocalTimeTypeError(_))));
746
747        Ok(())
748    }
749}