1use crate::constants::*;
4use crate::timezone::*;
5
6#[derive(Debug, PartialEq, Eq)]
8struct JulianDayCheckInfos {
9 start_normal_year_offset: i64,
11 end_normal_year_offset: i64,
13 start_leap_year_offset: i64,
15 end_leap_year_offset: i64,
17}
18
19#[derive(Debug, PartialEq, Eq)]
21struct MonthWeekDayCheckInfos {
22 start_normal_year_offset_range: (i64, i64),
24 end_normal_year_offset_range: (i64, i64),
26 start_leap_year_offset_range: (i64, i64),
28 end_leap_year_offset_range: (i64, i64),
30}
31
32#[derive(Debug, Copy, Clone, Eq, PartialEq)]
34pub struct Julian1WithoutLeap(u16);
35
36impl Julian1WithoutLeap {
37 #[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 #[inline]
50 #[cfg_attr(feature = "const", const_fn::const_fn)]
51 pub fn get(&self) -> u16 {
52 self.0
53 }
54
55 #[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 #[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#[derive(Debug, Copy, Clone, Eq, PartialEq)]
93pub struct Julian0WithLeap(u16);
94
95impl Julian0WithLeap {
96 #[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 #[inline]
109 #[cfg_attr(feature = "const", const_fn::const_fn)]
110 pub fn get(&self) -> u16 {
111 self.0
112 }
113
114 #[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 #[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#[derive(Debug, Copy, Clone, Eq, PartialEq)]
155pub struct MonthWeekDay {
156 month: u8,
158 week: u8,
160 week_day: u8,
162}
163
164impl MonthWeekDay {
165 #[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 #[inline]
186 #[cfg_attr(feature = "const", const_fn::const_fn)]
187 pub fn month(&self) -> u8 {
188 self.month
189 }
190
191 #[inline]
193 #[cfg_attr(feature = "const", const_fn::const_fn)]
194 pub fn week(&self) -> u8 {
195 self.week
196 }
197
198 #[inline]
200 #[cfg_attr(feature = "const", const_fn::const_fn)]
201 pub fn week_day(&self) -> u8 {
202 self.week_day
203 }
204
205 #[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 #[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#[derive(Debug, Copy, Clone, Eq, PartialEq)]
279pub enum RuleDay {
280 Julian1WithoutLeap(Julian1WithoutLeap),
282 Julian0WithLeap(Julian0WithLeap),
284 MonthWeekDay(MonthWeekDay),
286}
287
288impl RuleDay {
289 #[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 #[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#[derive(Debug, Copy, Clone, Eq, PartialEq)]
317pub struct AlternateTime {
318 std: LocalTimeType,
320 dst: LocalTimeType,
322 dst_start: RuleDay,
324 dst_start_time: i32,
326 dst_end: RuleDay,
328 dst_end_time: i32,
330}
331
332impl AlternateTime {
333 #[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 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 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 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 #[inline]
370 #[cfg_attr(feature = "const", const_fn::const_fn)]
371 pub fn std(&self) -> &LocalTimeType {
372 &self.std
373 }
374
375 #[inline]
377 #[cfg_attr(feature = "const", const_fn::const_fn)]
378 pub fn dst(&self) -> &LocalTimeType {
379 &self.dst
380 }
381
382 #[inline]
384 #[cfg_attr(feature = "const", const_fn::const_fn)]
385 pub fn dst_start(&self) -> &RuleDay {
386 &self.dst_start
387 }
388
389 #[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 #[inline]
398 #[cfg_attr(feature = "const", const_fn::const_fn)]
399 pub fn dst_end(&self) -> &RuleDay {
400 &self.dst_end
401 }
402
403 #[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 #[cfg_attr(feature = "const", const_fn::const_fn)]
412 fn find_local_time_type(&self, unix_time: i64) -> Result<&LocalTimeType, OutOfRangeError> {
413 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 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 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#[derive(Debug, Copy, Clone, Eq, PartialEq)]
488pub enum TransitionRule {
489 Fixed(LocalTimeType),
491 Alternate(AlternateTime),
493}
494
495impl TransitionRule {
496 #[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#[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 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#[cfg_attr(feature = "const", const_fn::const_fn)]
556fn check_two_julian_days(check_infos_1: JulianDayCheckInfos, check_infos_2: JulianDayCheckInfos) -> bool {
557 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 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#[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 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#[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 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 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 let (diff_week_min, diff_week_max) = match (week_before, week_after) {
673 (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, };
678
679 (diff_week_min * DAYS_PER_WEEK, diff_week_max * DAYS_PER_WEEK)
680 } else {
681 let n = (week_day_after - week_day_before).rem_euclid(DAYS_PER_WEEK); 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 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, Ordering::Greater => (n + DAYS_PER_WEEK * (3 - week_before), n + DAYS_PER_WEEK * (4 - week_before)),
700 }
701 }
702 _ => const_panic!(), }
704 } else {
705 match (week_before, week_after) {
707 (1..=4, 1..=4) => {
708 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, 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, }
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 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}