icu_calendar/cal/
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;
9//!
10//! let date_iso = Date::try_new_iso(1970, 1, 2)
11//!     .expect("Failed to initialize ISO Date instance.");
12//!
13//! assert_eq!(date_iso.era_year().year, 1970);
14//! assert_eq!(date_iso.month().ordinal, 1);
15//! assert_eq!(date_iso.day_of_month().0, 2);
16//! ```
17
18use crate::calendar_arithmetic::{ArithmeticDate, CalendarArithmetic};
19use crate::error::DateError;
20use crate::{types, Calendar, Date, DateDuration, DateDurationUnit, RangeError};
21use calendrical_calculations::helpers::I32CastError;
22use calendrical_calculations::rata_die::RataDie;
23use tinystr::tinystr;
24
25/// The [ISO-8601 Calendar](https://en.wikipedia.org/wiki/ISO_8601#Dates)
26///
27/// The ISO-8601 Calendar is a standardized solar calendar with twelve months.
28/// It is identical to the [`Gregorian`](super::Gregorian) calendar, except it uses
29/// negative years for years before 1 CE, and may have differing formatting data for a given locale.
30///
31/// This type can be used with [`Date`] to represent dates in this calendar.
32///
33/// # Era codes
34///
35/// This calendar uses a single era: `default`
36
37#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
38#[allow(clippy::exhaustive_structs)] // this type is stable
39pub struct Iso;
40
41#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, PartialOrd, Ord)]
42/// The inner date type used for representing [`Date`]s of [`Iso`]. See [`Date`] and [`Iso`] for more details.
43pub struct IsoDateInner(pub(crate) ArithmeticDate<Iso>);
44
45impl CalendarArithmetic for Iso {
46    type YearInfo = i32;
47
48    fn days_in_provided_month(year: i32, month: u8) -> u8 {
49        match month {
50            4 | 6 | 9 | 11 => 30,
51            2 if Self::provided_year_is_leap(year) => 29,
52            2 => 28,
53            1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
54            _ => 0,
55        }
56    }
57
58    fn months_in_provided_year(_: i32) -> u8 {
59        12
60    }
61
62    fn provided_year_is_leap(year: i32) -> bool {
63        calendrical_calculations::iso::is_leap_year(year)
64    }
65
66    fn last_month_day_in_provided_year(_year: i32) -> (u8, u8) {
67        (12, 31)
68    }
69
70    fn days_in_provided_year(year: i32) -> u16 {
71        if Self::provided_year_is_leap(year) {
72            366
73        } else {
74            365
75        }
76    }
77}
78
79impl crate::cal::scaffold::UnstableSealed for Iso {}
80impl Calendar for Iso {
81    type DateInner = IsoDateInner;
82    type Year = types::EraYear;
83    /// Construct a date from era/month codes and fields
84    fn from_codes(
85        &self,
86        era: Option<&str>,
87        year: i32,
88        month_code: types::MonthCode,
89        day: u8,
90    ) -> Result<Self::DateInner, DateError> {
91        let year = match era {
92            Some("default") | None => year,
93            Some(_) => return Err(DateError::UnknownEra),
94        };
95
96        ArithmeticDate::new_from_codes(self, year, month_code, day).map(IsoDateInner)
97    }
98
99    fn from_rata_die(&self, date: RataDie) -> IsoDateInner {
100        IsoDateInner(match calendrical_calculations::iso::iso_from_fixed(date) {
101            Err(I32CastError::BelowMin) => ArithmeticDate::min_date(),
102            Err(I32CastError::AboveMax) => ArithmeticDate::max_date(),
103            Ok((year, month, day)) => ArithmeticDate::new_unchecked(year, month, day),
104        })
105    }
106
107    fn to_rata_die(&self, date: &IsoDateInner) -> RataDie {
108        calendrical_calculations::iso::fixed_from_iso(date.0.year, date.0.month, date.0.day)
109    }
110
111    fn from_iso(&self, iso: IsoDateInner) -> IsoDateInner {
112        iso
113    }
114
115    fn to_iso(&self, date: &Self::DateInner) -> IsoDateInner {
116        *date
117    }
118
119    fn months_in_year(&self, date: &Self::DateInner) -> u8 {
120        date.0.months_in_year()
121    }
122
123    fn days_in_year(&self, date: &Self::DateInner) -> u16 {
124        date.0.days_in_year()
125    }
126
127    fn days_in_month(&self, date: &Self::DateInner) -> u8 {
128        date.0.days_in_month()
129    }
130
131    fn offset_date(&self, date: &mut Self::DateInner, offset: DateDuration<Self>) {
132        date.0.offset_date(offset, &());
133    }
134
135    #[allow(clippy::field_reassign_with_default)]
136    fn until(
137        &self,
138        date1: &Self::DateInner,
139        date2: &Self::DateInner,
140        _calendar2: &Self,
141        _largest_unit: DateDurationUnit,
142        _smallest_unit: DateDurationUnit,
143    ) -> DateDuration<Self> {
144        date1.0.until(date2.0, _largest_unit, _smallest_unit)
145    }
146
147    fn year_info(&self, date: &Self::DateInner) -> Self::Year {
148        types::EraYear {
149            era_index: Some(0),
150            era: tinystr!(16, "default"),
151            year: self.extended_year(date),
152            ambiguity: types::YearAmbiguity::Unambiguous,
153        }
154    }
155
156    fn extended_year(&self, date: &Self::DateInner) -> i32 {
157        date.0.extended_year()
158    }
159
160    fn is_in_leap_year(&self, date: &Self::DateInner) -> bool {
161        Self::provided_year_is_leap(date.0.year)
162    }
163
164    /// The calendar-specific month represented by `date`
165    fn month(&self, date: &Self::DateInner) -> types::MonthInfo {
166        date.0.month()
167    }
168
169    /// The calendar-specific day-of-month represented by `date`
170    fn day_of_month(&self, date: &Self::DateInner) -> types::DayOfMonth {
171        date.0.day_of_month()
172    }
173
174    fn day_of_year(&self, date: &Self::DateInner) -> types::DayOfYear {
175        date.0.day_of_year()
176    }
177
178    fn debug_name(&self) -> &'static str {
179        "ISO"
180    }
181
182    fn calendar_algorithm(&self) -> Option<crate::preferences::CalendarAlgorithm> {
183        None
184    }
185}
186
187impl Date<Iso> {
188    /// Construct a new ISO date from integers.
189    ///
190    /// ```rust
191    /// use icu::calendar::Date;
192    ///
193    /// let date_iso = Date::try_new_iso(1970, 1, 2)
194    ///     .expect("Failed to initialize ISO Date instance.");
195    ///
196    /// assert_eq!(date_iso.era_year().year, 1970);
197    /// assert_eq!(date_iso.month().ordinal, 1);
198    /// assert_eq!(date_iso.day_of_month().0, 2);
199    /// ```
200    pub fn try_new_iso(year: i32, month: u8, day: u8) -> Result<Date<Iso>, RangeError> {
201        ArithmeticDate::new_from_ordinals(year, month, day)
202            .map(IsoDateInner)
203            .map(|inner| Date::from_raw(inner, Iso))
204    }
205}
206
207impl Iso {
208    /// Construct a new ISO Calendar
209    pub fn new() -> Self {
210        Self
211    }
212
213    pub(crate) fn iso_from_year_day(year: i32, year_day: u16) -> IsoDateInner {
214        let mut month = 1;
215        let mut day = year_day as i32;
216        while month <= 12 {
217            let month_days = Self::days_in_provided_month(year, month) as i32;
218            if day <= month_days {
219                break;
220            } else {
221                debug_assert!(month < 12); // don't try going to month 13
222                day -= month_days;
223                month += 1;
224            }
225        }
226        let day = day as u8; // day <= month_days < u8::MAX
227
228        // month in 1..=12, day <= month_days
229        IsoDateInner(ArithmeticDate::new_unchecked(year, month, day))
230    }
231
232    pub(crate) fn day_of_year(date: IsoDateInner) -> u16 {
233        // Cumulatively how much are dates in each month
234        // offset from "30 days in each month" (in non leap years)
235        let month_offset = [0, 1, -1, 0, 0, 1, 1, 2, 3, 3, 4, 4];
236        #[allow(clippy::indexing_slicing)] // date.0.month in 1..=12
237        let mut offset = month_offset[date.0.month as usize - 1];
238        if Self::provided_year_is_leap(date.0.year) && date.0.month > 2 {
239            // Months after February in a leap year are offset by one less
240            offset += 1;
241        }
242        let prev_month_days = (30 * (date.0.month as i32 - 1) + offset) as u16;
243
244        prev_month_days + date.0.day as u16
245    }
246}
247
248#[cfg(test)]
249mod test {
250    use super::*;
251    use crate::types::Weekday;
252
253    #[test]
254    fn iso_overflow() {
255        #[derive(Debug)]
256        struct TestCase {
257            year: i32,
258            month: u8,
259            day: u8,
260            rd: RataDie,
261            saturating: bool,
262        }
263        // Calculates the max possible year representable using i32::MAX as the RD
264        let max_year = Iso.from_rata_die(RataDie::new(i32::MAX as i64)).0.year;
265
266        // Calculates the minimum possible year representable using i32::MIN as the RD
267        // *Cannot be tested yet due to hard coded date not being available yet (see line 436)
268        let min_year = -5879610;
269
270        let cases = [
271            TestCase {
272                // Earliest date that can be represented before causing a minimum overflow
273                year: min_year,
274                month: 6,
275                day: 22,
276                rd: RataDie::new(i32::MIN as i64),
277                saturating: false,
278            },
279            TestCase {
280                year: min_year,
281                month: 6,
282                day: 23,
283                rd: RataDie::new(i32::MIN as i64 + 1),
284                saturating: false,
285            },
286            TestCase {
287                year: min_year,
288                month: 6,
289                day: 21,
290                rd: RataDie::new(i32::MIN as i64 - 1),
291                saturating: false,
292            },
293            TestCase {
294                year: min_year,
295                month: 12,
296                day: 31,
297                rd: RataDie::new(-2147483456),
298                saturating: false,
299            },
300            TestCase {
301                year: min_year + 1,
302                month: 1,
303                day: 1,
304                rd: RataDie::new(-2147483455),
305                saturating: false,
306            },
307            TestCase {
308                year: max_year,
309                month: 6,
310                day: 11,
311                rd: RataDie::new(i32::MAX as i64 - 30),
312                saturating: false,
313            },
314            TestCase {
315                year: max_year,
316                month: 7,
317                day: 9,
318                rd: RataDie::new(i32::MAX as i64 - 2),
319                saturating: false,
320            },
321            TestCase {
322                year: max_year,
323                month: 7,
324                day: 10,
325                rd: RataDie::new(i32::MAX as i64 - 1),
326                saturating: false,
327            },
328            TestCase {
329                // Latest date that can be represented before causing a maximum overflow
330                year: max_year,
331                month: 7,
332                day: 11,
333                rd: RataDie::new(i32::MAX as i64),
334                saturating: false,
335            },
336            TestCase {
337                year: max_year,
338                month: 7,
339                day: 12,
340                rd: RataDie::new(i32::MAX as i64 + 1),
341                saturating: false,
342            },
343            TestCase {
344                year: i32::MIN,
345                month: 1,
346                day: 2,
347                rd: RataDie::new(-784352296669),
348                saturating: false,
349            },
350            TestCase {
351                year: i32::MIN,
352                month: 1,
353                day: 1,
354                rd: RataDie::new(-784352296670),
355                saturating: false,
356            },
357            TestCase {
358                year: i32::MIN,
359                month: 1,
360                day: 1,
361                rd: RataDie::new(-784352296671),
362                saturating: true,
363            },
364            TestCase {
365                year: i32::MAX,
366                month: 12,
367                day: 30,
368                rd: RataDie::new(784352295938),
369                saturating: false,
370            },
371            TestCase {
372                year: i32::MAX,
373                month: 12,
374                day: 31,
375                rd: RataDie::new(784352295939),
376                saturating: false,
377            },
378            TestCase {
379                year: i32::MAX,
380                month: 12,
381                day: 31,
382                rd: RataDie::new(784352295940),
383                saturating: true,
384            },
385        ];
386
387        for case in cases {
388            let date = Date::try_new_iso(case.year, case.month, case.day).unwrap();
389            if !case.saturating {
390                assert_eq!(date.to_rata_die(), case.rd, "{case:?}");
391            }
392            assert_eq!(Date::from_rata_die(case.rd, Iso), date, "{case:?}");
393        }
394    }
395
396    // Calculates the minimum possible year representable using a large negative fixed date
397    #[test]
398    fn min_year() {
399        assert_eq!(
400            Date::from_rata_die(RataDie::big_negative(), Iso)
401                .year()
402                .era()
403                .unwrap()
404                .year,
405            i32::MIN
406        );
407    }
408
409    #[test]
410    fn test_day_of_week() {
411        // June 23, 2021 is a Wednesday
412        assert_eq!(
413            Date::try_new_iso(2021, 6, 23).unwrap().day_of_week(),
414            Weekday::Wednesday,
415        );
416        // Feb 2, 1983 was a Wednesday
417        assert_eq!(
418            Date::try_new_iso(1983, 2, 2).unwrap().day_of_week(),
419            Weekday::Wednesday,
420        );
421        // Jan 21, 2021 was a Tuesday
422        assert_eq!(
423            Date::try_new_iso(2020, 1, 21).unwrap().day_of_week(),
424            Weekday::Tuesday,
425        );
426    }
427
428    #[test]
429    fn test_day_of_year() {
430        // June 23, 2021 was day 174
431        assert_eq!(Date::try_new_iso(2021, 6, 23).unwrap().day_of_year().0, 174,);
432        // June 23, 2020 was day 175
433        assert_eq!(Date::try_new_iso(2020, 6, 23).unwrap().day_of_year().0, 175,);
434        // Feb 2, 1983 was a Wednesday
435        assert_eq!(Date::try_new_iso(1983, 2, 2).unwrap().day_of_year().0, 33,);
436    }
437
438    fn simple_subtract(a: &Date<Iso>, b: &Date<Iso>) -> DateDuration<Iso> {
439        let a = a.inner();
440        let b = b.inner();
441        DateDuration::new(
442            a.0.year - b.0.year,
443            a.0.month as i32 - b.0.month as i32,
444            0,
445            a.0.day as i32 - b.0.day as i32,
446        )
447    }
448
449    #[test]
450    fn test_offset() {
451        let today = Date::try_new_iso(2021, 6, 23).unwrap();
452        let today_plus_5000 = Date::try_new_iso(2035, 3, 2).unwrap();
453        let offset = today.added(DateDuration::new(0, 0, 0, 5000));
454        assert_eq!(offset, today_plus_5000);
455        let offset = today.added(simple_subtract(&today_plus_5000, &today));
456        assert_eq!(offset, today_plus_5000);
457
458        let today = Date::try_new_iso(2021, 6, 23).unwrap();
459        let today_minus_5000 = Date::try_new_iso(2007, 10, 15).unwrap();
460        let offset = today.added(DateDuration::new(0, 0, 0, -5000));
461        assert_eq!(offset, today_minus_5000);
462        let offset = today.added(simple_subtract(&today_minus_5000, &today));
463        assert_eq!(offset, today_minus_5000);
464    }
465
466    #[test]
467    fn test_offset_at_month_boundary() {
468        let today = Date::try_new_iso(2020, 2, 28).unwrap();
469        let today_plus_2 = Date::try_new_iso(2020, 3, 1).unwrap();
470        let offset = today.added(DateDuration::new(0, 0, 0, 2));
471        assert_eq!(offset, today_plus_2);
472
473        let today = Date::try_new_iso(2020, 2, 28).unwrap();
474        let today_plus_3 = Date::try_new_iso(2020, 3, 2).unwrap();
475        let offset = today.added(DateDuration::new(0, 0, 0, 3));
476        assert_eq!(offset, today_plus_3);
477
478        let today = Date::try_new_iso(2020, 2, 28).unwrap();
479        let today_plus_1 = Date::try_new_iso(2020, 2, 29).unwrap();
480        let offset = today.added(DateDuration::new(0, 0, 0, 1));
481        assert_eq!(offset, today_plus_1);
482
483        let today = Date::try_new_iso(2019, 2, 28).unwrap();
484        let today_plus_2 = Date::try_new_iso(2019, 3, 2).unwrap();
485        let offset = today.added(DateDuration::new(0, 0, 0, 2));
486        assert_eq!(offset, today_plus_2);
487
488        let today = Date::try_new_iso(2019, 2, 28).unwrap();
489        let today_plus_1 = Date::try_new_iso(2019, 3, 1).unwrap();
490        let offset = today.added(DateDuration::new(0, 0, 0, 1));
491        assert_eq!(offset, today_plus_1);
492
493        let today = Date::try_new_iso(2020, 3, 1).unwrap();
494        let today_minus_1 = Date::try_new_iso(2020, 2, 29).unwrap();
495        let offset = today.added(DateDuration::new(0, 0, 0, -1));
496        assert_eq!(offset, today_minus_1);
497    }
498
499    #[test]
500    fn test_offset_handles_negative_month_offset() {
501        let today = Date::try_new_iso(2020, 3, 1).unwrap();
502        let today_minus_2_months = Date::try_new_iso(2020, 1, 1).unwrap();
503        let offset = today.added(DateDuration::new(0, -2, 0, 0));
504        assert_eq!(offset, today_minus_2_months);
505
506        let today = Date::try_new_iso(2020, 3, 1).unwrap();
507        let today_minus_4_months = Date::try_new_iso(2019, 11, 1).unwrap();
508        let offset = today.added(DateDuration::new(0, -4, 0, 0));
509        assert_eq!(offset, today_minus_4_months);
510
511        let today = Date::try_new_iso(2020, 3, 1).unwrap();
512        let today_minus_24_months = Date::try_new_iso(2018, 3, 1).unwrap();
513        let offset = today.added(DateDuration::new(0, -24, 0, 0));
514        assert_eq!(offset, today_minus_24_months);
515
516        let today = Date::try_new_iso(2020, 3, 1).unwrap();
517        let today_minus_27_months = Date::try_new_iso(2017, 12, 1).unwrap();
518        let offset = today.added(DateDuration::new(0, -27, 0, 0));
519        assert_eq!(offset, today_minus_27_months);
520    }
521
522    #[test]
523    fn test_offset_handles_out_of_bound_month_offset() {
524        let today = Date::try_new_iso(2021, 1, 31).unwrap();
525        // since 2021/02/31 isn't a valid date, `offset_date` auto-adjusts by adding 3 days to 2021/02/28
526        let today_plus_1_month = Date::try_new_iso(2021, 3, 3).unwrap();
527        let offset = today.added(DateDuration::new(0, 1, 0, 0));
528        assert_eq!(offset, today_plus_1_month);
529
530        let today = Date::try_new_iso(2021, 1, 31).unwrap();
531        // since 2021/02/31 isn't a valid date, `offset_date` auto-adjusts by adding 3 days to 2021/02/28
532        let today_plus_1_month_1_day = Date::try_new_iso(2021, 3, 4).unwrap();
533        let offset = today.added(DateDuration::new(0, 1, 0, 1));
534        assert_eq!(offset, today_plus_1_month_1_day);
535    }
536
537    #[test]
538    fn test_iso_to_from_rd() {
539        // Reminder: ISO year 0 is Gregorian year 1 BCE.
540        // Year 0 is a leap year due to the 400-year rule.
541        fn check(rd: i64, year: i32, month: u8, day: u8) {
542            let rd = RataDie::new(rd);
543
544            assert_eq!(
545                Date::from_rata_die(rd, Iso),
546                Date::try_new_iso(year, month, day).unwrap(),
547                "RD: {rd:?}"
548            );
549        }
550        check(-1828, -5, 12, 30);
551        check(-1827, -5, 12, 31); // leap year
552        check(-1826, -4, 1, 1);
553        check(-1462, -4, 12, 30);
554        check(-1461, -4, 12, 31);
555        check(-1460, -3, 1, 1);
556        check(-1459, -3, 1, 2);
557        check(-732, -2, 12, 30);
558        check(-731, -2, 12, 31);
559        check(-730, -1, 1, 1);
560        check(-367, -1, 12, 30);
561        check(-366, -1, 12, 31);
562        check(-365, 0, 1, 1); // leap year
563        check(-364, 0, 1, 2);
564        check(-1, 0, 12, 30);
565        check(0, 0, 12, 31);
566        check(1, 1, 1, 1);
567        check(2, 1, 1, 2);
568        check(364, 1, 12, 30);
569        check(365, 1, 12, 31);
570        check(366, 2, 1, 1);
571        check(1459, 4, 12, 29);
572        check(1460, 4, 12, 30);
573        check(1461, 4, 12, 31); // leap year
574        check(1462, 5, 1, 1);
575    }
576}