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