1mod 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#[derive(Debug, Copy, Clone, Eq, PartialEq)]
29pub struct Transition {
30 unix_leap_time: i64,
32 local_time_type_index: usize,
34}
35
36impl Transition {
37 #[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 #[inline]
45 pub const fn unix_leap_time(&self) -> i64 {
46 self.unix_leap_time
47 }
48
49 #[inline]
51 pub const fn local_time_type_index(&self) -> usize {
52 self.local_time_type_index
53 }
54}
55
56#[derive(Debug, Copy, Clone, Eq, PartialEq)]
58pub struct LeapSecond {
59 unix_leap_time: i64,
61 correction: i32,
63}
64
65impl LeapSecond {
66 #[inline]
68 pub const fn new(unix_leap_time: i64, correction: i32) -> Self {
69 Self { unix_leap_time, correction }
70 }
71
72 #[inline]
74 pub const fn unix_leap_time(&self) -> i64 {
75 self.unix_leap_time
76 }
77
78 #[inline]
80 pub const fn correction(&self) -> i32 {
81 self.correction
82 }
83}
84
85#[derive(Copy, Clone, Eq, PartialEq)]
90struct TzAsciiStr {
91 bytes: [u8; 8],
93}
94
95impl TzAsciiStr {
96 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 #[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 #[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 #[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#[derive(Debug, Copy, Clone, Eq, PartialEq)]
162pub struct LocalTimeType {
163 ut_offset: i32,
165 is_dst: bool,
167 time_zone_designation: Option<TzAsciiStr>,
169}
170
171impl LocalTimeType {
172 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 #[inline]
191 pub const fn utc() -> Self {
192 Self { ut_offset: 0, is_dst: false, time_zone_designation: None }
193 }
194
195 #[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 #[inline]
207 pub const fn ut_offset(&self) -> i32 {
208 self.ut_offset
209 }
210
211 #[inline]
213 pub const fn is_dst(&self) -> bool {
214 self.is_dst
215 }
216
217 #[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 #[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#[derive(Debug, Copy, Clone, Eq, PartialEq)]
241pub struct TimeZoneRef<'a> {
242 transitions: &'a [Transition],
244 local_time_types: &'a [LocalTimeType],
246 leap_seconds: &'a [LeapSecond],
248 extra_rule: &'a Option<TransitionRule>,
250}
251
252impl<'a> TimeZoneRef<'a> {
253 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 #[inline]
271 pub const fn utc() -> Self {
272 Self { transitions: &[], local_time_types: &[const { LocalTimeType::utc() }], leap_seconds: &[], extra_rule: &None }
273 }
274
275 #[inline]
277 pub const fn transitions(&self) -> &'a [Transition] {
278 self.transitions
279 }
280
281 #[inline]
283 pub const fn local_time_types(&self) -> &'a [LocalTimeType] {
284 self.local_time_types
285 }
286
287 #[inline]
289 pub const fn leap_seconds(&self) -> &'a [LeapSecond] {
290 self.leap_seconds
291 }
292
293 #[inline]
295 pub const fn extra_rule(&self) -> &'a Option<TransitionRule> {
296 self.extra_rule
297 }
298
299 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 #[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 const fn check_inputs(&self) -> Result<(), TzError> {
345 use crate::constants::*;
346
347 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 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 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 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 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 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#[cfg(feature = "alloc")]
457#[derive(Debug, Clone, Eq, PartialEq)]
458pub struct TimeZone {
459 transitions: Vec<Transition>,
461 local_time_types: Vec<LocalTimeType>,
463 leap_seconds: Vec<LeapSecond>,
465 extra_rule: Option<TransitionRule>,
467}
468
469#[cfg(feature = "alloc")]
470impl TimeZone {
471 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 #[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 #[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 #[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 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 pub fn from_tz_data(bytes: &[u8]) -> Result<Self, TzError> {
507 parse_tz_file(bytes)
508 }
509
510 #[cfg(feature = "std")]
515 pub fn local() -> Result<Self, crate::Error> {
516 TimeZoneSettings::DEFAULT.parse_local()
517 }
518
519 #[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 #[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#[cfg(feature = "alloc")]
534type ReadFileFn = fn(path: &str) -> Result<Vec<u8>, Box<dyn core::error::Error + Send + Sync + 'static>>;
535
536#[cfg(feature = "alloc")]
538#[derive(Debug)]
539pub struct TimeZoneSettings<'a> {
540 directories: &'a [&'a str],
542 read_file_fn: ReadFileFn,
544}
545
546#[cfg(feature = "alloc")]
547impl<'a> TimeZoneSettings<'a> {
548 pub const DEFAULT_DIRECTORIES: &'static [&'static str] = &["/usr/share/zoneinfo", "/share/zoneinfo", "/etc/zoneinfo"];
550
551 #[cfg(feature = "std")]
553 pub const DEFAULT_READ_FILE_FN: ReadFileFn = |path| Ok(std::fs::read(path)?);
554
555 #[cfg(feature = "std")]
557 pub const DEFAULT: TimeZoneSettings<'static> = TimeZoneSettings { directories: Self::DEFAULT_DIRECTORIES, read_file_fn: Self::DEFAULT_READ_FILE_FN };
558
559 pub const fn new(directories: &'a [&'a str], read_file_fn: ReadFileFn) -> TimeZoneSettings<'a> {
561 Self { directories, read_file_fn }
562 }
563
564 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 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 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 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 #[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}