chrono/offset/local/tz_info/
rule.rs

1use std::cmp::Ordering;
2
3use super::parser::Cursor;
4use super::timezone::{LocalTimeType, SECONDS_PER_WEEK};
5use 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)]
12pub(super) enum TransitionRule {
13    /// Fixed local time type
14    Fixed(LocalTimeType),
15    /// Alternate local time types
16    Alternate(AlternateTime),
17}
18
19impl 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    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
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
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
98impl From<LocalTimeType> for TransitionRule {
99    fn from(inner: LocalTimeType) -> Self {
100        TransitionRule::Fixed(inner)
101    }
102}
103
104impl From<AlternateTime> for TransitionRule {
105    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)]
112pub(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
127impl AlternateTime {
128    /// Construct a transition rule representing alternate local time types
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
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
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
351fn 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
364fn 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
381fn 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
398fn 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
415fn 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
433fn 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)]
450enum 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
466impl RuleDay {
467    /// Parse transition rule
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
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
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
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]`
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
607    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)]
615pub(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
630impl UtcDateTime {
631    /// Construct a UTC date time from a Unix time in seconds and nanoseconds
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
707const NANOSECONDS_PER_SECOND: u32 = 1_000_000_000;
708/// Number of seconds in one minute
709const SECONDS_PER_MINUTE: i64 = 60;
710/// Number of seconds in one hour
711const SECONDS_PER_HOUR: i64 = 3600;
712/// Number of minutes in one hour
713const MINUTES_PER_HOUR: i64 = 60;
714/// Number of months in one year
715const MONTHS_PER_YEAR: i64 = 12;
716/// Number of days in a normal year
717const DAYS_PER_NORMAL_YEAR: i64 = 365;
718/// Number of days in 4 years (including 1 leap year)
719const DAYS_PER_4_YEARS: i64 = DAYS_PER_NORMAL_YEAR * 4 + 1;
720/// Number of days in 100 years (including 24 leap years)
721const DAYS_PER_100_YEARS: i64 = DAYS_PER_NORMAL_YEAR * 100 + 24;
722/// Number of days in 400 years (including 97 leap years)
723const DAYS_PER_400_YEARS: i64 = DAYS_PER_NORMAL_YEAR * 400 + 97;
724/// Unix time at `2000-03-01T00:00:00Z` (Wednesday)
725const UNIX_OFFSET_SECS: i64 = 951868800;
726/// Offset year
727const OFFSET_YEAR: i64 = 2000;
728/// Month days in a leap year from March
729const 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]`
739pub(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
770pub(crate) const fn is_leap_year(year: i32) -> bool {
771    year % 400 == 0 || (year % 4 == 0 && year % 100 != 0)
772}
773
774#[cfg(test)]
775mod tests {
776    use super::super::timezone::Transition;
777    use super::super::{Error, TimeZone};
778    use super::{AlternateTime, LocalTimeType, RuleDay, TransitionRule};
779
780    #[test]
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]
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]
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]
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]
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]
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]
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]
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]
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}