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#[derive(Debug, Copy, Clone, Eq, PartialEq)]
26pub struct Transition {
27 unix_leap_time: i64,
29 local_time_type_index: usize,
31}
32
33impl Transition {
34 #[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 #[inline]
42 pub const fn unix_leap_time(&self) -> i64 {
43 self.unix_leap_time
44 }
45
46 #[inline]
48 pub const fn local_time_type_index(&self) -> usize {
49 self.local_time_type_index
50 }
51}
52
53#[derive(Debug, Copy, Clone, Eq, PartialEq)]
55pub struct LeapSecond {
56 unix_leap_time: i64,
58 correction: i32,
60}
61
62impl LeapSecond {
63 #[inline]
65 pub const fn new(unix_leap_time: i64, correction: i32) -> Self {
66 Self { unix_leap_time, correction }
67 }
68
69 #[inline]
71 pub const fn unix_leap_time(&self) -> i64 {
72 self.unix_leap_time
73 }
74
75 #[inline]
77 pub const fn correction(&self) -> i32 {
78 self.correction
79 }
80}
81
82#[derive(Copy, Clone, Eq, PartialEq)]
87struct TzAsciiStr {
88 bytes: [u8; 8],
90}
91
92impl TzAsciiStr {
93 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 #[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 #[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 #[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#[derive(Debug, Copy, Clone, Eq, PartialEq)]
159pub struct LocalTimeType {
160 ut_offset: i32,
162 is_dst: bool,
164 time_zone_designation: Option<TzAsciiStr>,
166}
167
168impl LocalTimeType {
169 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 #[inline]
188 pub const fn utc() -> Self {
189 Self { ut_offset: 0, is_dst: false, time_zone_designation: None }
190 }
191
192 #[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 #[inline]
204 pub const fn ut_offset(&self) -> i32 {
205 self.ut_offset
206 }
207
208 #[inline]
210 pub const fn is_dst(&self) -> bool {
211 self.is_dst
212 }
213
214 #[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 #[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#[derive(Debug, Copy, Clone, Eq, PartialEq)]
238pub struct TimeZoneRef<'a> {
239 transitions: &'a [Transition],
241 local_time_types: &'a [LocalTimeType],
243 leap_seconds: &'a [LeapSecond],
245 extra_rule: &'a Option<TransitionRule>,
247}
248
249impl<'a> TimeZoneRef<'a> {
250 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 #[inline]
268 pub const fn utc() -> Self {
269 Self { transitions: &[], local_time_types: &[const { LocalTimeType::utc() }], leap_seconds: &[], extra_rule: &None }
270 }
271
272 #[inline]
274 pub const fn transitions(&self) -> &'a [Transition] {
275 self.transitions
276 }
277
278 #[inline]
280 pub const fn local_time_types(&self) -> &'a [LocalTimeType] {
281 self.local_time_types
282 }
283
284 #[inline]
286 pub const fn leap_seconds(&self) -> &'a [LeapSecond] {
287 self.leap_seconds
288 }
289
290 #[inline]
292 pub const fn extra_rule(&self) -> &'a Option<TransitionRule> {
293 self.extra_rule
294 }
295
296 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 #[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 const fn check_inputs(&self) -> Result<(), TzError> {
342 use crate::constants::*;
343
344 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 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 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 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 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 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#[cfg(feature = "alloc")]
454#[derive(Debug, Clone, Eq, PartialEq)]
455pub struct TimeZone {
456 transitions: Vec<Transition>,
458 local_time_types: Vec<LocalTimeType>,
460 leap_seconds: Vec<LeapSecond>,
462 extra_rule: Option<TransitionRule>,
464}
465
466#[cfg(feature = "alloc")]
467impl TimeZone {
468 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 #[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 #[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 #[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 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 pub fn from_tz_data(bytes: &[u8]) -> Result<Self, TzError> {
504 parse_tz_file(bytes)
505 }
506
507 #[cfg(feature = "std")]
512 pub fn local() -> Result<Self, crate::Error> {
513 TimeZoneSettings::DEFAULT.parse_local()
514 }
515
516 #[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 #[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#[cfg(feature = "alloc")]
531type ReadFileFn = fn(path: &str) -> Result<Vec<u8>, Box<dyn core::error::Error + Send + Sync + 'static>>;
532
533#[cfg(feature = "alloc")]
535#[derive(Debug)]
536pub struct TimeZoneSettings<'a> {
537 directories: &'a [&'a str],
539 read_file_fn: ReadFileFn,
541}
542
543#[cfg(feature = "alloc")]
544impl<'a> TimeZoneSettings<'a> {
545 pub const DEFAULT_DIRECTORIES: &'static [&'static str] = &["/usr/share/zoneinfo", "/share/zoneinfo", "/etc/zoneinfo"];
547
548 #[cfg(feature = "std")]
550 pub const DEFAULT_READ_FILE_FN: ReadFileFn = |path| Ok(std::fs::read(path)?);
551
552 #[cfg(feature = "std")]
554 pub const DEFAULT: TimeZoneSettings<'static> = TimeZoneSettings { directories: Self::DEFAULT_DIRECTORIES, read_file_fn: Self::DEFAULT_READ_FILE_FN };
555
556 pub const fn new(directories: &'a [&'a str], read_file_fn: ReadFileFn) -> TimeZoneSettings<'a> {
558 Self { directories, read_file_fn }
559 }
560
561 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 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 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 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 #[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}