tz/timezone/
rule.rs

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