1use crate::any_calendar::AnyCalendarKind;
33use crate::calendar_arithmetic::{ArithmeticDate, CalendarArithmetic};
34use crate::{types, Calendar, CalendarError, Date, DateDuration, DateDurationUnit, DateTime, Time};
35use calendrical_calculations::helpers::{i64_to_saturated_i32, I32CastError};
36use calendrical_calculations::rata_die::RataDie;
37use tinystr::tinystr;
38
39#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
54#[allow(clippy::exhaustive_structs)] pub struct Iso;
56
57#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, PartialOrd, Ord)]
58pub struct IsoDateInner(pub(crate) ArithmeticDate<Iso>);
60
61impl CalendarArithmetic for Iso {
62 type YearInfo = ();
63
64 fn month_days(year: i32, month: u8, _data: ()) -> u8 {
65 match month {
66 4 | 6 | 9 | 11 => 30,
67 2 if Self::is_leap_year(year, ()) => 29,
68 2 => 28,
69 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
70 _ => 0,
71 }
72 }
73
74 fn months_for_every_year(_: i32, _data: ()) -> u8 {
75 12
76 }
77
78 fn is_leap_year(year: i32, _data: ()) -> bool {
79 calendrical_calculations::iso::is_leap_year(year)
80 }
81
82 fn last_month_day_in_year(_year: i32, _data: ()) -> (u8, u8) {
83 (12, 31)
84 }
85
86 fn days_in_provided_year(year: i32, _data: ()) -> u16 {
87 if Self::is_leap_year(year, ()) {
88 366
89 } else {
90 365
91 }
92 }
93}
94
95impl Calendar for Iso {
96 type DateInner = IsoDateInner;
97 fn date_from_codes(
99 &self,
100 era: types::Era,
101 year: i32,
102 month_code: types::MonthCode,
103 day: u8,
104 ) -> Result<Self::DateInner, CalendarError> {
105 if era.0 != tinystr!(16, "default") {
106 return Err(CalendarError::UnknownEra(era.0, self.debug_name()));
107 }
108
109 ArithmeticDate::new_from_codes(self, year, month_code, day).map(IsoDateInner)
110 }
111
112 fn date_from_iso(&self, iso: Date<Iso>) -> IsoDateInner {
113 *iso.inner()
114 }
115
116 fn date_to_iso(&self, date: &Self::DateInner) -> Date<Iso> {
117 Date::from_raw(*date, Iso)
118 }
119
120 fn months_in_year(&self, date: &Self::DateInner) -> u8 {
121 date.0.months_in_year()
122 }
123
124 fn days_in_year(&self, date: &Self::DateInner) -> u16 {
125 date.0.days_in_year()
126 }
127
128 fn days_in_month(&self, date: &Self::DateInner) -> u8 {
129 date.0.days_in_month()
130 }
131
132 fn day_of_week(&self, date: &Self::DateInner) -> types::IsoWeekday {
133 let years_since_400 = date.0.year.rem_euclid(400);
139 debug_assert!(years_since_400 >= 0); let years_since_400 = years_since_400 as u32;
141 let leap_years_since_400 = years_since_400 / 4 - years_since_400 / 100;
142 let days_to_current_year = 365 * years_since_400 + leap_years_since_400;
145 let year_offset = days_to_current_year % 7;
147
148 let month_offset = if Self::is_leap_year(date.0.year, ()) {
151 match date.0.month {
152 10 => 0,
153 5 => 1,
154 2 | 8 => 2,
155 3 | 11 => 3,
156 6 => 4,
157 9 | 12 => 5,
158 1 | 4 | 7 => 6,
159 _ => unreachable!(),
160 }
161 } else {
162 match date.0.month {
163 1 | 10 => 0,
164 5 => 1,
165 8 => 2,
166 2 | 3 | 11 => 3,
167 6 => 4,
168 9 | 12 => 5,
169 4 | 7 => 6,
170 _ => unreachable!(),
171 }
172 };
173 let january_1_2000 = 5; let day_offset = (january_1_2000 + year_offset + month_offset + date.0.day as u32) % 7;
175
176 types::IsoWeekday::from((day_offset + 1) as usize)
178 }
179
180 fn offset_date(&self, date: &mut Self::DateInner, offset: DateDuration<Self>) {
181 date.0.offset_date(offset, &());
182 }
183
184 #[allow(clippy::field_reassign_with_default)]
185 fn until(
186 &self,
187 date1: &Self::DateInner,
188 date2: &Self::DateInner,
189 _calendar2: &Self,
190 _largest_unit: DateDurationUnit,
191 _smallest_unit: DateDurationUnit,
192 ) -> DateDuration<Self> {
193 date1.0.until(date2.0, _largest_unit, _smallest_unit)
194 }
195
196 fn year(&self, date: &Self::DateInner) -> types::FormattableYear {
198 Self::year_as_iso(date.0.year)
199 }
200
201 fn is_in_leap_year(&self, date: &Self::DateInner) -> bool {
202 Self::is_leap_year(date.0.year, ())
203 }
204
205 fn month(&self, date: &Self::DateInner) -> types::FormattableMonth {
207 date.0.month()
208 }
209
210 fn day_of_month(&self, date: &Self::DateInner) -> types::DayOfMonth {
212 date.0.day_of_month()
213 }
214
215 fn day_of_year_info(&self, date: &Self::DateInner) -> types::DayOfYearInfo {
216 let prev_year = date.0.year.saturating_sub(1);
217 let next_year = date.0.year.saturating_add(1);
218 types::DayOfYearInfo {
219 day_of_year: date.0.day_of_year(),
220 days_in_year: date.0.days_in_year(),
221 prev_year: Self::year_as_iso(prev_year),
222 days_in_prev_year: Iso::days_in_year_direct(prev_year),
223 next_year: Self::year_as_iso(next_year),
224 }
225 }
226
227 fn debug_name(&self) -> &'static str {
228 "ISO"
229 }
230
231 fn any_calendar_kind(&self) -> Option<AnyCalendarKind> {
232 Some(AnyCalendarKind::Iso)
233 }
234}
235
236impl Date<Iso> {
237 pub fn try_new_iso_date(year: i32, month: u8, day: u8) -> Result<Date<Iso>, CalendarError> {
250 ArithmeticDate::new_from_ordinals(year, month, day)
251 .map(IsoDateInner)
252 .map(|inner| Date::from_raw(inner, Iso))
253 }
254
255 pub fn unix_epoch() -> Self {
257 Date::from_raw(IsoDateInner(ArithmeticDate::new_unchecked(1970, 1, 1)), Iso)
258 }
259}
260
261impl DateTime<Iso> {
262 pub fn try_new_iso_datetime(
278 year: i32,
279 month: u8,
280 day: u8,
281 hour: u8,
282 minute: u8,
283 second: u8,
284 ) -> Result<DateTime<Iso>, CalendarError> {
285 Ok(DateTime {
286 date: Date::try_new_iso_date(year, month, day)?,
287 time: Time::try_new(hour, minute, second, 0)?,
288 })
289 }
290
291 pub fn local_unix_epoch() -> Self {
294 DateTime {
295 date: Date::unix_epoch(),
296 time: Time::midnight(),
297 }
298 }
299
300 pub fn minutes_since_local_unix_epoch(&self) -> i32 {
319 let minutes_a_hour = 60;
320 let hours_a_day = 24;
321 let minutes_a_day = minutes_a_hour * hours_a_day;
322 let unix_epoch = Iso::fixed_from_iso(Date::unix_epoch().inner);
323 let result = (Iso::fixed_from_iso(*self.date.inner()) - unix_epoch) * minutes_a_day
324 + i64::from(self.time.hour.number()) * minutes_a_hour
325 + i64::from(self.time.minute.number());
326 i64_to_saturated_i32(result)
327 }
328
329 pub fn from_minutes_since_local_unix_epoch(minute: i32) -> DateTime<Iso> {
361 let (time, extra_days) = Time::from_minute_with_remainder_days(minute);
362 let unix_epoch = Date::unix_epoch();
363 let unix_epoch_days = Iso::fixed_from_iso(unix_epoch.inner);
364 let date = Iso::iso_from_fixed(unix_epoch_days + extra_days as i64);
365 DateTime { date, time }
366 }
367}
368
369impl Iso {
370 pub fn new() -> Self {
372 Self
373 }
374
375 fn days_in_month(year: i32, month: u8) -> u8 {
377 match month {
378 4 | 6 | 9 | 11 => 30,
379 2 if Self::is_leap_year(year, ()) => 29,
380 2 => 28,
381 _ => 31,
382 }
383 }
384
385 pub(crate) fn days_in_year_direct(year: i32) -> u16 {
386 if Self::is_leap_year(year, ()) {
387 366
388 } else {
389 365
390 }
391 }
392
393 pub(crate) fn fixed_from_iso(date: IsoDateInner) -> RataDie {
396 calendrical_calculations::iso::fixed_from_iso(date.0.year, date.0.month, date.0.day)
397 }
398
399 pub(crate) fn iso_from_year_day(year: i32, year_day: u16) -> Date<Iso> {
400 let mut month = 1;
401 let mut day = year_day as i32;
402 while month <= 12 {
403 let month_days = Self::days_in_month(year, month) as i32;
404 if day <= month_days {
405 break;
406 } else {
407 debug_assert!(month < 12); day -= month_days;
409 month += 1;
410 }
411 }
412 let day = day as u8; #[allow(clippy::unwrap_used)] Date::try_new_iso_date(year, month, day).unwrap()
416 }
417 pub(crate) fn iso_from_fixed(date: RataDie) -> Date<Iso> {
418 let (year, month, day) = match calendrical_calculations::iso::iso_from_fixed(date) {
419 Err(I32CastError::BelowMin) => {
420 return Date::from_raw(IsoDateInner(ArithmeticDate::min_date()), Iso)
421 }
422 Err(I32CastError::AboveMax) => {
423 return Date::from_raw(IsoDateInner(ArithmeticDate::max_date()), Iso)
424 }
425 Ok(ymd) => ymd,
426 };
427 #[allow(clippy::unwrap_used)] Date::try_new_iso_date(year, month, day).unwrap()
429 }
430
431 pub(crate) fn day_of_year(date: IsoDateInner) -> u16 {
432 let month_offset = [0, 1, -1, 0, 0, 1, 1, 2, 3, 3, 4, 4];
435 #[allow(clippy::indexing_slicing)] let mut offset = month_offset[date.0.month as usize - 1];
437 if Self::is_leap_year(date.0.year, ()) && date.0.month > 2 {
438 offset += 1;
440 }
441 let prev_month_days = (30 * (date.0.month as i32 - 1) + offset) as u16;
442
443 prev_month_days + date.0.day as u16
444 }
445
446 fn year_as_iso(year: i32) -> types::FormattableYear {
448 types::FormattableYear {
449 era: types::Era(tinystr!(16, "default")),
450 number: year,
451 cyclic: None,
452 related_iso: None,
453 }
454 }
455}
456
457impl IsoDateInner {
458 pub(crate) fn jan_1(year: i32) -> Self {
459 Self(ArithmeticDate::new_unchecked(year, 1, 1))
460 }
461 pub(crate) fn dec_31(year: i32) -> Self {
462 Self(ArithmeticDate::new_unchecked(year, 12, 1))
463 }
464}
465
466impl From<&'_ IsoDateInner> for crate::provider::EraStartDate {
467 fn from(other: &'_ IsoDateInner) -> Self {
468 Self {
469 year: other.0.year,
470 month: other.0.month,
471 day: other.0.day,
472 }
473 }
474}
475
476#[cfg(test)]
477mod test {
478 use super::*;
479 use crate::types::IsoWeekday;
480
481 #[test]
482 fn iso_overflow() {
483 #[derive(Debug)]
484 struct TestCase {
485 year: i32,
486 month: u8,
487 day: u8,
488 fixed: RataDie,
489 saturating: bool,
490 }
491 let max_year = Iso::iso_from_fixed(RataDie::new(i32::MAX as i64))
493 .year()
494 .number;
495
496 let min_year = -5879610;
499
500 let cases = [
501 TestCase {
502 year: min_year,
504 month: 6,
505 day: 22,
506 fixed: RataDie::new(i32::MIN as i64),
507 saturating: false,
508 },
509 TestCase {
510 year: min_year,
511 month: 6,
512 day: 23,
513 fixed: RataDie::new(i32::MIN as i64 + 1),
514 saturating: false,
515 },
516 TestCase {
517 year: min_year,
518 month: 6,
519 day: 21,
520 fixed: RataDie::new(i32::MIN as i64 - 1),
521 saturating: false,
522 },
523 TestCase {
524 year: min_year,
525 month: 12,
526 day: 31,
527 fixed: RataDie::new(-2147483456),
528 saturating: false,
529 },
530 TestCase {
531 year: min_year + 1,
532 month: 1,
533 day: 1,
534 fixed: RataDie::new(-2147483455),
535 saturating: false,
536 },
537 TestCase {
538 year: max_year,
539 month: 6,
540 day: 11,
541 fixed: RataDie::new(i32::MAX as i64 - 30),
542 saturating: false,
543 },
544 TestCase {
545 year: max_year,
546 month: 7,
547 day: 9,
548 fixed: RataDie::new(i32::MAX as i64 - 2),
549 saturating: false,
550 },
551 TestCase {
552 year: max_year,
553 month: 7,
554 day: 10,
555 fixed: RataDie::new(i32::MAX as i64 - 1),
556 saturating: false,
557 },
558 TestCase {
559 year: max_year,
561 month: 7,
562 day: 11,
563 fixed: RataDie::new(i32::MAX as i64),
564 saturating: false,
565 },
566 TestCase {
567 year: max_year,
568 month: 7,
569 day: 12,
570 fixed: RataDie::new(i32::MAX as i64 + 1),
571 saturating: false,
572 },
573 TestCase {
574 year: i32::MIN,
575 month: 1,
576 day: 2,
577 fixed: RataDie::new(-784352296669),
578 saturating: false,
579 },
580 TestCase {
581 year: i32::MIN,
582 month: 1,
583 day: 1,
584 fixed: RataDie::new(-784352296670),
585 saturating: false,
586 },
587 TestCase {
588 year: i32::MIN,
589 month: 1,
590 day: 1,
591 fixed: RataDie::new(-784352296671),
592 saturating: true,
593 },
594 TestCase {
595 year: i32::MAX,
596 month: 12,
597 day: 30,
598 fixed: RataDie::new(784352295938),
599 saturating: false,
600 },
601 TestCase {
602 year: i32::MAX,
603 month: 12,
604 day: 31,
605 fixed: RataDie::new(784352295939),
606 saturating: false,
607 },
608 TestCase {
609 year: i32::MAX,
610 month: 12,
611 day: 31,
612 fixed: RataDie::new(784352295940),
613 saturating: true,
614 },
615 ];
616
617 for case in cases {
618 let date = Date::try_new_iso_date(case.year, case.month, case.day).unwrap();
619 if !case.saturating {
620 assert_eq!(Iso::fixed_from_iso(date.inner), case.fixed, "{case:?}");
621 }
622 assert_eq!(Iso::iso_from_fixed(case.fixed), date, "{case:?}");
623 }
624 }
625
626 #[test]
628 fn min_year() {
629 assert_eq!(
630 Iso::iso_from_fixed(RataDie::big_negative()).year().number,
631 i32::MIN
632 );
633 }
634
635 #[test]
636 fn test_day_of_week() {
637 assert_eq!(
639 Date::try_new_iso_date(2021, 6, 23).unwrap().day_of_week(),
640 IsoWeekday::Wednesday,
641 );
642 assert_eq!(
644 Date::try_new_iso_date(1983, 2, 2).unwrap().day_of_week(),
645 IsoWeekday::Wednesday,
646 );
647 assert_eq!(
649 Date::try_new_iso_date(2020, 1, 21).unwrap().day_of_week(),
650 IsoWeekday::Tuesday,
651 );
652 }
653
654 #[test]
655 fn test_day_of_year() {
656 assert_eq!(
658 Date::try_new_iso_date(2021, 6, 23)
659 .unwrap()
660 .day_of_year_info()
661 .day_of_year,
662 174,
663 );
664 assert_eq!(
666 Date::try_new_iso_date(2020, 6, 23)
667 .unwrap()
668 .day_of_year_info()
669 .day_of_year,
670 175,
671 );
672 assert_eq!(
674 Date::try_new_iso_date(1983, 2, 2)
675 .unwrap()
676 .day_of_year_info()
677 .day_of_year,
678 33,
679 );
680 }
681
682 fn simple_subtract(a: &Date<Iso>, b: &Date<Iso>) -> DateDuration<Iso> {
683 let a = a.inner();
684 let b = b.inner();
685 DateDuration::new(
686 a.0.year - b.0.year,
687 a.0.month as i32 - b.0.month as i32,
688 0,
689 a.0.day as i32 - b.0.day as i32,
690 )
691 }
692
693 #[test]
694 fn test_offset() {
695 let today = Date::try_new_iso_date(2021, 6, 23).unwrap();
696 let today_plus_5000 = Date::try_new_iso_date(2035, 3, 2).unwrap();
697 let offset = today.added(DateDuration::new(0, 0, 0, 5000));
698 assert_eq!(offset, today_plus_5000);
699 let offset = today.added(simple_subtract(&today_plus_5000, &today));
700 assert_eq!(offset, today_plus_5000);
701
702 let today = Date::try_new_iso_date(2021, 6, 23).unwrap();
703 let today_minus_5000 = Date::try_new_iso_date(2007, 10, 15).unwrap();
704 let offset = today.added(DateDuration::new(0, 0, 0, -5000));
705 assert_eq!(offset, today_minus_5000);
706 let offset = today.added(simple_subtract(&today_minus_5000, &today));
707 assert_eq!(offset, today_minus_5000);
708 }
709
710 #[test]
711 fn test_offset_at_month_boundary() {
712 let today = Date::try_new_iso_date(2020, 2, 28).unwrap();
713 let today_plus_2 = Date::try_new_iso_date(2020, 3, 1).unwrap();
714 let offset = today.added(DateDuration::new(0, 0, 0, 2));
715 assert_eq!(offset, today_plus_2);
716
717 let today = Date::try_new_iso_date(2020, 2, 28).unwrap();
718 let today_plus_3 = Date::try_new_iso_date(2020, 3, 2).unwrap();
719 let offset = today.added(DateDuration::new(0, 0, 0, 3));
720 assert_eq!(offset, today_plus_3);
721
722 let today = Date::try_new_iso_date(2020, 2, 28).unwrap();
723 let today_plus_1 = Date::try_new_iso_date(2020, 2, 29).unwrap();
724 let offset = today.added(DateDuration::new(0, 0, 0, 1));
725 assert_eq!(offset, today_plus_1);
726
727 let today = Date::try_new_iso_date(2019, 2, 28).unwrap();
728 let today_plus_2 = Date::try_new_iso_date(2019, 3, 2).unwrap();
729 let offset = today.added(DateDuration::new(0, 0, 0, 2));
730 assert_eq!(offset, today_plus_2);
731
732 let today = Date::try_new_iso_date(2019, 2, 28).unwrap();
733 let today_plus_1 = Date::try_new_iso_date(2019, 3, 1).unwrap();
734 let offset = today.added(DateDuration::new(0, 0, 0, 1));
735 assert_eq!(offset, today_plus_1);
736
737 let today = Date::try_new_iso_date(2020, 3, 1).unwrap();
738 let today_minus_1 = Date::try_new_iso_date(2020, 2, 29).unwrap();
739 let offset = today.added(DateDuration::new(0, 0, 0, -1));
740 assert_eq!(offset, today_minus_1);
741 }
742
743 #[test]
744 fn test_offset_handles_negative_month_offset() {
745 let today = Date::try_new_iso_date(2020, 3, 1).unwrap();
746 let today_minus_2_months = Date::try_new_iso_date(2020, 1, 1).unwrap();
747 let offset = today.added(DateDuration::new(0, -2, 0, 0));
748 assert_eq!(offset, today_minus_2_months);
749
750 let today = Date::try_new_iso_date(2020, 3, 1).unwrap();
751 let today_minus_4_months = Date::try_new_iso_date(2019, 11, 1).unwrap();
752 let offset = today.added(DateDuration::new(0, -4, 0, 0));
753 assert_eq!(offset, today_minus_4_months);
754
755 let today = Date::try_new_iso_date(2020, 3, 1).unwrap();
756 let today_minus_24_months = Date::try_new_iso_date(2018, 3, 1).unwrap();
757 let offset = today.added(DateDuration::new(0, -24, 0, 0));
758 assert_eq!(offset, today_minus_24_months);
759
760 let today = Date::try_new_iso_date(2020, 3, 1).unwrap();
761 let today_minus_27_months = Date::try_new_iso_date(2017, 12, 1).unwrap();
762 let offset = today.added(DateDuration::new(0, -27, 0, 0));
763 assert_eq!(offset, today_minus_27_months);
764 }
765
766 #[test]
767 fn test_offset_handles_out_of_bound_month_offset() {
768 let today = Date::try_new_iso_date(2021, 1, 31).unwrap();
769 let today_plus_1_month = Date::try_new_iso_date(2021, 3, 3).unwrap();
771 let offset = today.added(DateDuration::new(0, 1, 0, 0));
772 assert_eq!(offset, today_plus_1_month);
773
774 let today = Date::try_new_iso_date(2021, 1, 31).unwrap();
775 let today_plus_1_month_1_day = Date::try_new_iso_date(2021, 3, 4).unwrap();
777 let offset = today.added(DateDuration::new(0, 1, 0, 1));
778 assert_eq!(offset, today_plus_1_month_1_day);
779 }
780
781 #[test]
782 fn test_iso_to_from_fixed() {
783 fn check(fixed: i64, year: i32, month: u8, day: u8) {
786 let fixed = RataDie::new(fixed);
787
788 assert_eq!(
789 Iso::iso_from_fixed(fixed),
790 Date::try_new_iso_date(year, month, day).unwrap(),
791 "fixed: {fixed:?}"
792 );
793 }
794 check(-1828, -5, 12, 30);
795 check(-1827, -5, 12, 31); check(-1826, -4, 1, 1);
797 check(-1462, -4, 12, 30);
798 check(-1461, -4, 12, 31);
799 check(-1460, -3, 1, 1);
800 check(-1459, -3, 1, 2);
801 check(-732, -2, 12, 30);
802 check(-731, -2, 12, 31);
803 check(-730, -1, 1, 1);
804 check(-367, -1, 12, 30);
805 check(-366, -1, 12, 31);
806 check(-365, 0, 1, 1); check(-364, 0, 1, 2);
808 check(-1, 0, 12, 30);
809 check(0, 0, 12, 31);
810 check(1, 1, 1, 1);
811 check(2, 1, 1, 2);
812 check(364, 1, 12, 30);
813 check(365, 1, 12, 31);
814 check(366, 2, 1, 1);
815 check(1459, 4, 12, 29);
816 check(1460, 4, 12, 30);
817 check(1461, 4, 12, 31); check(1462, 5, 1, 1);
819 }
820
821 #[test]
822 fn test_from_minutes_since_local_unix_epoch() {
823 fn check(minutes: i32, year: i32, month: u8, day: u8, hour: u8, minute: u8) {
824 let today = DateTime::try_new_iso_datetime(year, month, day, hour, minute, 0).unwrap();
825 assert_eq!(today.minutes_since_local_unix_epoch(), minutes);
826 assert_eq!(
827 DateTime::from_minutes_since_local_unix_epoch(minutes),
828 today
829 );
830 }
831
832 check(-1441, 1969, 12, 30, 23, 59);
833 check(-1440, 1969, 12, 31, 0, 0);
834 check(-1439, 1969, 12, 31, 0, 1);
835 check(-2879, 1969, 12, 30, 0, 1);
836 }
837}