1 use std::cmp::Ordering;
2 
3 use super::parser::Cursor;
4 use super::timezone::{LocalTimeType, SECONDS_PER_WEEK};
5 use super::{
6     Error, CUMUL_DAY_IN_MONTHS_NORMAL_YEAR, DAYS_PER_WEEK, DAY_IN_MONTHS_NORMAL_YEAR,
7     SECONDS_PER_DAY,
8 };
9 
10 /// Transition rule
11 #[derive(Debug, Copy, Clone, Eq, PartialEq)]
12 pub(super) enum TransitionRule {
13     /// Fixed local time type
14     Fixed(LocalTimeType),
15     /// Alternate local time types
16     Alternate(AlternateTime),
17 }
18 
19 impl TransitionRule {
20     /// Parse a POSIX TZ string containing a time zone description, as described in [the POSIX documentation of the `TZ` environment variable](https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html).
21     ///
22     /// TZ string extensions from [RFC 8536](https://datatracker.ietf.org/doc/html/rfc8536#section-3.3.1) may be used.
23     ///
from_tz_string( tz_string: &[u8], use_string_extensions: bool, ) -> Result<Self, Error>24     pub(super) fn from_tz_string(
25         tz_string: &[u8],
26         use_string_extensions: bool,
27     ) -> Result<Self, Error> {
28         let mut cursor = Cursor::new(tz_string);
29 
30         let std_time_zone = Some(parse_name(&mut cursor)?);
31         let std_offset = parse_offset(&mut cursor)?;
32 
33         if cursor.is_empty() {
34             return Ok(LocalTimeType::new(-std_offset, false, std_time_zone)?.into());
35         }
36 
37         let dst_time_zone = Some(parse_name(&mut cursor)?);
38 
39         let dst_offset = match cursor.peek() {
40             Some(&b',') => std_offset - 3600,
41             Some(_) => parse_offset(&mut cursor)?,
42             None => {
43                 return Err(Error::UnsupportedTzString("DST start and end rules must be provided"))
44             }
45         };
46 
47         if cursor.is_empty() {
48             return Err(Error::UnsupportedTzString("DST start and end rules must be provided"));
49         }
50 
51         cursor.read_tag(b",")?;
52         let (dst_start, dst_start_time) = RuleDay::parse(&mut cursor, use_string_extensions)?;
53 
54         cursor.read_tag(b",")?;
55         let (dst_end, dst_end_time) = RuleDay::parse(&mut cursor, use_string_extensions)?;
56 
57         if !cursor.is_empty() {
58             return Err(Error::InvalidTzString("remaining data after parsing TZ string"));
59         }
60 
61         Ok(AlternateTime::new(
62             LocalTimeType::new(-std_offset, false, std_time_zone)?,
63             LocalTimeType::new(-dst_offset, true, dst_time_zone)?,
64             dst_start,
65             dst_start_time,
66             dst_end,
67             dst_end_time,
68         )?
69         .into())
70     }
71 
72     /// Find the local time type associated to the transition rule at the specified Unix time in seconds
find_local_time_type(&self, unix_time: i64) -> Result<&LocalTimeType, Error>73     pub(super) fn find_local_time_type(&self, unix_time: i64) -> Result<&LocalTimeType, Error> {
74         match self {
75             TransitionRule::Fixed(local_time_type) => Ok(local_time_type),
76             TransitionRule::Alternate(alternate_time) => {
77                 alternate_time.find_local_time_type(unix_time)
78             }
79         }
80     }
81 
82     /// Find the local time type associated to the transition rule at the specified Unix time in seconds
find_local_time_type_from_local( &self, local_time: i64, year: i32, ) -> Result<crate::LocalResult<LocalTimeType>, Error>83     pub(super) fn find_local_time_type_from_local(
84         &self,
85         local_time: i64,
86         year: i32,
87     ) -> Result<crate::LocalResult<LocalTimeType>, Error> {
88         match self {
89             TransitionRule::Fixed(local_time_type) => {
90                 Ok(crate::LocalResult::Single(*local_time_type))
91             }
92             TransitionRule::Alternate(alternate_time) => {
93                 alternate_time.find_local_time_type_from_local(local_time, year)
94             }
95         }
96     }
97 }
98 
99 impl From<LocalTimeType> for TransitionRule {
from(inner: LocalTimeType) -> Self100     fn from(inner: LocalTimeType) -> Self {
101         TransitionRule::Fixed(inner)
102     }
103 }
104 
105 impl From<AlternateTime> for TransitionRule {
from(inner: AlternateTime) -> Self106     fn from(inner: AlternateTime) -> Self {
107         TransitionRule::Alternate(inner)
108     }
109 }
110 
111 /// Transition rule representing alternate local time types
112 #[derive(Debug, Copy, Clone, Eq, PartialEq)]
113 pub(super) struct AlternateTime {
114     /// Local time type for standard time
115     pub(super) std: LocalTimeType,
116     /// Local time type for Daylight Saving Time
117     pub(super) dst: LocalTimeType,
118     /// Start day of Daylight Saving Time
119     dst_start: RuleDay,
120     /// Local start day time of Daylight Saving Time, in seconds
121     dst_start_time: i32,
122     /// End day of Daylight Saving Time
123     dst_end: RuleDay,
124     /// Local end day time of Daylight Saving Time, in seconds
125     dst_end_time: i32,
126 }
127 
128 impl AlternateTime {
129     /// Construct a transition rule representing alternate local time types
new( std: LocalTimeType, dst: LocalTimeType, dst_start: RuleDay, dst_start_time: i32, dst_end: RuleDay, dst_end_time: i32, ) -> Result<Self, Error>130     const fn new(
131         std: LocalTimeType,
132         dst: LocalTimeType,
133         dst_start: RuleDay,
134         dst_start_time: i32,
135         dst_end: RuleDay,
136         dst_end_time: i32,
137     ) -> Result<Self, Error> {
138         // Overflow is not possible
139         if !((dst_start_time as i64).abs() < SECONDS_PER_WEEK
140             && (dst_end_time as i64).abs() < SECONDS_PER_WEEK)
141         {
142             return Err(Error::TransitionRule("invalid DST start or end time"));
143         }
144 
145         Ok(Self { std, dst, dst_start, dst_start_time, dst_end, dst_end_time })
146     }
147 
148     /// Find the local time type associated to the alternate transition rule at the specified Unix time in seconds
find_local_time_type(&self, unix_time: i64) -> Result<&LocalTimeType, Error>149     fn find_local_time_type(&self, unix_time: i64) -> Result<&LocalTimeType, Error> {
150         // Overflow is not possible
151         let dst_start_time_in_utc = self.dst_start_time as i64 - self.std.ut_offset as i64;
152         let dst_end_time_in_utc = self.dst_end_time as i64 - self.dst.ut_offset as i64;
153 
154         let current_year = match UtcDateTime::from_timespec(unix_time) {
155             Ok(dt) => dt.year,
156             Err(error) => return Err(error),
157         };
158 
159         // Check if the current year is valid for the following computations
160         if !(i32::min_value() + 2 <= current_year && current_year <= i32::max_value() - 2) {
161             return Err(Error::OutOfRange("out of range date time"));
162         }
163 
164         let current_year_dst_start_unix_time =
165             self.dst_start.unix_time(current_year, dst_start_time_in_utc);
166         let current_year_dst_end_unix_time =
167             self.dst_end.unix_time(current_year, dst_end_time_in_utc);
168 
169         // Check DST start/end Unix times for previous/current/next years to support for transition day times outside of [0h, 24h] range
170         let is_dst =
171             match Ord::cmp(&current_year_dst_start_unix_time, &current_year_dst_end_unix_time) {
172                 Ordering::Less | Ordering::Equal => {
173                     if unix_time < current_year_dst_start_unix_time {
174                         let previous_year_dst_end_unix_time =
175                             self.dst_end.unix_time(current_year - 1, dst_end_time_in_utc);
176                         if unix_time < previous_year_dst_end_unix_time {
177                             let previous_year_dst_start_unix_time =
178                                 self.dst_start.unix_time(current_year - 1, dst_start_time_in_utc);
179                             previous_year_dst_start_unix_time <= unix_time
180                         } else {
181                             false
182                         }
183                     } else if unix_time < current_year_dst_end_unix_time {
184                         true
185                     } else {
186                         let next_year_dst_start_unix_time =
187                             self.dst_start.unix_time(current_year + 1, dst_start_time_in_utc);
188                         if next_year_dst_start_unix_time <= unix_time {
189                             let next_year_dst_end_unix_time =
190                                 self.dst_end.unix_time(current_year + 1, dst_end_time_in_utc);
191                             unix_time < next_year_dst_end_unix_time
192                         } else {
193                             false
194                         }
195                     }
196                 }
197                 Ordering::Greater => {
198                     if unix_time < current_year_dst_end_unix_time {
199                         let previous_year_dst_start_unix_time =
200                             self.dst_start.unix_time(current_year - 1, dst_start_time_in_utc);
201                         if unix_time < previous_year_dst_start_unix_time {
202                             let previous_year_dst_end_unix_time =
203                                 self.dst_end.unix_time(current_year - 1, dst_end_time_in_utc);
204                             unix_time < previous_year_dst_end_unix_time
205                         } else {
206                             true
207                         }
208                     } else if unix_time < current_year_dst_start_unix_time {
209                         false
210                     } else {
211                         let next_year_dst_end_unix_time =
212                             self.dst_end.unix_time(current_year + 1, dst_end_time_in_utc);
213                         if next_year_dst_end_unix_time <= unix_time {
214                             let next_year_dst_start_unix_time =
215                                 self.dst_start.unix_time(current_year + 1, dst_start_time_in_utc);
216                             next_year_dst_start_unix_time <= unix_time
217                         } else {
218                             true
219                         }
220                     }
221                 }
222             };
223 
224         if is_dst {
225             Ok(&self.dst)
226         } else {
227             Ok(&self.std)
228         }
229     }
230 
find_local_time_type_from_local( &self, local_time: i64, current_year: i32, ) -> Result<crate::LocalResult<LocalTimeType>, Error>231     fn find_local_time_type_from_local(
232         &self,
233         local_time: i64,
234         current_year: i32,
235     ) -> Result<crate::LocalResult<LocalTimeType>, Error> {
236         // Check if the current year is valid for the following computations
237         if !(i32::min_value() + 2 <= current_year && current_year <= i32::max_value() - 2) {
238             return Err(Error::OutOfRange("out of range date time"));
239         }
240 
241         let dst_start_transition_start =
242             self.dst_start.unix_time(current_year, 0) + i64::from(self.dst_start_time);
243         let dst_start_transition_end = self.dst_start.unix_time(current_year, 0)
244             + i64::from(self.dst_start_time)
245             + i64::from(self.dst.ut_offset)
246             - i64::from(self.std.ut_offset);
247 
248         let dst_end_transition_start =
249             self.dst_end.unix_time(current_year, 0) + i64::from(self.dst_end_time);
250         let dst_end_transition_end = self.dst_end.unix_time(current_year, 0)
251             + i64::from(self.dst_end_time)
252             + i64::from(self.std.ut_offset)
253             - i64::from(self.dst.ut_offset);
254 
255         match self.std.ut_offset.cmp(&self.dst.ut_offset) {
256             Ordering::Equal => Ok(crate::LocalResult::Single(self.std)),
257             Ordering::Less => {
258                 if self.dst_start.transition_date(current_year).0
259                     < self.dst_end.transition_date(current_year).0
260                 {
261                     // northern hemisphere
262                     // For the DST END transition, the `start` happens at a later timestamp than the `end`.
263                     if local_time <= dst_start_transition_start {
264                         Ok(crate::LocalResult::Single(self.std))
265                     } else if local_time > dst_start_transition_start
266                         && local_time < dst_start_transition_end
267                     {
268                         Ok(crate::LocalResult::None)
269                     } else if local_time >= dst_start_transition_end
270                         && local_time < dst_end_transition_end
271                     {
272                         Ok(crate::LocalResult::Single(self.dst))
273                     } else if local_time >= dst_end_transition_end
274                         && local_time <= dst_end_transition_start
275                     {
276                         Ok(crate::LocalResult::Ambiguous(self.std, self.dst))
277                     } else {
278                         Ok(crate::LocalResult::Single(self.std))
279                     }
280                 } else {
281                     // southern hemisphere regular DST
282                     // For the DST END transition, the `start` happens at a later timestamp than the `end`.
283                     if local_time < dst_end_transition_end {
284                         Ok(crate::LocalResult::Single(self.dst))
285                     } else if local_time >= dst_end_transition_end
286                         && local_time <= dst_end_transition_start
287                     {
288                         Ok(crate::LocalResult::Ambiguous(self.std, self.dst))
289                     } else if local_time > dst_end_transition_end
290                         && local_time < dst_start_transition_start
291                     {
292                         Ok(crate::LocalResult::Single(self.std))
293                     } else if local_time >= dst_start_transition_start
294                         && local_time < dst_start_transition_end
295                     {
296                         Ok(crate::LocalResult::None)
297                     } else {
298                         Ok(crate::LocalResult::Single(self.dst))
299                     }
300                 }
301             }
302             Ordering::Greater => {
303                 if self.dst_start.transition_date(current_year).0
304                     < self.dst_end.transition_date(current_year).0
305                 {
306                     // southern hemisphere reverse DST
307                     // For the DST END transition, the `start` happens at a later timestamp than the `end`.
308                     if local_time < dst_start_transition_end {
309                         Ok(crate::LocalResult::Single(self.std))
310                     } else if local_time >= dst_start_transition_end
311                         && local_time <= dst_start_transition_start
312                     {
313                         Ok(crate::LocalResult::Ambiguous(self.dst, self.std))
314                     } else if local_time > dst_start_transition_start
315                         && local_time < dst_end_transition_start
316                     {
317                         Ok(crate::LocalResult::Single(self.dst))
318                     } else if local_time >= dst_end_transition_start
319                         && local_time < dst_end_transition_end
320                     {
321                         Ok(crate::LocalResult::None)
322                     } else {
323                         Ok(crate::LocalResult::Single(self.std))
324                     }
325                 } else {
326                     // northern hemisphere reverse DST
327                     // For the DST END transition, the `start` happens at a later timestamp than the `end`.
328                     if local_time <= dst_end_transition_start {
329                         Ok(crate::LocalResult::Single(self.dst))
330                     } else if local_time > dst_end_transition_start
331                         && local_time < dst_end_transition_end
332                     {
333                         Ok(crate::LocalResult::None)
334                     } else if local_time >= dst_end_transition_end
335                         && local_time < dst_start_transition_end
336                     {
337                         Ok(crate::LocalResult::Single(self.std))
338                     } else if local_time >= dst_start_transition_end
339                         && local_time <= dst_start_transition_start
340                     {
341                         Ok(crate::LocalResult::Ambiguous(self.dst, self.std))
342                     } else {
343                         Ok(crate::LocalResult::Single(self.dst))
344                     }
345                 }
346             }
347         }
348     }
349 }
350 
351 /// Parse time zone name
parse_name<'a>(cursor: &mut Cursor<'a>) -> Result<&'a [u8], Error>352 fn parse_name<'a>(cursor: &mut Cursor<'a>) -> Result<&'a [u8], Error> {
353     match cursor.peek() {
354         Some(b'<') => {}
355         _ => return Ok(cursor.read_while(u8::is_ascii_alphabetic)?),
356     }
357 
358     cursor.read_exact(1)?;
359     let unquoted = cursor.read_until(|&x| x == b'>')?;
360     cursor.read_exact(1)?;
361     Ok(unquoted)
362 }
363 
364 /// Parse time zone offset
parse_offset(cursor: &mut Cursor) -> Result<i32, Error>365 fn parse_offset(cursor: &mut Cursor) -> Result<i32, Error> {
366     let (sign, hour, minute, second) = parse_signed_hhmmss(cursor)?;
367 
368     if !(0..=24).contains(&hour) {
369         return Err(Error::InvalidTzString("invalid offset hour"));
370     }
371     if !(0..=59).contains(&minute) {
372         return Err(Error::InvalidTzString("invalid offset minute"));
373     }
374     if !(0..=59).contains(&second) {
375         return Err(Error::InvalidTzString("invalid offset second"));
376     }
377 
378     Ok(sign * (hour * 3600 + minute * 60 + second))
379 }
380 
381 /// Parse transition rule time
parse_rule_time(cursor: &mut Cursor) -> Result<i32, Error>382 fn parse_rule_time(cursor: &mut Cursor) -> Result<i32, Error> {
383     let (hour, minute, second) = parse_hhmmss(cursor)?;
384 
385     if !(0..=24).contains(&hour) {
386         return Err(Error::InvalidTzString("invalid day time hour"));
387     }
388     if !(0..=59).contains(&minute) {
389         return Err(Error::InvalidTzString("invalid day time minute"));
390     }
391     if !(0..=59).contains(&second) {
392         return Err(Error::InvalidTzString("invalid day time second"));
393     }
394 
395     Ok(hour * 3600 + minute * 60 + second)
396 }
397 
398 /// Parse transition rule time with TZ string extensions
parse_rule_time_extended(cursor: &mut Cursor) -> Result<i32, Error>399 fn parse_rule_time_extended(cursor: &mut Cursor) -> Result<i32, Error> {
400     let (sign, hour, minute, second) = parse_signed_hhmmss(cursor)?;
401 
402     if !(-167..=167).contains(&hour) {
403         return Err(Error::InvalidTzString("invalid day time hour"));
404     }
405     if !(0..=59).contains(&minute) {
406         return Err(Error::InvalidTzString("invalid day time minute"));
407     }
408     if !(0..=59).contains(&second) {
409         return Err(Error::InvalidTzString("invalid day time second"));
410     }
411 
412     Ok(sign * (hour * 3600 + minute * 60 + second))
413 }
414 
415 /// Parse hours, minutes and seconds
parse_hhmmss(cursor: &mut Cursor) -> Result<(i32, i32, i32), Error>416 fn parse_hhmmss(cursor: &mut Cursor) -> Result<(i32, i32, i32), Error> {
417     let hour = cursor.read_int()?;
418 
419     let mut minute = 0;
420     let mut second = 0;
421 
422     if cursor.read_optional_tag(b":")? {
423         minute = cursor.read_int()?;
424 
425         if cursor.read_optional_tag(b":")? {
426             second = cursor.read_int()?;
427         }
428     }
429 
430     Ok((hour, minute, second))
431 }
432 
433 /// Parse signed hours, minutes and seconds
parse_signed_hhmmss(cursor: &mut Cursor) -> Result<(i32, i32, i32, i32), Error>434 fn parse_signed_hhmmss(cursor: &mut Cursor) -> Result<(i32, i32, i32, i32), Error> {
435     let mut sign = 1;
436     if let Some(&c) = cursor.peek() {
437         if c == b'+' || c == b'-' {
438             cursor.read_exact(1)?;
439             if c == b'-' {
440                 sign = -1;
441             }
442         }
443     }
444 
445     let (hour, minute, second) = parse_hhmmss(cursor)?;
446     Ok((sign, hour, minute, second))
447 }
448 
449 /// Transition rule day
450 #[derive(Debug, Copy, Clone, Eq, PartialEq)]
451 enum RuleDay {
452     /// Julian day in `[1, 365]`, without taking occasional Feb 29 into account, which is not referenceable
453     Julian1WithoutLeap(u16),
454     /// Zero-based Julian day in `[0, 365]`, taking occasional Feb 29 into account
455     Julian0WithLeap(u16),
456     /// Day represented by a month, a month week and a week day
457     MonthWeekday {
458         /// Month in `[1, 12]`
459         month: u8,
460         /// Week of the month in `[1, 5]`, with `5` representing the last week of the month
461         week: u8,
462         /// Day of the week in `[0, 6]` from Sunday
463         week_day: u8,
464     },
465 }
466 
467 impl RuleDay {
468     /// Parse transition rule
parse(cursor: &mut Cursor, use_string_extensions: bool) -> Result<(Self, i32), Error>469     fn parse(cursor: &mut Cursor, use_string_extensions: bool) -> Result<(Self, i32), Error> {
470         let date = match cursor.peek() {
471             Some(b'M') => {
472                 cursor.read_exact(1)?;
473                 let month = cursor.read_int()?;
474                 cursor.read_tag(b".")?;
475                 let week = cursor.read_int()?;
476                 cursor.read_tag(b".")?;
477                 let week_day = cursor.read_int()?;
478                 RuleDay::month_weekday(month, week, week_day)?
479             }
480             Some(b'J') => {
481                 cursor.read_exact(1)?;
482                 RuleDay::julian_1(cursor.read_int()?)?
483             }
484             _ => RuleDay::julian_0(cursor.read_int()?)?,
485         };
486 
487         Ok((
488             date,
489             match (cursor.read_optional_tag(b"/")?, use_string_extensions) {
490                 (false, _) => 2 * 3600,
491                 (true, true) => parse_rule_time_extended(cursor)?,
492                 (true, false) => parse_rule_time(cursor)?,
493             },
494         ))
495     }
496 
497     /// Construct a transition rule day represented by a Julian day in `[1, 365]`, without taking occasional Feb 29 into account, which is not referenceable
julian_1(julian_day_1: u16) -> Result<Self, Error>498     fn julian_1(julian_day_1: u16) -> Result<Self, Error> {
499         if !(1..=365).contains(&julian_day_1) {
500             return Err(Error::TransitionRule("invalid rule day julian day"));
501         }
502 
503         Ok(RuleDay::Julian1WithoutLeap(julian_day_1))
504     }
505 
506     /// Construct a transition rule day represented by a zero-based Julian day in `[0, 365]`, taking occasional Feb 29 into account
julian_0(julian_day_0: u16) -> Result<Self, Error>507     const fn julian_0(julian_day_0: u16) -> Result<Self, Error> {
508         if julian_day_0 > 365 {
509             return Err(Error::TransitionRule("invalid rule day julian day"));
510         }
511 
512         Ok(RuleDay::Julian0WithLeap(julian_day_0))
513     }
514 
515     /// Construct a transition rule day represented by a month, a month week and a week day
month_weekday(month: u8, week: u8, week_day: u8) -> Result<Self, Error>516     fn month_weekday(month: u8, week: u8, week_day: u8) -> Result<Self, Error> {
517         if !(1..=12).contains(&month) {
518             return Err(Error::TransitionRule("invalid rule day month"));
519         }
520 
521         if !(1..=5).contains(&week) {
522             return Err(Error::TransitionRule("invalid rule day week"));
523         }
524 
525         if week_day > 6 {
526             return Err(Error::TransitionRule("invalid rule day week day"));
527         }
528 
529         Ok(RuleDay::MonthWeekday { month, week, week_day })
530     }
531 
532     /// Get the transition date for the provided year
533     ///
534     /// ## Outputs
535     ///
536     /// * `month`: Month in `[1, 12]`
537     /// * `month_day`: Day of the month in `[1, 31]`
transition_date(&self, year: i32) -> (usize, i64)538     fn transition_date(&self, year: i32) -> (usize, i64) {
539         match *self {
540             RuleDay::Julian1WithoutLeap(year_day) => {
541                 let year_day = year_day as i64;
542 
543                 let month = match CUMUL_DAY_IN_MONTHS_NORMAL_YEAR.binary_search(&(year_day - 1)) {
544                     Ok(x) => x + 1,
545                     Err(x) => x,
546                 };
547 
548                 let month_day = year_day - CUMUL_DAY_IN_MONTHS_NORMAL_YEAR[month - 1];
549 
550                 (month, month_day)
551             }
552             RuleDay::Julian0WithLeap(year_day) => {
553                 let leap = is_leap_year(year) as i64;
554 
555                 let cumul_day_in_months = [
556                     0,
557                     31,
558                     59 + leap,
559                     90 + leap,
560                     120 + leap,
561                     151 + leap,
562                     181 + leap,
563                     212 + leap,
564                     243 + leap,
565                     273 + leap,
566                     304 + leap,
567                     334 + leap,
568                 ];
569 
570                 let year_day = year_day as i64;
571 
572                 let month = match cumul_day_in_months.binary_search(&year_day) {
573                     Ok(x) => x + 1,
574                     Err(x) => x,
575                 };
576 
577                 let month_day = 1 + year_day - cumul_day_in_months[month - 1];
578 
579                 (month, month_day)
580             }
581             RuleDay::MonthWeekday { month: rule_month, week, week_day } => {
582                 let leap = is_leap_year(year) as i64;
583 
584                 let month = rule_month as usize;
585 
586                 let mut day_in_month = DAY_IN_MONTHS_NORMAL_YEAR[month - 1];
587                 if month == 2 {
588                     day_in_month += leap;
589                 }
590 
591                 let week_day_of_first_month_day =
592                     (4 + days_since_unix_epoch(year, month, 1)).rem_euclid(DAYS_PER_WEEK);
593                 let first_week_day_occurence_in_month =
594                     1 + (week_day as i64 - week_day_of_first_month_day).rem_euclid(DAYS_PER_WEEK);
595 
596                 let mut month_day =
597                     first_week_day_occurence_in_month + (week as i64 - 1) * DAYS_PER_WEEK;
598                 if month_day > day_in_month {
599                     month_day -= DAYS_PER_WEEK
600                 }
601 
602                 (month, month_day)
603             }
604         }
605     }
606 
607     /// Returns the UTC Unix time in seconds associated to the transition date for the provided year
unix_time(&self, year: i32, day_time_in_utc: i64) -> i64608     fn unix_time(&self, year: i32, day_time_in_utc: i64) -> i64 {
609         let (month, month_day) = self.transition_date(year);
610         days_since_unix_epoch(year, month, month_day) * SECONDS_PER_DAY + day_time_in_utc
611     }
612 }
613 
614 /// UTC date time exprimed in the [proleptic gregorian calendar](https://en.wikipedia.org/wiki/Proleptic_Gregorian_calendar)
615 #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
616 pub(crate) struct UtcDateTime {
617     /// Year
618     pub(crate) year: i32,
619     /// Month in `[1, 12]`
620     pub(crate) month: u8,
621     /// Day of the month in `[1, 31]`
622     pub(crate) month_day: u8,
623     /// Hours since midnight in `[0, 23]`
624     pub(crate) hour: u8,
625     /// Minutes in `[0, 59]`
626     pub(crate) minute: u8,
627     /// Seconds in `[0, 60]`, with a possible leap second
628     pub(crate) second: u8,
629 }
630 
631 impl UtcDateTime {
632     /// Construct a UTC date time from a Unix time in seconds and nanoseconds
from_timespec(unix_time: i64) -> Result<Self, Error>633     pub(crate) fn from_timespec(unix_time: i64) -> Result<Self, Error> {
634         let seconds = match unix_time.checked_sub(UNIX_OFFSET_SECS) {
635             Some(seconds) => seconds,
636             None => return Err(Error::OutOfRange("out of range operation")),
637         };
638 
639         let mut remaining_days = seconds / SECONDS_PER_DAY;
640         let mut remaining_seconds = seconds % SECONDS_PER_DAY;
641         if remaining_seconds < 0 {
642             remaining_seconds += SECONDS_PER_DAY;
643             remaining_days -= 1;
644         }
645 
646         let mut cycles_400_years = remaining_days / DAYS_PER_400_YEARS;
647         remaining_days %= DAYS_PER_400_YEARS;
648         if remaining_days < 0 {
649             remaining_days += DAYS_PER_400_YEARS;
650             cycles_400_years -= 1;
651         }
652 
653         let cycles_100_years = Ord::min(remaining_days / DAYS_PER_100_YEARS, 3);
654         remaining_days -= cycles_100_years * DAYS_PER_100_YEARS;
655 
656         let cycles_4_years = Ord::min(remaining_days / DAYS_PER_4_YEARS, 24);
657         remaining_days -= cycles_4_years * DAYS_PER_4_YEARS;
658 
659         let remaining_years = Ord::min(remaining_days / DAYS_PER_NORMAL_YEAR, 3);
660         remaining_days -= remaining_years * DAYS_PER_NORMAL_YEAR;
661 
662         let mut year = OFFSET_YEAR
663             + remaining_years
664             + cycles_4_years * 4
665             + cycles_100_years * 100
666             + cycles_400_years * 400;
667 
668         let mut month = 0;
669         while month < DAY_IN_MONTHS_LEAP_YEAR_FROM_MARCH.len() {
670             let days = DAY_IN_MONTHS_LEAP_YEAR_FROM_MARCH[month];
671             if remaining_days < days {
672                 break;
673             }
674             remaining_days -= days;
675             month += 1;
676         }
677         month += 2;
678 
679         if month >= MONTHS_PER_YEAR as usize {
680             month -= MONTHS_PER_YEAR as usize;
681             year += 1;
682         }
683         month += 1;
684 
685         let month_day = 1 + remaining_days;
686 
687         let hour = remaining_seconds / SECONDS_PER_HOUR;
688         let minute = (remaining_seconds / SECONDS_PER_MINUTE) % MINUTES_PER_HOUR;
689         let second = remaining_seconds % SECONDS_PER_MINUTE;
690 
691         let year = match year >= i32::min_value() as i64 && year <= i32::max_value() as i64 {
692             true => year as i32,
693             false => return Err(Error::OutOfRange("i64 is out of range for i32")),
694         };
695 
696         Ok(Self {
697             year,
698             month: month as u8,
699             month_day: month_day as u8,
700             hour: hour as u8,
701             minute: minute as u8,
702             second: second as u8,
703         })
704     }
705 }
706 
707 /// Number of nanoseconds in one second
708 const NANOSECONDS_PER_SECOND: u32 = 1_000_000_000;
709 /// Number of seconds in one minute
710 const SECONDS_PER_MINUTE: i64 = 60;
711 /// Number of seconds in one hour
712 const SECONDS_PER_HOUR: i64 = 3600;
713 /// Number of minutes in one hour
714 const MINUTES_PER_HOUR: i64 = 60;
715 /// Number of months in one year
716 const MONTHS_PER_YEAR: i64 = 12;
717 /// Number of days in a normal year
718 const DAYS_PER_NORMAL_YEAR: i64 = 365;
719 /// Number of days in 4 years (including 1 leap year)
720 const DAYS_PER_4_YEARS: i64 = DAYS_PER_NORMAL_YEAR * 4 + 1;
721 /// Number of days in 100 years (including 24 leap years)
722 const DAYS_PER_100_YEARS: i64 = DAYS_PER_NORMAL_YEAR * 100 + 24;
723 /// Number of days in 400 years (including 97 leap years)
724 const DAYS_PER_400_YEARS: i64 = DAYS_PER_NORMAL_YEAR * 400 + 97;
725 /// Unix time at `2000-03-01T00:00:00Z` (Wednesday)
726 const UNIX_OFFSET_SECS: i64 = 951868800;
727 /// Offset year
728 const OFFSET_YEAR: i64 = 2000;
729 /// Month days in a leap year from March
730 const DAY_IN_MONTHS_LEAP_YEAR_FROM_MARCH: [i64; 12] =
731     [31, 30, 31, 30, 31, 31, 30, 31, 30, 31, 31, 29];
732 
733 /// Compute the number of days since Unix epoch (`1970-01-01T00:00:00Z`).
734 ///
735 /// ## Inputs
736 ///
737 /// * `year`: Year
738 /// * `month`: Month in `[1, 12]`
739 /// * `month_day`: Day of the month in `[1, 31]`
days_since_unix_epoch(year: i32, month: usize, month_day: i64) -> i64740 pub(crate) const fn days_since_unix_epoch(year: i32, month: usize, month_day: i64) -> i64 {
741     let is_leap_year = is_leap_year(year);
742 
743     let year = year as i64;
744 
745     let mut result = (year - 1970) * 365;
746 
747     if year >= 1970 {
748         result += (year - 1968) / 4;
749         result -= (year - 1900) / 100;
750         result += (year - 1600) / 400;
751 
752         if is_leap_year && month < 3 {
753             result -= 1;
754         }
755     } else {
756         result += (year - 1972) / 4;
757         result -= (year - 2000) / 100;
758         result += (year - 2000) / 400;
759 
760         if is_leap_year && month >= 3 {
761             result += 1;
762         }
763     }
764 
765     result += CUMUL_DAY_IN_MONTHS_NORMAL_YEAR[month - 1] + month_day - 1;
766 
767     result
768 }
769 
770 /// Check if a year is a leap year
is_leap_year(year: i32) -> bool771 pub(crate) const fn is_leap_year(year: i32) -> bool {
772     year % 400 == 0 || (year % 4 == 0 && year % 100 != 0)
773 }
774 
775 #[cfg(test)]
776 mod tests {
777     use super::super::timezone::Transition;
778     use super::super::{Error, TimeZone};
779     use super::{AlternateTime, LocalTimeType, RuleDay, TransitionRule};
780 
781     #[test]
test_quoted() -> Result<(), Error>782     fn test_quoted() -> Result<(), Error> {
783         let transition_rule = TransitionRule::from_tz_string(b"<-03>+3<+03>-3,J1,J365", false)?;
784         assert_eq!(
785             transition_rule,
786             AlternateTime::new(
787                 LocalTimeType::new(-10800, false, Some(b"-03"))?,
788                 LocalTimeType::new(10800, true, Some(b"+03"))?,
789                 RuleDay::julian_1(1)?,
790                 7200,
791                 RuleDay::julian_1(365)?,
792                 7200,
793             )?
794             .into()
795         );
796         Ok(())
797     }
798 
799     #[test]
test_full() -> Result<(), Error>800     fn test_full() -> Result<(), Error> {
801         let tz_string = b"NZST-12:00:00NZDT-13:00:00,M10.1.0/02:00:00,M3.3.0/02:00:00";
802         let transition_rule = TransitionRule::from_tz_string(tz_string, false)?;
803         assert_eq!(
804             transition_rule,
805             AlternateTime::new(
806                 LocalTimeType::new(43200, false, Some(b"NZST"))?,
807                 LocalTimeType::new(46800, true, Some(b"NZDT"))?,
808                 RuleDay::month_weekday(10, 1, 0)?,
809                 7200,
810                 RuleDay::month_weekday(3, 3, 0)?,
811                 7200,
812             )?
813             .into()
814         );
815         Ok(())
816     }
817 
818     #[test]
test_negative_dst() -> Result<(), Error>819     fn test_negative_dst() -> Result<(), Error> {
820         let tz_string = b"IST-1GMT0,M10.5.0,M3.5.0/1";
821         let transition_rule = TransitionRule::from_tz_string(tz_string, false)?;
822         assert_eq!(
823             transition_rule,
824             AlternateTime::new(
825                 LocalTimeType::new(3600, false, Some(b"IST"))?,
826                 LocalTimeType::new(0, true, Some(b"GMT"))?,
827                 RuleDay::month_weekday(10, 5, 0)?,
828                 7200,
829                 RuleDay::month_weekday(3, 5, 0)?,
830                 3600,
831             )?
832             .into()
833         );
834         Ok(())
835     }
836 
837     #[test]
test_negative_hour() -> Result<(), Error>838     fn test_negative_hour() -> Result<(), Error> {
839         let tz_string = b"<-03>3<-02>,M3.5.0/-2,M10.5.0/-1";
840         assert!(TransitionRule::from_tz_string(tz_string, false).is_err());
841 
842         assert_eq!(
843             TransitionRule::from_tz_string(tz_string, true)?,
844             AlternateTime::new(
845                 LocalTimeType::new(-10800, false, Some(b"-03"))?,
846                 LocalTimeType::new(-7200, true, Some(b"-02"))?,
847                 RuleDay::month_weekday(3, 5, 0)?,
848                 -7200,
849                 RuleDay::month_weekday(10, 5, 0)?,
850                 -3600,
851             )?
852             .into()
853         );
854         Ok(())
855     }
856 
857     #[test]
test_all_year_dst() -> Result<(), Error>858     fn test_all_year_dst() -> Result<(), Error> {
859         let tz_string = b"EST5EDT,0/0,J365/25";
860         assert!(TransitionRule::from_tz_string(tz_string, false).is_err());
861 
862         assert_eq!(
863             TransitionRule::from_tz_string(tz_string, true)?,
864             AlternateTime::new(
865                 LocalTimeType::new(-18000, false, Some(b"EST"))?,
866                 LocalTimeType::new(-14400, true, Some(b"EDT"))?,
867                 RuleDay::julian_0(0)?,
868                 0,
869                 RuleDay::julian_1(365)?,
870                 90000,
871             )?
872             .into()
873         );
874         Ok(())
875     }
876 
877     #[test]
test_v3_file() -> Result<(), Error>878     fn test_v3_file() -> Result<(), Error> {
879         let bytes = b"TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x01\0\0\0\x04\0\0\x1c\x20\0\0IST\0TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x01\0\0\0\x01\0\0\0\0\0\0\0\x01\0\0\0\x01\0\0\0\x04\0\0\0\0\x7f\xe8\x17\x80\0\0\0\x1c\x20\0\0IST\0\x01\x01\x0aIST-2IDT,M3.4.4/26,M10.5.0\x0a";
880 
881         let time_zone = TimeZone::from_tz_data(bytes)?;
882 
883         let time_zone_result = TimeZone::new(
884             vec![Transition::new(2145916800, 0)],
885             vec![LocalTimeType::new(7200, false, Some(b"IST"))?],
886             Vec::new(),
887             Some(TransitionRule::from(AlternateTime::new(
888                 LocalTimeType::new(7200, false, Some(b"IST"))?,
889                 LocalTimeType::new(10800, true, Some(b"IDT"))?,
890                 RuleDay::month_weekday(3, 4, 4)?,
891                 93600,
892                 RuleDay::month_weekday(10, 5, 0)?,
893                 7200,
894             )?)),
895         )?;
896 
897         assert_eq!(time_zone, time_zone_result);
898 
899         Ok(())
900     }
901 
902     #[test]
test_rule_day() -> Result<(), Error>903     fn test_rule_day() -> Result<(), Error> {
904         let rule_day_j1 = RuleDay::julian_1(60)?;
905         assert_eq!(rule_day_j1.transition_date(2000), (3, 1));
906         assert_eq!(rule_day_j1.transition_date(2001), (3, 1));
907         assert_eq!(rule_day_j1.unix_time(2000, 43200), 951912000);
908 
909         let rule_day_j0 = RuleDay::julian_0(59)?;
910         assert_eq!(rule_day_j0.transition_date(2000), (2, 29));
911         assert_eq!(rule_day_j0.transition_date(2001), (3, 1));
912         assert_eq!(rule_day_j0.unix_time(2000, 43200), 951825600);
913 
914         let rule_day_mwd = RuleDay::month_weekday(2, 5, 2)?;
915         assert_eq!(rule_day_mwd.transition_date(2000), (2, 29));
916         assert_eq!(rule_day_mwd.transition_date(2001), (2, 27));
917         assert_eq!(rule_day_mwd.unix_time(2000, 43200), 951825600);
918         assert_eq!(rule_day_mwd.unix_time(2001, 43200), 983275200);
919 
920         Ok(())
921     }
922 
923     #[test]
test_transition_rule() -> Result<(), Error>924     fn test_transition_rule() -> Result<(), Error> {
925         let transition_rule_fixed = TransitionRule::from(LocalTimeType::new(-36000, false, None)?);
926         assert_eq!(transition_rule_fixed.find_local_time_type(0)?.offset(), -36000);
927 
928         let transition_rule_dst = TransitionRule::from(AlternateTime::new(
929             LocalTimeType::new(43200, false, Some(b"NZST"))?,
930             LocalTimeType::new(46800, true, Some(b"NZDT"))?,
931             RuleDay::month_weekday(10, 1, 0)?,
932             7200,
933             RuleDay::month_weekday(3, 3, 0)?,
934             7200,
935         )?);
936 
937         assert_eq!(transition_rule_dst.find_local_time_type(953384399)?.offset(), 46800);
938         assert_eq!(transition_rule_dst.find_local_time_type(953384400)?.offset(), 43200);
939         assert_eq!(transition_rule_dst.find_local_time_type(970322399)?.offset(), 43200);
940         assert_eq!(transition_rule_dst.find_local_time_type(970322400)?.offset(), 46800);
941 
942         let transition_rule_negative_dst = TransitionRule::from(AlternateTime::new(
943             LocalTimeType::new(3600, false, Some(b"IST"))?,
944             LocalTimeType::new(0, true, Some(b"GMT"))?,
945             RuleDay::month_weekday(10, 5, 0)?,
946             7200,
947             RuleDay::month_weekday(3, 5, 0)?,
948             3600,
949         )?);
950 
951         assert_eq!(transition_rule_negative_dst.find_local_time_type(954032399)?.offset(), 0);
952         assert_eq!(transition_rule_negative_dst.find_local_time_type(954032400)?.offset(), 3600);
953         assert_eq!(transition_rule_negative_dst.find_local_time_type(972781199)?.offset(), 3600);
954         assert_eq!(transition_rule_negative_dst.find_local_time_type(972781200)?.offset(), 0);
955 
956         let transition_rule_negative_time_1 = TransitionRule::from(AlternateTime::new(
957             LocalTimeType::new(0, false, None)?,
958             LocalTimeType::new(0, true, None)?,
959             RuleDay::julian_0(100)?,
960             0,
961             RuleDay::julian_0(101)?,
962             -86500,
963         )?);
964 
965         assert!(transition_rule_negative_time_1.find_local_time_type(8639899)?.is_dst());
966         assert!(!transition_rule_negative_time_1.find_local_time_type(8639900)?.is_dst());
967         assert!(!transition_rule_negative_time_1.find_local_time_type(8639999)?.is_dst());
968         assert!(transition_rule_negative_time_1.find_local_time_type(8640000)?.is_dst());
969 
970         let transition_rule_negative_time_2 = TransitionRule::from(AlternateTime::new(
971             LocalTimeType::new(-10800, false, Some(b"-03"))?,
972             LocalTimeType::new(-7200, true, Some(b"-02"))?,
973             RuleDay::month_weekday(3, 5, 0)?,
974             -7200,
975             RuleDay::month_weekday(10, 5, 0)?,
976             -3600,
977         )?);
978 
979         assert_eq!(
980             transition_rule_negative_time_2.find_local_time_type(954032399)?.offset(),
981             -10800
982         );
983         assert_eq!(
984             transition_rule_negative_time_2.find_local_time_type(954032400)?.offset(),
985             -7200
986         );
987         assert_eq!(
988             transition_rule_negative_time_2.find_local_time_type(972781199)?.offset(),
989             -7200
990         );
991         assert_eq!(
992             transition_rule_negative_time_2.find_local_time_type(972781200)?.offset(),
993             -10800
994         );
995 
996         let transition_rule_all_year_dst = TransitionRule::from(AlternateTime::new(
997             LocalTimeType::new(-18000, false, Some(b"EST"))?,
998             LocalTimeType::new(-14400, true, Some(b"EDT"))?,
999             RuleDay::julian_0(0)?,
1000             0,
1001             RuleDay::julian_1(365)?,
1002             90000,
1003         )?);
1004 
1005         assert_eq!(transition_rule_all_year_dst.find_local_time_type(946702799)?.offset(), -14400);
1006         assert_eq!(transition_rule_all_year_dst.find_local_time_type(946702800)?.offset(), -14400);
1007 
1008         Ok(())
1009     }
1010 
1011     #[test]
test_transition_rule_overflow() -> Result<(), Error>1012     fn test_transition_rule_overflow() -> Result<(), Error> {
1013         let transition_rule_1 = TransitionRule::from(AlternateTime::new(
1014             LocalTimeType::new(-1, false, None)?,
1015             LocalTimeType::new(-1, true, None)?,
1016             RuleDay::julian_1(365)?,
1017             0,
1018             RuleDay::julian_1(1)?,
1019             0,
1020         )?);
1021 
1022         let transition_rule_2 = TransitionRule::from(AlternateTime::new(
1023             LocalTimeType::new(1, false, None)?,
1024             LocalTimeType::new(1, true, None)?,
1025             RuleDay::julian_1(365)?,
1026             0,
1027             RuleDay::julian_1(1)?,
1028             0,
1029         )?);
1030 
1031         let min_unix_time = -67768100567971200;
1032         let max_unix_time = 67767976233532799;
1033 
1034         assert!(matches!(
1035             transition_rule_1.find_local_time_type(min_unix_time),
1036             Err(Error::OutOfRange(_))
1037         ));
1038         assert!(matches!(
1039             transition_rule_2.find_local_time_type(max_unix_time),
1040             Err(Error::OutOfRange(_))
1041         ));
1042 
1043         Ok(())
1044     }
1045 }
1046