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