icu_calendar/
iso.rs

1// This file is part of ICU4X. For terms of use, please see the file
2// called LICENSE at the top level of the ICU4X source tree
3// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ).
4
5//! This module contains types and implementations for the ISO calendar.
6//!
7//! ```rust
8//! use icu::calendar::{Date, DateTime};
9//!
10//! // `Date` type
11//! let date_iso = Date::try_new_iso_date(1970, 1, 2)
12//!     .expect("Failed to initialize ISO Date instance.");
13//!
14//! // `DateTime` type
15//! let datetime_iso = DateTime::try_new_iso_datetime(1970, 1, 2, 13, 1, 0)
16//!     .expect("Failed to initialize ISO DateTime instance.");
17//!
18//! // `Date` checks
19//! assert_eq!(date_iso.year().number, 1970);
20//! assert_eq!(date_iso.month().ordinal, 1);
21//! assert_eq!(date_iso.day_of_month().0, 2);
22//!
23//! // `DateTime` type
24//! assert_eq!(datetime_iso.date.year().number, 1970);
25//! assert_eq!(datetime_iso.date.month().ordinal, 1);
26//! assert_eq!(datetime_iso.date.day_of_month().0, 2);
27//! assert_eq!(datetime_iso.time.hour.number(), 13);
28//! assert_eq!(datetime_iso.time.minute.number(), 1);
29//! assert_eq!(datetime_iso.time.second.number(), 0);
30//! ```
31
32use 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/// The [ISO Calendar]
40///
41/// The [ISO Calendar] is a standardized solar calendar with twelve months.
42/// It is identical to the Gregorian calendar, except it uses negative years for years before 1 CE,
43/// and may have differing formatting data for a given locale.
44///
45/// This type can be used with [`Date`] or [`DateTime`] to represent dates in this calendar.
46///
47/// [ISO Calendar]: https://en.wikipedia.org/wiki/ISO_8601#Dates
48///
49/// # Era codes
50///
51/// This calendar supports one era, `"default"`
52
53#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
54#[allow(clippy::exhaustive_structs)] // this type is stable
55pub struct Iso;
56
57#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, PartialOrd, Ord)]
58/// The inner date type used for representing [`Date`]s of [`Iso`]. See [`Date`] and [`Iso`] for more details.
59pub 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    /// Construct a date from era/month codes and fields
98    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        // For the purposes of the calculation here, Monday is 0, Sunday is 6
134        // ISO has Monday=1, Sunday=7, which we transform in the last step
135
136        // The days of the week are the same every 400 years
137        // so we normalize to the nearest multiple of 400
138        let years_since_400 = date.0.year.rem_euclid(400);
139        debug_assert!(years_since_400 >= 0); // rem_euclid returns positive numbers
140        let years_since_400 = years_since_400 as u32;
141        let leap_years_since_400 = years_since_400 / 4 - years_since_400 / 100;
142        // The number of days to the current year
143        // Can never cause an overflow because years_since_400 has a maximum value of 399.
144        let days_to_current_year = 365 * years_since_400 + leap_years_since_400;
145        // The weekday offset from January 1 this year and January 1 2000
146        let year_offset = days_to_current_year % 7;
147
148        // Corresponding months from
149        // https://en.wikipedia.org/wiki/Determination_of_the_day_of_the_week#Corresponding_months
150        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; // Saturday
174        let day_offset = (january_1_2000 + year_offset + month_offset + date.0.day as u32) % 7;
175
176        // We calculated in a zero-indexed fashion, but ISO specifies one-indexed
177        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    /// The calendar-specific year represented by `date`
197    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    /// The calendar-specific month represented by `date`
206    fn month(&self, date: &Self::DateInner) -> types::FormattableMonth {
207        date.0.month()
208    }
209
210    /// The calendar-specific day-of-month represented by `date`
211    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    /// Construct a new ISO date from integers.
238    ///
239    /// ```rust
240    /// use icu::calendar::Date;
241    ///
242    /// let date_iso = Date::try_new_iso_date(1970, 1, 2)
243    ///     .expect("Failed to initialize ISO Date instance.");
244    ///
245    /// assert_eq!(date_iso.year().number, 1970);
246    /// assert_eq!(date_iso.month().ordinal, 1);
247    /// assert_eq!(date_iso.day_of_month().0, 2);
248    /// ```
249    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    /// Constructs an ISO date representing the UNIX epoch on January 1, 1970.
256    pub fn unix_epoch() -> Self {
257        Date::from_raw(IsoDateInner(ArithmeticDate::new_unchecked(1970, 1, 1)), Iso)
258    }
259}
260
261impl DateTime<Iso> {
262    /// Construct a new ISO datetime from integers.
263    ///
264    /// ```rust
265    /// use icu::calendar::DateTime;
266    ///
267    /// let datetime_iso = DateTime::try_new_iso_datetime(1970, 1, 2, 13, 1, 0)
268    ///     .expect("Failed to initialize ISO DateTime instance.");
269    ///
270    /// assert_eq!(datetime_iso.date.year().number, 1970);
271    /// assert_eq!(datetime_iso.date.month().ordinal, 1);
272    /// assert_eq!(datetime_iso.date.day_of_month().0, 2);
273    /// assert_eq!(datetime_iso.time.hour.number(), 13);
274    /// assert_eq!(datetime_iso.time.minute.number(), 1);
275    /// assert_eq!(datetime_iso.time.second.number(), 0);
276    /// ```
277    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    /// Constructs an ISO datetime representing the UNIX epoch on January 1, 1970
292    /// at midnight.
293    pub fn local_unix_epoch() -> Self {
294        DateTime {
295            date: Date::unix_epoch(),
296            time: Time::midnight(),
297        }
298    }
299
300    /// Minute count representation of calendars starting from 00:00:00 on Jan 1st, 1970.
301    ///
302    /// ```rust
303    /// use icu::calendar::DateTime;
304    ///
305    /// let today = DateTime::try_new_iso_datetime(2020, 2, 29, 0, 0, 0).unwrap();
306    ///
307    /// assert_eq!(today.minutes_since_local_unix_epoch(), 26382240);
308    /// assert_eq!(
309    ///     DateTime::from_minutes_since_local_unix_epoch(26382240),
310    ///     today
311    /// );
312    ///
313    /// let today = DateTime::try_new_iso_datetime(1970, 1, 1, 0, 0, 0).unwrap();
314    ///
315    /// assert_eq!(today.minutes_since_local_unix_epoch(), 0);
316    /// assert_eq!(DateTime::from_minutes_since_local_unix_epoch(0), today);
317    /// ```
318    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    /// Convert minute count since 00:00:00 on Jan 1st, 1970 to ISO Date.
330    ///
331    /// # Examples
332    ///
333    /// ```rust
334    /// use icu::calendar::DateTime;
335    ///
336    /// // After Unix Epoch
337    /// let today = DateTime::try_new_iso_datetime(2020, 2, 29, 0, 0, 0).unwrap();
338    ///
339    /// assert_eq!(today.minutes_since_local_unix_epoch(), 26382240);
340    /// assert_eq!(
341    ///     DateTime::from_minutes_since_local_unix_epoch(26382240),
342    ///     today
343    /// );
344    ///
345    /// // Unix Epoch
346    /// let today = DateTime::try_new_iso_datetime(1970, 1, 1, 0, 0, 0).unwrap();
347    ///
348    /// assert_eq!(today.minutes_since_local_unix_epoch(), 0);
349    /// assert_eq!(DateTime::from_minutes_since_local_unix_epoch(0), today);
350    ///
351    /// // Before Unix Epoch
352    /// let today = DateTime::try_new_iso_datetime(1967, 4, 6, 20, 40, 0).unwrap();
353    ///
354    /// assert_eq!(today.minutes_since_local_unix_epoch(), -1440200);
355    /// assert_eq!(
356    ///     DateTime::from_minutes_since_local_unix_epoch(-1440200),
357    ///     today
358    /// );
359    /// ```
360    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    /// Construct a new ISO Calendar
371    pub fn new() -> Self {
372        Self
373    }
374
375    /// Count the number of days in a given month/year combo
376    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    // Fixed is day count representation of calendars starting from Jan 1st of year 1.
394    // The fixed calculations algorithms are from the Calendrical Calculations book.
395    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); // don't try going to month 13
408                day -= month_days;
409                month += 1;
410            }
411        }
412        let day = day as u8; // day <= month_days < u8::MAX
413
414        #[allow(clippy::unwrap_used)] // month in 1..=12, day <= month_days
415        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)] // valid day and month
428        Date::try_new_iso_date(year, month, day).unwrap()
429    }
430
431    pub(crate) fn day_of_year(date: IsoDateInner) -> u16 {
432        // Cumulatively how much are dates in each month
433        // offset from "30 days in each month" (in non leap years)
434        let month_offset = [0, 1, -1, 0, 0, 1, 1, 2, 3, 3, 4, 4];
435        #[allow(clippy::indexing_slicing)] // date.0.month in 1..=12
436        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            // Months after February in a leap year are offset by one less
439            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    /// Wrap the year in the appropriate era code
447    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        // Calculates the max possible year representable using i32::MAX as the fixed date
492        let max_year = Iso::iso_from_fixed(RataDie::new(i32::MAX as i64))
493            .year()
494            .number;
495
496        // Calculates the minimum possible year representable using i32::MIN as the fixed date
497        // *Cannot be tested yet due to hard coded date not being available yet (see line 436)
498        let min_year = -5879610;
499
500        let cases = [
501            TestCase {
502                // Earliest date that can be represented before causing a minimum overflow
503                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                // Latest date that can be represented before causing a maximum overflow
560                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    // Calculates the minimum possible year representable using a large negative fixed date
627    #[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        // June 23, 2021 is a Wednesday
638        assert_eq!(
639            Date::try_new_iso_date(2021, 6, 23).unwrap().day_of_week(),
640            IsoWeekday::Wednesday,
641        );
642        // Feb 2, 1983 was a Wednesday
643        assert_eq!(
644            Date::try_new_iso_date(1983, 2, 2).unwrap().day_of_week(),
645            IsoWeekday::Wednesday,
646        );
647        // Jan 21, 2021 was a Tuesday
648        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        // June 23, 2021 was day 174
657        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        // June 23, 2020 was day 175
665        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        // Feb 2, 1983 was a Wednesday
673        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        // since 2021/02/31 isn't a valid date, `offset_date` auto-adjusts by adding 3 days to 2021/02/28
770        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        // since 2021/02/31 isn't a valid date, `offset_date` auto-adjusts by adding 3 days to 2021/02/28
776        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        // Reminder: ISO year 0 is Gregorian year 1 BCE.
784        // Year 0 is a leap year due to the 400-year rule.
785        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); // leap year
796        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); // leap year
807        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); // leap year
818        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}