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