tz/timezone/
mod.rs

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