tz/timezone/
rule.rs

1//! Types related to a time zone extra transition rule.
2
3use crate::constants::*;
4use crate::timezone::*;
5
6/// Informations needed for checking DST transition rules consistency, for a Julian day
7#[derive(Debug, PartialEq, Eq)]
8struct JulianDayCheckInfos {
9    /// Offset in seconds from the start of a normal year
10    start_normal_year_offset: i64,
11    /// Offset in seconds from the end of a normal year
12    end_normal_year_offset: i64,
13    /// Offset in seconds from the start of a leap year
14    start_leap_year_offset: i64,
15    /// Offset in seconds from the end of a leap year
16    end_leap_year_offset: i64,
17}
18
19/// Informations needed for checking DST transition rules consistency, for a day represented by a month, a month week and a week day
20#[derive(Debug, PartialEq, Eq)]
21struct MonthWeekDayCheckInfos {
22    /// Possible offset range in seconds from the start of a normal year
23    start_normal_year_offset_range: (i64, i64),
24    /// Possible offset range in seconds from the end of a normal year
25    end_normal_year_offset_range: (i64, i64),
26    /// Possible offset range in seconds from the start of a leap year
27    start_leap_year_offset_range: (i64, i64),
28    /// Possible offset range in seconds from the end of a leap year
29    end_leap_year_offset_range: (i64, i64),
30}
31
32/// Julian day in `[1, 365]`, without taking occasional February 29th into account, which is not referenceable
33#[derive(Debug, Copy, Clone, Eq, PartialEq)]
34pub struct Julian1WithoutLeap(u16);
35
36impl Julian1WithoutLeap {
37    /// Construct a transition rule day represented by a Julian day in `[1, 365]`, without taking occasional February 29th into account, which is not referenceable
38    #[inline]
39    #[cfg_attr(feature = "const", const_fn::const_fn)]
40    pub fn new(julian_day_1: u16) -> Result<Self, TransitionRuleError> {
41        if !(1 <= julian_day_1 && julian_day_1 <= 365) {
42            return Err(TransitionRuleError("invalid rule day julian day"));
43        }
44
45        Ok(Self(julian_day_1))
46    }
47
48    /// Returns inner value
49    #[inline]
50    #[cfg_attr(feature = "const", const_fn::const_fn)]
51    pub fn get(&self) -> u16 {
52        self.0
53    }
54
55    /// Compute transition date
56    ///
57    /// ## Outputs
58    ///
59    /// * `month`: Month in `[1, 12]`
60    /// * `month_day`: Day of the month in `[1, 31]`
61    ///
62    #[cfg_attr(feature = "const", const_fn::const_fn)]
63    fn transition_date(&self) -> (usize, i64) {
64        let year_day = self.0 as i64;
65
66        let month = match binary_search_i64(&CUMUL_DAYS_IN_MONTHS_NORMAL_YEAR, year_day - 1) {
67            Ok(x) => x + 1,
68            Err(x) => x,
69        };
70
71        let month_day = year_day - CUMUL_DAYS_IN_MONTHS_NORMAL_YEAR[month - 1];
72
73        (month, month_day)
74    }
75
76    /// Compute the informations needed for checking DST transition rules consistency
77    #[cfg_attr(feature = "const", const_fn::const_fn)]
78    fn compute_check_infos(&self, utc_day_time: i64) -> JulianDayCheckInfos {
79        let start_normal_year_offset = (self.0 as i64 - 1) * SECONDS_PER_DAY + utc_day_time;
80        let start_leap_year_offset = if self.0 <= 59 { start_normal_year_offset } else { start_normal_year_offset + SECONDS_PER_DAY };
81
82        JulianDayCheckInfos {
83            start_normal_year_offset,
84            end_normal_year_offset: start_normal_year_offset - SECONDS_PER_NORMAL_YEAR,
85            start_leap_year_offset,
86            end_leap_year_offset: start_leap_year_offset - SECONDS_PER_LEAP_YEAR,
87        }
88    }
89}
90
91/// Zero-based Julian day in `[0, 365]`, taking occasional February 29th into account and allowing December 32nd
92#[derive(Debug, Copy, Clone, Eq, PartialEq)]
93pub struct Julian0WithLeap(u16);
94
95impl Julian0WithLeap {
96    /// Construct a transition rule day represented by a zero-based Julian day in `[0, 365]`, taking occasional February 29th into account and allowing December 32nd
97    #[inline]
98    #[cfg_attr(feature = "const", const_fn::const_fn)]
99    pub fn new(julian_day_0: u16) -> Result<Self, TransitionRuleError> {
100        if julian_day_0 > 365 {
101            return Err(TransitionRuleError("invalid rule day julian day"));
102        }
103
104        Ok(Self(julian_day_0))
105    }
106
107    /// Returns inner value
108    #[inline]
109    #[cfg_attr(feature = "const", const_fn::const_fn)]
110    pub fn get(&self) -> u16 {
111        self.0
112    }
113
114    /// Compute transition date.
115    ///
116    /// On a non-leap year, a value of `365` corresponds to December 32nd (which is January 1st of the next year).
117    ///
118    /// ## Outputs
119    ///
120    /// * `month`: Month in `[1, 12]`
121    /// * `month_day`: Day of the month in `[1, 32]`
122    ///
123    #[cfg_attr(feature = "const", const_fn::const_fn)]
124    fn transition_date(&self, leap_year: bool) -> (usize, i64) {
125        let cumul_day_in_months = if leap_year { &CUMUL_DAYS_IN_MONTHS_LEAP_YEAR } else { &CUMUL_DAYS_IN_MONTHS_NORMAL_YEAR };
126
127        let year_day = self.0 as i64;
128
129        let month = match binary_search_i64(cumul_day_in_months, year_day) {
130            Ok(x) => x + 1,
131            Err(x) => x,
132        };
133
134        let month_day = 1 + year_day - cumul_day_in_months[month - 1];
135
136        (month, month_day)
137    }
138
139    /// Compute the informations needed for checking DST transition rules consistency
140    #[cfg_attr(feature = "const", const_fn::const_fn)]
141    fn compute_check_infos(&self, utc_day_time: i64) -> JulianDayCheckInfos {
142        let start_year_offset = self.0 as i64 * SECONDS_PER_DAY + utc_day_time;
143
144        JulianDayCheckInfos {
145            start_normal_year_offset: start_year_offset,
146            end_normal_year_offset: start_year_offset - SECONDS_PER_NORMAL_YEAR,
147            start_leap_year_offset: start_year_offset,
148            end_leap_year_offset: start_year_offset - SECONDS_PER_LEAP_YEAR,
149        }
150    }
151}
152
153/// Day represented by a month, a month week and a week day
154#[derive(Debug, Copy, Clone, Eq, PartialEq)]
155pub struct MonthWeekDay {
156    /// Month in `[1, 12]`
157    month: u8,
158    /// Week of the month in `[1, 5]`, with `5` representing the last week of the month
159    week: u8,
160    /// Day of the week in `[0, 6]` from Sunday
161    week_day: u8,
162}
163
164impl MonthWeekDay {
165    /// Construct a transition rule day represented by a month, a month week and a week day
166    #[inline]
167    #[cfg_attr(feature = "const", const_fn::const_fn)]
168    pub fn new(month: u8, week: u8, week_day: u8) -> Result<Self, TransitionRuleError> {
169        if !(1 <= month && month <= 12) {
170            return Err(TransitionRuleError("invalid rule day month"));
171        }
172
173        if !(1 <= week && week <= 5) {
174            return Err(TransitionRuleError("invalid rule day week"));
175        }
176
177        if week_day > 6 {
178            return Err(TransitionRuleError("invalid rule day week day"));
179        }
180
181        Ok(Self { month, week, week_day })
182    }
183
184    /// Returns month in `[1, 12]`
185    #[inline]
186    #[cfg_attr(feature = "const", const_fn::const_fn)]
187    pub fn month(&self) -> u8 {
188        self.month
189    }
190
191    /// Returns week of the month in `[1, 5]`, with `5` representing the last week of the month
192    #[inline]
193    #[cfg_attr(feature = "const", const_fn::const_fn)]
194    pub fn week(&self) -> u8 {
195        self.week
196    }
197
198    /// Returns day of the week in `[0, 6]` from Sunday
199    #[inline]
200    #[cfg_attr(feature = "const", const_fn::const_fn)]
201    pub fn week_day(&self) -> u8 {
202        self.week_day
203    }
204
205    /// Compute transition date on a specific year
206    ///
207    /// ## Outputs
208    ///
209    /// * `month`: Month in `[1, 12]`
210    /// * `month_day`: Day of the month in `[1, 31]`
211    ///
212    #[cfg_attr(feature = "const", const_fn::const_fn)]
213    fn transition_date(&self, year: i32) -> (usize, i64) {
214        let month = self.month as usize;
215        let week = self.week as i64;
216        let week_day = self.week_day as i64;
217
218        let mut days_in_month = DAYS_IN_MONTHS_NORMAL_YEAR[month - 1];
219        if month == 2 {
220            days_in_month += is_leap_year(year) as i64;
221        }
222
223        let week_day_of_first_month_day = (4 + days_since_unix_epoch(year, month, 1)).rem_euclid(DAYS_PER_WEEK);
224        let first_week_day_occurence_in_month = 1 + (week_day as i64 - week_day_of_first_month_day).rem_euclid(DAYS_PER_WEEK);
225
226        let mut month_day = first_week_day_occurence_in_month + (week as i64 - 1) * DAYS_PER_WEEK;
227        if month_day > days_in_month {
228            month_day -= DAYS_PER_WEEK
229        }
230
231        (month, month_day)
232    }
233
234    /// Compute the informations needed for checking DST transition rules consistency
235    #[cfg_attr(feature = "const", const_fn::const_fn)]
236    fn compute_check_infos(&self, utc_day_time: i64) -> MonthWeekDayCheckInfos {
237        let month = self.month as usize;
238        let week = self.week as i64;
239
240        let (normal_year_month_day_range, leap_year_month_day_range) = {
241            if week == 5 {
242                let normal_year_days_in_month = DAYS_IN_MONTHS_NORMAL_YEAR[month - 1];
243                let leap_year_days_in_month = if month == 2 { normal_year_days_in_month + 1 } else { normal_year_days_in_month };
244
245                let normal_year_month_day_range = (normal_year_days_in_month - 6, normal_year_days_in_month);
246                let leap_year_month_day_range = (leap_year_days_in_month - 6, leap_year_days_in_month);
247
248                (normal_year_month_day_range, leap_year_month_day_range)
249            } else {
250                let month_day_range = (week * DAYS_PER_WEEK - 6, week * DAYS_PER_WEEK);
251                (month_day_range, month_day_range)
252            }
253        };
254
255        let start_normal_year_offset_range = (
256            (CUMUL_DAYS_IN_MONTHS_NORMAL_YEAR[month - 1] + normal_year_month_day_range.0 - 1) * SECONDS_PER_DAY + utc_day_time,
257            (CUMUL_DAYS_IN_MONTHS_NORMAL_YEAR[month - 1] + normal_year_month_day_range.1 - 1) * SECONDS_PER_DAY + utc_day_time,
258        );
259
260        let start_leap_year_offset_range = (
261            (CUMUL_DAYS_IN_MONTHS_LEAP_YEAR[month - 1] + leap_year_month_day_range.0 - 1) * SECONDS_PER_DAY + utc_day_time,
262            (CUMUL_DAYS_IN_MONTHS_LEAP_YEAR[month - 1] + leap_year_month_day_range.1 - 1) * SECONDS_PER_DAY + utc_day_time,
263        );
264
265        MonthWeekDayCheckInfos {
266            start_normal_year_offset_range,
267            end_normal_year_offset_range: (
268                start_normal_year_offset_range.0 - SECONDS_PER_NORMAL_YEAR,
269                start_normal_year_offset_range.1 - SECONDS_PER_NORMAL_YEAR,
270            ),
271            start_leap_year_offset_range,
272            end_leap_year_offset_range: (start_leap_year_offset_range.0 - SECONDS_PER_LEAP_YEAR, start_leap_year_offset_range.1 - SECONDS_PER_LEAP_YEAR),
273        }
274    }
275}
276
277/// Transition rule day
278#[derive(Debug, Copy, Clone, Eq, PartialEq)]
279pub enum RuleDay {
280    /// Julian day in `[1, 365]`, without taking occasional February 29th into account, which is not referenceable
281    Julian1WithoutLeap(Julian1WithoutLeap),
282    /// Zero-based Julian day in `[0, 365]`, taking occasional February 29th into account and allowing December 32nd
283    Julian0WithLeap(Julian0WithLeap),
284    /// Day represented by a month, a month week and a week day
285    MonthWeekDay(MonthWeekDay),
286}
287
288impl RuleDay {
289    /// Compute transition date for the provided year.
290    ///
291    /// The December 32nd date is possible, which corresponds to January 1st of the next year.
292    ///
293    /// ## Outputs
294    ///
295    /// * `month`: Month in `[1, 12]`
296    /// * `month_day`: Day of the month in `[1, 32]`
297    ///
298    #[cfg_attr(feature = "const", const_fn::const_fn)]
299    fn transition_date(&self, year: i32) -> (usize, i64) {
300        match self {
301            Self::Julian1WithoutLeap(rule_day) => rule_day.transition_date(),
302            Self::Julian0WithLeap(rule_day) => rule_day.transition_date(is_leap_year(year)),
303            Self::MonthWeekDay(rule_day) => rule_day.transition_date(year),
304        }
305    }
306
307    /// Returns the UTC Unix time in seconds associated to the transition date for the provided year
308    #[cfg_attr(feature = "const", const_fn::const_fn)]
309    pub(crate) fn unix_time(&self, year: i32, day_time_in_utc: i64) -> i64 {
310        let (month, month_day) = self.transition_date(year);
311        days_since_unix_epoch(year, month, month_day) * SECONDS_PER_DAY + day_time_in_utc
312    }
313}
314
315/// Transition rule representing alternate local time types
316#[derive(Debug, Copy, Clone, Eq, PartialEq)]
317pub struct AlternateTime {
318    /// Local time type for standard time
319    std: LocalTimeType,
320    /// Local time type for Daylight Saving Time
321    dst: LocalTimeType,
322    /// Start day of Daylight Saving Time
323    dst_start: RuleDay,
324    /// Local start day time of Daylight Saving Time, in seconds
325    dst_start_time: i32,
326    /// End day of Daylight Saving Time
327    dst_end: RuleDay,
328    /// Local end day time of Daylight Saving Time, in seconds
329    dst_end_time: i32,
330}
331
332impl AlternateTime {
333    /// Construct a transition rule representing alternate local time types
334    #[cfg_attr(feature = "const", const_fn::const_fn)]
335    pub fn new(
336        std: LocalTimeType,
337        dst: LocalTimeType,
338        dst_start: RuleDay,
339        dst_start_time: i32,
340        dst_end: RuleDay,
341        dst_end_time: i32,
342    ) -> Result<Self, TransitionRuleError> {
343        let std_ut_offset = std.ut_offset as i64;
344        let dst_ut_offset = dst.ut_offset as i64;
345
346        // Limit UTC offset to POSIX-required range
347        if !(-25 * SECONDS_PER_HOUR < std_ut_offset && std_ut_offset < 26 * SECONDS_PER_HOUR) {
348            return Err(TransitionRuleError("invalid standard time UTC offset"));
349        }
350
351        if !(-25 * SECONDS_PER_HOUR < dst_ut_offset && dst_ut_offset < 26 * SECONDS_PER_HOUR) {
352            return Err(TransitionRuleError("invalid Daylight Saving Time UTC offset"));
353        }
354
355        // Overflow is not possible
356        if !((dst_start_time as i64).abs() < SECONDS_PER_WEEK && (dst_end_time as i64).abs() < SECONDS_PER_WEEK) {
357            return Err(TransitionRuleError("invalid DST start or end time"));
358        }
359
360        // Check DST transition rules consistency
361        if !check_dst_transition_rules_consistency(&std, &dst, dst_start, dst_start_time, dst_end, dst_end_time) {
362            return Err(TransitionRuleError("DST transition rules are not consistent from one year to another"));
363        }
364
365        Ok(Self { std, dst, dst_start, dst_start_time, dst_end, dst_end_time })
366    }
367
368    /// Returns local time type for standard time
369    #[inline]
370    #[cfg_attr(feature = "const", const_fn::const_fn)]
371    pub fn std(&self) -> &LocalTimeType {
372        &self.std
373    }
374
375    /// Returns local time type for Daylight Saving Time
376    #[inline]
377    #[cfg_attr(feature = "const", const_fn::const_fn)]
378    pub fn dst(&self) -> &LocalTimeType {
379        &self.dst
380    }
381
382    /// Returns start day of Daylight Saving Time
383    #[inline]
384    #[cfg_attr(feature = "const", const_fn::const_fn)]
385    pub fn dst_start(&self) -> &RuleDay {
386        &self.dst_start
387    }
388
389    /// Returns local start day time of Daylight Saving Time, in seconds
390    #[inline]
391    #[cfg_attr(feature = "const", const_fn::const_fn)]
392    pub fn dst_start_time(&self) -> i32 {
393        self.dst_start_time
394    }
395
396    /// Returns end day of Daylight Saving Time
397    #[inline]
398    #[cfg_attr(feature = "const", const_fn::const_fn)]
399    pub fn dst_end(&self) -> &RuleDay {
400        &self.dst_end
401    }
402
403    /// Returns local end day time of Daylight Saving Time, in seconds
404    #[inline]
405    #[cfg_attr(feature = "const", const_fn::const_fn)]
406    pub fn dst_end_time(&self) -> i32 {
407        self.dst_end_time
408    }
409
410    /// Find the local time type associated to the alternate transition rule at the specified Unix time in seconds
411    #[cfg_attr(feature = "const", const_fn::const_fn)]
412    fn find_local_time_type(&self, unix_time: i64) -> Result<&LocalTimeType, OutOfRangeError> {
413        // Overflow is not possible
414        let dst_start_time_in_utc = self.dst_start_time as i64 - self.std.ut_offset as i64;
415        let dst_end_time_in_utc = self.dst_end_time as i64 - self.dst.ut_offset as i64;
416
417        let current_year = match UtcDateTime::from_timespec(unix_time, 0) {
418            Ok(utc_date_time) => utc_date_time.year(),
419            Err(error) => return Err(error),
420        };
421
422        // Check if the current year is valid for the following computations
423        if !(i32::MIN + 2 <= current_year && current_year <= i32::MAX - 2) {
424            return Err(OutOfRangeError("out of range date time"));
425        }
426
427        let current_year_dst_start_unix_time = self.dst_start.unix_time(current_year, dst_start_time_in_utc);
428        let current_year_dst_end_unix_time = self.dst_end.unix_time(current_year, dst_end_time_in_utc);
429
430        // Check DST start/end Unix times for previous/current/next years to support for transition day times outside of [0h, 24h] range.
431        // This is sufficient since the absolute value of DST start/end time in UTC is less than 2 weeks.
432        // Moreover, inconsistent DST transition rules are not allowed, so there won't be additional transitions at the year boundary.
433        let is_dst = match cmp(current_year_dst_start_unix_time, current_year_dst_end_unix_time) {
434            Ordering::Less | Ordering::Equal => {
435                if unix_time < current_year_dst_start_unix_time {
436                    let previous_year_dst_end_unix_time = self.dst_end.unix_time(current_year - 1, dst_end_time_in_utc);
437                    if unix_time < previous_year_dst_end_unix_time {
438                        let previous_year_dst_start_unix_time = self.dst_start.unix_time(current_year - 1, dst_start_time_in_utc);
439                        previous_year_dst_start_unix_time <= unix_time
440                    } else {
441                        false
442                    }
443                } else if unix_time < current_year_dst_end_unix_time {
444                    true
445                } else {
446                    let next_year_dst_start_unix_time = self.dst_start.unix_time(current_year + 1, dst_start_time_in_utc);
447                    if next_year_dst_start_unix_time <= unix_time {
448                        let next_year_dst_end_unix_time = self.dst_end.unix_time(current_year + 1, dst_end_time_in_utc);
449                        unix_time < next_year_dst_end_unix_time
450                    } else {
451                        false
452                    }
453                }
454            }
455            Ordering::Greater => {
456                if unix_time < current_year_dst_end_unix_time {
457                    let previous_year_dst_start_unix_time = self.dst_start.unix_time(current_year - 1, dst_start_time_in_utc);
458                    if unix_time < previous_year_dst_start_unix_time {
459                        let previous_year_dst_end_unix_time = self.dst_end.unix_time(current_year - 1, dst_end_time_in_utc);
460                        unix_time < previous_year_dst_end_unix_time
461                    } else {
462                        true
463                    }
464                } else if unix_time < current_year_dst_start_unix_time {
465                    false
466                } else {
467                    let next_year_dst_end_unix_time = self.dst_end.unix_time(current_year + 1, dst_end_time_in_utc);
468                    if next_year_dst_end_unix_time <= unix_time {
469                        let next_year_dst_start_unix_time = self.dst_start.unix_time(current_year + 1, dst_start_time_in_utc);
470                        next_year_dst_start_unix_time <= unix_time
471                    } else {
472                        true
473                    }
474                }
475            }
476        };
477
478        if is_dst {
479            Ok(&self.dst)
480        } else {
481            Ok(&self.std)
482        }
483    }
484}
485
486/// Transition rule
487#[derive(Debug, Copy, Clone, Eq, PartialEq)]
488pub enum TransitionRule {
489    /// Fixed local time type
490    Fixed(LocalTimeType),
491    /// Alternate local time types
492    Alternate(AlternateTime),
493}
494
495impl TransitionRule {
496    /// Find the local time type associated to the transition rule at the specified Unix time in seconds
497    #[cfg_attr(feature = "const", const_fn::const_fn)]
498    pub(super) fn find_local_time_type(&self, unix_time: i64) -> Result<&LocalTimeType, OutOfRangeError> {
499        match self {
500            Self::Fixed(local_time_type) => Ok(local_time_type),
501            Self::Alternate(alternate_time) => alternate_time.find_local_time_type(unix_time),
502        }
503    }
504}
505
506/// Check DST transition rules consistency, which ensures that the DST start and end time are always in the same order.
507///
508/// This prevents from having an additional transition at the year boundary, when the order of DST start and end time is different on consecutive years.
509///
510#[cfg_attr(feature = "const", const_fn::const_fn)]
511fn check_dst_transition_rules_consistency(
512    std: &LocalTimeType,
513    dst: &LocalTimeType,
514    dst_start: RuleDay,
515    dst_start_time: i32,
516    dst_end: RuleDay,
517    dst_end_time: i32,
518) -> bool {
519    // Overflow is not possible
520    let dst_start_time_in_utc = dst_start_time as i64 - std.ut_offset as i64;
521    let dst_end_time_in_utc = dst_end_time as i64 - dst.ut_offset as i64;
522
523    match (dst_start, dst_end) {
524        (RuleDay::Julian1WithoutLeap(start_day), RuleDay::Julian1WithoutLeap(end_day)) => {
525            check_two_julian_days(start_day.compute_check_infos(dst_start_time_in_utc), end_day.compute_check_infos(dst_end_time_in_utc))
526        }
527        (RuleDay::Julian1WithoutLeap(start_day), RuleDay::Julian0WithLeap(end_day)) => {
528            check_two_julian_days(start_day.compute_check_infos(dst_start_time_in_utc), end_day.compute_check_infos(dst_end_time_in_utc))
529        }
530        (RuleDay::Julian0WithLeap(start_day), RuleDay::Julian1WithoutLeap(end_day)) => {
531            check_two_julian_days(start_day.compute_check_infos(dst_start_time_in_utc), end_day.compute_check_infos(dst_end_time_in_utc))
532        }
533        (RuleDay::Julian0WithLeap(start_day), RuleDay::Julian0WithLeap(end_day)) => {
534            check_two_julian_days(start_day.compute_check_infos(dst_start_time_in_utc), end_day.compute_check_infos(dst_end_time_in_utc))
535        }
536        (RuleDay::Julian1WithoutLeap(start_day), RuleDay::MonthWeekDay(end_day)) => {
537            check_month_week_day_and_julian_day(end_day.compute_check_infos(dst_end_time_in_utc), start_day.compute_check_infos(dst_start_time_in_utc))
538        }
539        (RuleDay::Julian0WithLeap(start_day), RuleDay::MonthWeekDay(end_day)) => {
540            check_month_week_day_and_julian_day(end_day.compute_check_infos(dst_end_time_in_utc), start_day.compute_check_infos(dst_start_time_in_utc))
541        }
542        (RuleDay::MonthWeekDay(start_day), RuleDay::Julian1WithoutLeap(end_day)) => {
543            check_month_week_day_and_julian_day(start_day.compute_check_infos(dst_start_time_in_utc), end_day.compute_check_infos(dst_end_time_in_utc))
544        }
545        (RuleDay::MonthWeekDay(start_day), RuleDay::Julian0WithLeap(end_day)) => {
546            check_month_week_day_and_julian_day(start_day.compute_check_infos(dst_start_time_in_utc), end_day.compute_check_infos(dst_end_time_in_utc))
547        }
548        (RuleDay::MonthWeekDay(start_day), RuleDay::MonthWeekDay(end_day)) => {
549            check_two_month_week_days(start_day, dst_start_time_in_utc, end_day, dst_end_time_in_utc)
550        }
551    }
552}
553
554/// Check DST transition rules consistency for two Julian days
555#[cfg_attr(feature = "const", const_fn::const_fn)]
556fn check_two_julian_days(check_infos_1: JulianDayCheckInfos, check_infos_2: JulianDayCheckInfos) -> bool {
557    // Check in same year
558    let (before, after) = if check_infos_1.start_normal_year_offset <= check_infos_2.start_normal_year_offset
559        && check_infos_1.start_leap_year_offset <= check_infos_2.start_leap_year_offset
560    {
561        (&check_infos_1, &check_infos_2)
562    } else if check_infos_2.start_normal_year_offset <= check_infos_1.start_normal_year_offset
563        && check_infos_2.start_leap_year_offset <= check_infos_1.start_leap_year_offset
564    {
565        (&check_infos_2, &check_infos_1)
566    } else {
567        return false;
568    };
569
570    // Check in consecutive years
571    if after.end_normal_year_offset <= before.start_normal_year_offset
572        && after.end_normal_year_offset <= before.start_leap_year_offset
573        && after.end_leap_year_offset <= before.start_normal_year_offset
574    {
575        return true;
576    }
577
578    if before.start_normal_year_offset <= after.end_normal_year_offset
579        && before.start_leap_year_offset <= after.end_normal_year_offset
580        && before.start_normal_year_offset <= after.end_leap_year_offset
581    {
582        return true;
583    }
584
585    false
586}
587
588/// Check DST transition rules consistency for a Julian day and a day represented by a month, a month week and a week day
589#[cfg_attr(feature = "const", const_fn::const_fn)]
590fn check_month_week_day_and_julian_day(check_infos_1: MonthWeekDayCheckInfos, check_infos_2: JulianDayCheckInfos) -> bool {
591    // Check in same year, then in consecutive years
592    if check_infos_2.start_normal_year_offset <= check_infos_1.start_normal_year_offset_range.0
593        && check_infos_2.start_leap_year_offset <= check_infos_1.start_leap_year_offset_range.0
594    {
595        let (before, after) = (&check_infos_2, &check_infos_1);
596
597        if after.end_normal_year_offset_range.1 <= before.start_normal_year_offset
598            && after.end_normal_year_offset_range.1 <= before.start_leap_year_offset
599            && after.end_leap_year_offset_range.1 <= before.start_normal_year_offset
600        {
601            return true;
602        };
603
604        if before.start_normal_year_offset <= after.end_normal_year_offset_range.0
605            && before.start_leap_year_offset <= after.end_normal_year_offset_range.0
606            && before.start_normal_year_offset <= after.end_leap_year_offset_range.0
607        {
608            return true;
609        };
610
611        return false;
612    }
613
614    if check_infos_1.start_normal_year_offset_range.1 <= check_infos_2.start_normal_year_offset
615        && check_infos_1.start_leap_year_offset_range.1 <= check_infos_2.start_leap_year_offset
616    {
617        let (before, after) = (&check_infos_1, &check_infos_2);
618
619        if after.end_normal_year_offset <= before.start_normal_year_offset_range.0
620            && after.end_normal_year_offset <= before.start_leap_year_offset_range.0
621            && after.end_leap_year_offset <= before.start_normal_year_offset_range.0
622        {
623            return true;
624        }
625
626        if before.start_normal_year_offset_range.1 <= after.end_normal_year_offset
627            && before.start_leap_year_offset_range.1 <= after.end_normal_year_offset
628            && before.start_normal_year_offset_range.1 <= after.end_leap_year_offset
629        {
630            return true;
631        }
632
633        return false;
634    }
635
636    false
637}
638
639/// Check DST transition rules consistency for two days represented by a month, a month week and a week day
640#[cfg_attr(feature = "const", const_fn::const_fn)]
641fn check_two_month_week_days(month_week_day_1: MonthWeekDay, utc_day_time_1: i64, month_week_day_2: MonthWeekDay, utc_day_time_2: i64) -> bool {
642    // Sort rule days
643    let (month_week_day_before, utc_day_time_before, month_week_day_after, utc_day_time_after) = {
644        let rem = (month_week_day_2.month as i64 - month_week_day_1.month as i64).rem_euclid(MONTHS_PER_YEAR);
645
646        if rem == 0 {
647            if month_week_day_1.week <= month_week_day_2.week {
648                (month_week_day_1, utc_day_time_1, month_week_day_2, utc_day_time_2)
649            } else {
650                (month_week_day_2, utc_day_time_2, month_week_day_1, utc_day_time_1)
651            }
652        } else if rem == 1 {
653            (month_week_day_1, utc_day_time_1, month_week_day_2, utc_day_time_2)
654        } else if rem == MONTHS_PER_YEAR - 1 {
655            (month_week_day_2, utc_day_time_2, month_week_day_1, utc_day_time_1)
656        } else {
657            // Months are not equal or consecutive, so rule days are separated by more than 3 weeks and cannot swap their order
658            return true;
659        }
660    };
661
662    let month_before = month_week_day_before.month as usize;
663    let week_before = month_week_day_before.week as i64;
664    let week_day_before = month_week_day_before.week_day as i64;
665
666    let month_after = month_week_day_after.month as usize;
667    let week_after = month_week_day_after.week as i64;
668    let week_day_after = month_week_day_after.week_day as i64;
669
670    let (diff_days_min, diff_days_max) = if week_day_before == week_day_after {
671        // Rule days are separated by a whole number of weeks
672        let (diff_week_min, diff_week_max) = match (week_before, week_after) {
673            // All months have more than 29 days on a leap year, so the 5th week is non-empty
674            (1..=4, 5) if month_before == month_after => (4 - week_before, 5 - week_before),
675            (1..=4, 1..=4) if month_before != month_after => (4 - week_before + week_after, 5 - week_before + week_after),
676            _ => return true, // rule days are synchronized or separated by more than 3 weeks
677        };
678
679        (diff_week_min * DAYS_PER_WEEK, diff_week_max * DAYS_PER_WEEK)
680    } else {
681        // week_day_before != week_day_after
682        let n = (week_day_after - week_day_before).rem_euclid(DAYS_PER_WEEK); // n >= 1
683
684        if month_before == month_after {
685            match (week_before, week_after) {
686                (5, 5) => (n - DAYS_PER_WEEK, n),
687                (1..=4, 1..=4) => (n + DAYS_PER_WEEK * (week_after - week_before - 1), n + DAYS_PER_WEEK * (week_after - week_before)),
688                (1..=4, 5) => {
689                    // For February month:
690                    //   * On a normal year, we have n > (days_in_month % DAYS_PER_WEEK).
691                    //   * On a leap year, we have n >= (days_in_month % DAYS_PER_WEEK).
692                    //
693                    // Since we want to check all possible years at the same time, checking only non-leap year is enough.
694                    let days_in_month = DAYS_IN_MONTHS_NORMAL_YEAR[month_before - 1];
695
696                    match cmp(n, days_in_month % DAYS_PER_WEEK) {
697                        Ordering::Less => (n + DAYS_PER_WEEK * (4 - week_before), n + DAYS_PER_WEEK * (5 - week_before)),
698                        Ordering::Equal => return true, // rule days are synchronized
699                        Ordering::Greater => (n + DAYS_PER_WEEK * (3 - week_before), n + DAYS_PER_WEEK * (4 - week_before)),
700                    }
701                }
702                _ => const_panic!(), // unreachable
703            }
704        } else {
705            // month_before != month_after
706            match (week_before, week_after) {
707                (1..=4, 1..=4) => {
708                    // Same as above
709                    let days_in_month = DAYS_IN_MONTHS_NORMAL_YEAR[month_before - 1];
710
711                    match cmp(n, days_in_month % DAYS_PER_WEEK) {
712                        Ordering::Less => (n + DAYS_PER_WEEK * (4 - week_before + week_after), n + DAYS_PER_WEEK * (5 - week_before + week_after)),
713                        Ordering::Equal => return true, // rule days are synchronized
714                        Ordering::Greater => (n + DAYS_PER_WEEK * (3 - week_before + week_after), n + DAYS_PER_WEEK * (4 - week_before + week_after)),
715                    }
716                }
717                (5, 1..=4) => (n + DAYS_PER_WEEK * (week_after - 1), n + DAYS_PER_WEEK * week_after),
718                _ => return true, // rule days are separated by more than 3 weeks
719            }
720        }
721    };
722
723    let diff_days_seconds_min = diff_days_min * SECONDS_PER_DAY;
724    let diff_days_seconds_max = diff_days_max * SECONDS_PER_DAY;
725
726    // Check possible order swap of rule days
727    utc_day_time_before <= diff_days_seconds_min + utc_day_time_after || diff_days_seconds_max + utc_day_time_after <= utc_day_time_before
728}
729
730#[cfg(test)]
731mod test {
732    use super::*;
733    use crate::Result;
734
735    #[test]
736    fn test_compute_check_infos() -> Result<()> {
737        let check_julian = |check_infos: JulianDayCheckInfos, start_normal, end_normal, start_leap, end_leap| {
738            assert_eq!(check_infos.start_normal_year_offset, start_normal);
739            assert_eq!(check_infos.end_normal_year_offset, end_normal);
740            assert_eq!(check_infos.start_leap_year_offset, start_leap);
741            assert_eq!(check_infos.end_leap_year_offset, end_leap);
742        };
743
744        let check_mwd = |check_infos: MonthWeekDayCheckInfos, start_normal, end_normal, start_leap, end_leap| {
745            assert_eq!(check_infos.start_normal_year_offset_range, start_normal);
746            assert_eq!(check_infos.end_normal_year_offset_range, end_normal);
747            assert_eq!(check_infos.start_leap_year_offset_range, start_leap);
748            assert_eq!(check_infos.end_leap_year_offset_range, end_leap);
749        };
750
751        check_julian(Julian1WithoutLeap::new(1)?.compute_check_infos(1), 1, -31535999, 1, -31622399);
752        check_julian(Julian1WithoutLeap::new(365)?.compute_check_infos(1), 31449601, -86399, 31536001, -86399);
753
754        check_julian(Julian0WithLeap::new(0)?.compute_check_infos(1), 1, -31535999, 1, -31622399);
755        check_julian(Julian0WithLeap::new(365)?.compute_check_infos(1), 31536001, 1, 31536001, -86399);
756
757        check_mwd(MonthWeekDay::new(1, 1, 0)?.compute_check_infos(1), (1, 518401), (-31535999, -31017599), (1, 518401), (-31622399, -31103999));
758        check_mwd(MonthWeekDay::new(1, 5, 0)?.compute_check_infos(1), (2073601, 2592001), (-29462399, -28943999), (2073601, 2592001), (-29548799, -29030399));
759        check_mwd(MonthWeekDay::new(2, 4, 0)?.compute_check_infos(1), (4492801, 5011201), (-27043199, -26524799), (4492801, 5011201), (-27129599, -26611199));
760        check_mwd(MonthWeekDay::new(2, 5, 0)?.compute_check_infos(1), (4492801, 5011201), (-27043199, -26524799), (4579201, 5097601), (-27043199, -26524799));
761        check_mwd(MonthWeekDay::new(3, 1, 0)?.compute_check_infos(1), (5097601, 5616001), (-26438399, -25919999), (5184001, 5702401), (-26438399, -25919999));
762        check_mwd(MonthWeekDay::new(3, 5, 0)?.compute_check_infos(1), (7171201, 7689601), (-24364799, -23846399), (7257601, 7776001), (-24364799, -23846399));
763        check_mwd(MonthWeekDay::new(12, 5, 0)?.compute_check_infos(1), (30931201, 31449601), (-604799, -86399), (31017601, 31536001), (-604799, -86399));
764
765        Ok(())
766    }
767
768    #[test]
769    fn test_check_dst_transition_rules_consistency() -> Result<()> {
770        let utc = LocalTimeType::utc();
771
772        let julian_1 = |year_day| Result::Ok(RuleDay::Julian1WithoutLeap(Julian1WithoutLeap::new(year_day)?));
773        let julian_0 = |year_day| Result::Ok(RuleDay::Julian0WithLeap(Julian0WithLeap::new(year_day)?));
774        let mwd = |month, week, week_day| Result::Ok(RuleDay::MonthWeekDay(MonthWeekDay::new(month, week, week_day)?));
775
776        let check = |dst_start, dst_start_time, dst_end, dst_end_time| {
777            let check_1 = check_dst_transition_rules_consistency(&utc, &utc, dst_start, dst_start_time, dst_end, dst_end_time);
778            let check_2 = check_dst_transition_rules_consistency(&utc, &utc, dst_end, dst_end_time, dst_start, dst_start_time);
779            assert_eq!(check_1, check_2);
780
781            check_1
782        };
783
784        let check_all = |dst_start, dst_start_times: &[i32], dst_end, dst_end_time, results: &[bool]| {
785            assert_eq!(dst_start_times.len(), results.len());
786
787            for (&dst_start_time, &result) in dst_start_times.iter().zip(results) {
788                assert_eq!(check(dst_start, dst_start_time, dst_end, dst_end_time), result);
789            }
790        };
791
792        const DAY_1: i32 = 86400;
793        const DAY_2: i32 = 2 * DAY_1;
794        const DAY_3: i32 = 3 * DAY_1;
795        const DAY_4: i32 = 4 * DAY_1;
796        const DAY_5: i32 = 5 * DAY_1;
797        const DAY_6: i32 = 6 * DAY_1;
798
799        check_all(julian_1(59)?, &[-1, 0, 1], julian_1(60)?, -DAY_1, &[true, true, false]);
800        check_all(julian_1(365)?, &[-1, 0, 1], julian_1(1)?, -DAY_1, &[true, true, true]);
801
802        check_all(julian_0(58)?, &[-1, 0, 1], julian_0(59)?, -DAY_1, &[true, true, true]);
803        check_all(julian_0(364)?, &[-1, 0, 1], julian_0(0)?, -DAY_1, &[true, true, false]);
804        check_all(julian_0(365)?, &[-1, 0, 1], julian_0(0)?, 0, &[true, true, false]);
805
806        check_all(julian_1(90)?, &[-1, 0, 1], julian_0(90)?, 0, &[true, true, false]);
807        check_all(julian_1(365)?, &[-1, 0, 1], julian_0(0)?, 0, &[true, true, true]);
808
809        check_all(julian_0(89)?, &[-1, 0, 1], julian_1(90)?, 0, &[true, true, false]);
810        check_all(julian_0(364)?, &[-1, 0, 1], julian_1(1)?, -DAY_1, &[true, true, false]);
811        check_all(julian_0(365)?, &[-1, 0, 1], julian_1(1)?, 0, &[true, true, false]);
812
813        check_all(mwd(1, 4, 0)?, &[-1, 0, 1], julian_1(28)?, 0, &[true, true, false]);
814        check_all(mwd(2, 5, 0)?, &[-1, 0, 1], julian_1(60)?, -DAY_1, &[true, true, false]);
815        check_all(mwd(12, 5, 0)?, &[-1, 0, 1], julian_1(1)?, -DAY_1, &[true, true, false]);
816        check_all(mwd(12, 5, 0)?, &[DAY_3 - 1, DAY_3, DAY_3 + 1], julian_1(1)?, -DAY_4, &[false, true, true]);
817
818        check_all(mwd(1, 4, 0)?, &[-1, 0, 1], julian_0(27)?, 0, &[true, true, false]);
819        check_all(mwd(2, 5, 0)?, &[-1, 0, 1], julian_0(58)?, DAY_1, &[true, true, false]);
820        check_all(mwd(2, 4, 0)?, &[-1, 0, 1], julian_0(59)?, -DAY_1, &[true, true, false]);
821        check_all(mwd(2, 5, 0)?, &[-1, 0, 1], julian_0(59)?, 0, &[true, true, false]);
822        check_all(mwd(12, 5, 0)?, &[-1, 0, 1], julian_0(0)?, -DAY_1, &[true, true, false]);
823        check_all(mwd(12, 5, 0)?, &[DAY_3 - 1, DAY_3, DAY_3 + 1], julian_0(0)?, -DAY_4, &[false, true, true]);
824
825        check_all(julian_1(1)?, &[-1, 0, 1], mwd(1, 1, 0)?, 0, &[true, true, false]);
826        check_all(julian_1(53)?, &[-1, 0, 1], mwd(2, 5, 0)?, 0, &[true, true, false]);
827        check_all(julian_1(365)?, &[-1, 0, 1], mwd(1, 1, 0)?, -DAY_1, &[true, true, false]);
828        check_all(julian_1(365)?, &[DAY_3 - 1, DAY_3, DAY_3 + 1], mwd(1, 1, 0)?, -DAY_4, &[false, true, true]);
829
830        check_all(julian_0(0)?, &[-1, 0, 1], mwd(1, 1, 0)?, 0, &[true, true, false]);
831        check_all(julian_0(52)?, &[-1, 0, 1], mwd(2, 5, 0)?, 0, &[true, true, false]);
832        check_all(julian_0(59)?, &[-1, 0, 1], mwd(3, 1, 0)?, 0, &[true, true, false]);
833        check_all(julian_0(59)?, &[-DAY_3 - 1, -DAY_3, -DAY_3 + 1], mwd(2, 5, 0)?, DAY_4, &[true, true, false]);
834        check_all(julian_0(364)?, &[-1, 0, 1], mwd(1, 1, 0)?, -DAY_1, &[true, true, false]);
835        check_all(julian_0(365)?, &[-1, 0, 1], mwd(1, 1, 0)?, 0, &[true, true, false]);
836        check_all(julian_0(364)?, &[DAY_4 - 1, DAY_4, DAY_4 + 1], mwd(1, 1, 0)?, -DAY_4, &[false, true, true]);
837        check_all(julian_0(365)?, &[DAY_3 - 1, DAY_3, DAY_3 + 1], mwd(1, 1, 0)?, -DAY_4, &[false, true, true]);
838
839        let months_per_year = MONTHS_PER_YEAR as u8;
840        for i in 0..months_per_year - 1 {
841            let month = i + 1;
842            let month_1 = (i + 1) % months_per_year + 1;
843            let month_2 = (i + 2) % months_per_year + 1;
844
845            assert!(check(mwd(month, 1, 0)?, 0, mwd(month_2, 1, 0)?, 0));
846            assert!(check(mwd(month, 3, 0)?, DAY_4, mwd(month, 4, 0)?, -DAY_3));
847
848            check_all(mwd(month, 5, 0)?, &[-1, 0, 1], mwd(month, 5, 0)?, 0, &[true, true, true]);
849            check_all(mwd(month, 4, 0)?, &[-1, 0, 1], mwd(month, 5, 0)?, 0, &[true, true, false]);
850            check_all(mwd(month, 4, 0)?, &[DAY_4 - 1, DAY_4, DAY_4 + 1], mwd(month_1, 1, 0)?, -DAY_3, &[true, true, false]);
851            check_all(mwd(month, 5, 0)?, &[DAY_4 - 1, DAY_4, DAY_4 + 1], mwd(month_1, 1, 0)?, -DAY_3, &[true, true, true]);
852            check_all(mwd(month, 5, 0)?, &[-1, 0, 1], mwd(month_1, 5, 0)?, 0, &[true, true, true]);
853            check_all(mwd(month, 3, 2)?, &[-1, 0, 1], mwd(month, 4, 3)?, -DAY_1, &[true, true, false]);
854            check_all(mwd(month, 5, 2)?, &[-1, 0, 1], mwd(month, 5, 3)?, -DAY_1, &[false, true, true]);
855            check_all(mwd(month, 5, 2)?, &[-1, 0, 1], mwd(month_1, 1, 3)?, -DAY_1, &[true, true, false]);
856            check_all(mwd(month, 5, 2)?, &[-1, 0, 1], mwd(month_1, 5, 3)?, 0, &[true, true, true]);
857        }
858
859        check_all(mwd(2, 4, 2)?, &[-1, 0, 1], mwd(2, 5, 3)?, -DAY_1, &[false, true, true]);
860
861        check_all(mwd(3, 4, 2)?, &[-1, 0, 1], mwd(3, 5, 4)?, -DAY_2, &[true, true, false]);
862        check_all(mwd(3, 4, 2)?, &[-1, 0, 1], mwd(3, 5, 5)?, -DAY_3, &[true, true, true]);
863        check_all(mwd(3, 4, 2)?, &[-1, 0, 1], mwd(3, 5, 6)?, -DAY_4, &[false, true, true]);
864
865        check_all(mwd(4, 4, 2)?, &[-1, 0, 1], mwd(4, 5, 3)?, -DAY_1, &[true, true, false]);
866        check_all(mwd(4, 4, 2)?, &[-1, 0, 1], mwd(4, 5, 4)?, -DAY_2, &[true, true, true]);
867        check_all(mwd(4, 4, 2)?, &[-1, 0, 1], mwd(4, 5, 5)?, -DAY_3, &[false, true, true]);
868
869        check_all(mwd(2, 4, 2)?, &[DAY_5 - 1, DAY_5, DAY_5 + 1], mwd(3, 1, 3)?, -DAY_3, &[false, true, true]);
870
871        check_all(mwd(3, 4, 2)?, &[DAY_5 - 1, DAY_5, DAY_5 + 1], mwd(4, 1, 4)?, -DAY_4, &[true, true, false]);
872        check_all(mwd(3, 4, 2)?, &[DAY_5 - 1, DAY_5, DAY_5 + 1], mwd(4, 1, 5)?, -DAY_5, &[true, true, true]);
873        check_all(mwd(3, 4, 2)?, &[DAY_5 - 1, DAY_5, DAY_5 + 1], mwd(4, 1, 6)?, -DAY_6, &[false, true, true]);
874
875        check_all(mwd(4, 4, 2)?, &[DAY_5 - 1, DAY_5, DAY_5 + 1], mwd(5, 1, 3)?, -DAY_3, &[true, true, false]);
876        check_all(mwd(4, 4, 2)?, &[DAY_5 - 1, DAY_5, DAY_5 + 1], mwd(5, 1, 4)?, -DAY_4, &[true, true, true]);
877        check_all(mwd(4, 4, 2)?, &[DAY_5 - 1, DAY_5, DAY_5 + 1], mwd(5, 1, 5)?, -DAY_5, &[false, true, true]);
878
879        Ok(())
880    }
881
882    #[test]
883    fn test_rule_day() -> Result<()> {
884        let rule_day_j1 = RuleDay::Julian1WithoutLeap(Julian1WithoutLeap::new(60)?);
885        assert_eq!(rule_day_j1.transition_date(2000), (3, 1));
886        assert_eq!(rule_day_j1.transition_date(2001), (3, 1));
887        assert_eq!(rule_day_j1.unix_time(2000, 43200), 951912000);
888
889        let rule_day_j0 = RuleDay::Julian0WithLeap(Julian0WithLeap::new(59)?);
890        assert_eq!(rule_day_j0.transition_date(2000), (2, 29));
891        assert_eq!(rule_day_j0.transition_date(2001), (3, 1));
892        assert_eq!(rule_day_j0.unix_time(2000, 43200), 951825600);
893
894        let rule_day_j0_max = RuleDay::Julian0WithLeap(Julian0WithLeap::new(365)?);
895        assert_eq!(rule_day_j0_max.transition_date(2000), (12, 31));
896        assert_eq!(rule_day_j0_max.transition_date(2001), (12, 32));
897
898        assert_eq!(
899            RuleDay::Julian0WithLeap(Julian0WithLeap::new(365)?).unix_time(2000, 0),
900            RuleDay::Julian1WithoutLeap(Julian1WithoutLeap::new(365)?).unix_time(2000, 0)
901        );
902
903        assert_eq!(
904            RuleDay::Julian0WithLeap(Julian0WithLeap::new(365)?).unix_time(1999, 0),
905            RuleDay::Julian1WithoutLeap(Julian1WithoutLeap::new(1)?).unix_time(2000, 0),
906        );
907
908        let rule_day_mwd = RuleDay::MonthWeekDay(MonthWeekDay::new(2, 5, 2)?);
909        assert_eq!(rule_day_mwd.transition_date(2000), (2, 29));
910        assert_eq!(rule_day_mwd.transition_date(2001), (2, 27));
911        assert_eq!(rule_day_mwd.unix_time(2000, 43200), 951825600);
912        assert_eq!(rule_day_mwd.unix_time(2001, 43200), 983275200);
913
914        Ok(())
915    }
916
917    #[test]
918    fn test_transition_rule() -> Result<()> {
919        let transition_rule_fixed = TransitionRule::Fixed(LocalTimeType::new(-36000, false, None)?);
920        assert_eq!(transition_rule_fixed.find_local_time_type(0)?.ut_offset(), -36000);
921
922        let transition_rule_dst = TransitionRule::Alternate(AlternateTime::new(
923            LocalTimeType::new(43200, false, Some(b"NZST"))?,
924            LocalTimeType::new(46800, true, Some(b"NZDT"))?,
925            RuleDay::MonthWeekDay(MonthWeekDay::new(10, 1, 0)?),
926            7200,
927            RuleDay::MonthWeekDay(MonthWeekDay::new(3, 3, 0)?),
928            7200,
929        )?);
930
931        assert_eq!(transition_rule_dst.find_local_time_type(953384399)?.ut_offset(), 46800);
932        assert_eq!(transition_rule_dst.find_local_time_type(953384400)?.ut_offset(), 43200);
933        assert_eq!(transition_rule_dst.find_local_time_type(970322399)?.ut_offset(), 43200);
934        assert_eq!(transition_rule_dst.find_local_time_type(970322400)?.ut_offset(), 46800);
935
936        let transition_rule_negative_dst = TransitionRule::Alternate(AlternateTime::new(
937            LocalTimeType::new(3600, false, Some(b"IST"))?,
938            LocalTimeType::new(0, true, Some(b"GMT"))?,
939            RuleDay::MonthWeekDay(MonthWeekDay::new(10, 5, 0)?),
940            7200,
941            RuleDay::MonthWeekDay(MonthWeekDay::new(3, 5, 0)?),
942            3600,
943        )?);
944
945        assert_eq!(transition_rule_negative_dst.find_local_time_type(954032399)?.ut_offset(), 0);
946        assert_eq!(transition_rule_negative_dst.find_local_time_type(954032400)?.ut_offset(), 3600);
947        assert_eq!(transition_rule_negative_dst.find_local_time_type(972781199)?.ut_offset(), 3600);
948        assert_eq!(transition_rule_negative_dst.find_local_time_type(972781200)?.ut_offset(), 0);
949
950        let transition_rule_negative_time_1 = TransitionRule::Alternate(AlternateTime::new(
951            LocalTimeType::new(0, false, None)?,
952            LocalTimeType::new(0, true, None)?,
953            RuleDay::Julian0WithLeap(Julian0WithLeap::new(100)?),
954            0,
955            RuleDay::Julian0WithLeap(Julian0WithLeap::new(101)?),
956            -86500,
957        )?);
958
959        assert!(transition_rule_negative_time_1.find_local_time_type(8639899)?.is_dst());
960        assert!(!transition_rule_negative_time_1.find_local_time_type(8639900)?.is_dst());
961        assert!(!transition_rule_negative_time_1.find_local_time_type(8639999)?.is_dst());
962        assert!(transition_rule_negative_time_1.find_local_time_type(8640000)?.is_dst());
963
964        let transition_rule_negative_time_2 = TransitionRule::Alternate(AlternateTime::new(
965            LocalTimeType::new(-10800, false, Some(b"-03"))?,
966            LocalTimeType::new(-7200, true, Some(b"-02"))?,
967            RuleDay::MonthWeekDay(MonthWeekDay::new(3, 5, 0)?),
968            -7200,
969            RuleDay::MonthWeekDay(MonthWeekDay::new(10, 5, 0)?),
970            -3600,
971        )?);
972
973        assert_eq!(transition_rule_negative_time_2.find_local_time_type(954032399)?.ut_offset(), -10800);
974        assert_eq!(transition_rule_negative_time_2.find_local_time_type(954032400)?.ut_offset(), -7200);
975        assert_eq!(transition_rule_negative_time_2.find_local_time_type(972781199)?.ut_offset(), -7200);
976        assert_eq!(transition_rule_negative_time_2.find_local_time_type(972781200)?.ut_offset(), -10800);
977
978        let transition_rule_all_year_dst = TransitionRule::Alternate(AlternateTime::new(
979            LocalTimeType::new(-18000, false, Some(b"EST"))?,
980            LocalTimeType::new(-14400, true, Some(b"EDT"))?,
981            RuleDay::Julian0WithLeap(Julian0WithLeap::new(0)?),
982            0,
983            RuleDay::Julian1WithoutLeap(Julian1WithoutLeap::new(365)?),
984            90000,
985        )?);
986
987        assert_eq!(transition_rule_all_year_dst.find_local_time_type(946702799)?.ut_offset(), -14400);
988        assert_eq!(transition_rule_all_year_dst.find_local_time_type(946702800)?.ut_offset(), -14400);
989
990        Ok(())
991    }
992
993    #[test]
994    fn test_transition_rule_overflow() -> Result<()> {
995        let transition_rule_1 = TransitionRule::Alternate(AlternateTime::new(
996            LocalTimeType::new(-1, false, None)?,
997            LocalTimeType::new(-1, true, None)?,
998            RuleDay::Julian1WithoutLeap(Julian1WithoutLeap::new(365)?),
999            0,
1000            RuleDay::Julian1WithoutLeap(Julian1WithoutLeap::new(1)?),
1001            0,
1002        )?);
1003
1004        let transition_rule_2 = TransitionRule::Alternate(AlternateTime::new(
1005            LocalTimeType::new(1, false, None)?,
1006            LocalTimeType::new(1, true, None)?,
1007            RuleDay::Julian1WithoutLeap(Julian1WithoutLeap::new(365)?),
1008            0,
1009            RuleDay::Julian1WithoutLeap(Julian1WithoutLeap::new(1)?),
1010            0,
1011        )?);
1012
1013        assert!(matches!(transition_rule_1.find_local_time_type(i64::MIN), Err(OutOfRangeError(_))));
1014        assert!(matches!(transition_rule_2.find_local_time_type(i64::MAX), Err(OutOfRangeError(_))));
1015
1016        Ok(())
1017    }
1018}