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
5use crate::cal::abstract_gregorian::{impl_with_abstract_gregorian, GregorianYears};
6use crate::calendar_arithmetic::ArithmeticDate;
7use crate::error::UnknownEraError;
8use crate::{types, Date, DateError, RangeError};
9use tinystr::tinystr;
10
11/// The [ISO-8601 Calendar](https://en.wikipedia.org/wiki/ISO_8601#Dates)
12///
13/// This calendar is identical to the [`Gregorian`](super::Gregorian) calendar,
14/// except that it uses a single `default` era instead of `bce` and `ce`.
15///
16/// This corresponds to the `"iso8601"` [CLDR calendar](https://unicode.org/reports/tr35/#UnicodeCalendarIdentifier).
17///
18/// # Era codes
19///
20/// This calendar uses a single era: `default`
21#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
22#[allow(clippy::exhaustive_structs)] // this type is stable
23pub struct Iso;
24
25impl_with_abstract_gregorian!(crate::cal::Iso, IsoDateInner, IsoEra, _x, IsoEra);
26
27#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
28pub(crate) struct IsoEra;
29
30impl GregorianYears for IsoEra {
31    fn extended_from_era_year(
32        &self,
33        era: Option<&[u8]>,
34        year: i32,
35    ) -> Result<i32, UnknownEraError> {
36        match era {
37            Some(b"default") | None => Ok(year),
38            Some(_) => Err(UnknownEraError),
39        }
40    }
41
42    fn era_year_from_extended(&self, extended_year: i32, _month: u8, _day: u8) -> types::EraYear {
43        types::EraYear {
44            era_index: Some(0),
45            era: tinystr!(16, "default"),
46            year: extended_year,
47            extended_year,
48            ambiguity: types::YearAmbiguity::Unambiguous,
49        }
50    }
51
52    fn debug_name(&self) -> &'static str {
53        "ISO"
54    }
55}
56
57impl Date<Iso> {
58    /// Construct a new ISO date from integers.
59    ///
60    /// ```rust
61    /// use icu::calendar::Date;
62    ///
63    /// let date_iso = Date::try_new_iso(1970, 1, 2)
64    ///     .expect("Failed to initialize ISO Date instance.");
65    ///
66    /// assert_eq!(date_iso.era_year().year, 1970);
67    /// assert_eq!(date_iso.month().ordinal, 1);
68    /// assert_eq!(date_iso.day_of_month().0, 2);
69    /// ```
70    pub fn try_new_iso(year: i32, month: u8, day: u8) -> Result<Date<Iso>, RangeError> {
71        ArithmeticDate::new_gregorian::<IsoEra>(year, month, day)
72            .map(IsoDateInner)
73            .map(|i| Date::from_raw(i, Iso))
74    }
75}
76
77impl Iso {
78    /// Construct a new ISO Calendar
79    pub fn new() -> Self {
80        Self
81    }
82}
83
84#[cfg(test)]
85mod test {
86    use super::*;
87    use crate::types::{DateDuration, RataDie, Weekday};
88    use crate::Calendar;
89
90    #[test]
91    fn iso_overflow() {
92        #[derive(Debug)]
93        struct TestCase {
94            year: i32,
95            month: u8,
96            day: u8,
97            rd: RataDie,
98            saturating: bool,
99        }
100        // Calculates the max possible year representable using i32::MAX as the RD
101        let max_year = Iso.from_rata_die(RataDie::new(i32::MAX as i64)).0.year;
102
103        // Calculates the minimum possible year representable using i32::MIN as the RD
104        // *Cannot be tested yet due to hard coded date not being available yet (see line 436)
105        let min_year = -5879610;
106
107        let cases = [
108            TestCase {
109                // Earliest date that can be represented before causing a minimum overflow
110                year: min_year,
111                month: 6,
112                day: 22,
113                rd: RataDie::new(i32::MIN as i64),
114                saturating: false,
115            },
116            TestCase {
117                year: min_year,
118                month: 6,
119                day: 23,
120                rd: RataDie::new(i32::MIN as i64 + 1),
121                saturating: false,
122            },
123            TestCase {
124                year: min_year,
125                month: 6,
126                day: 21,
127                rd: RataDie::new(i32::MIN as i64 - 1),
128                saturating: false,
129            },
130            TestCase {
131                year: min_year,
132                month: 12,
133                day: 31,
134                rd: RataDie::new(-2147483456),
135                saturating: false,
136            },
137            TestCase {
138                year: min_year + 1,
139                month: 1,
140                day: 1,
141                rd: RataDie::new(-2147483455),
142                saturating: false,
143            },
144            TestCase {
145                year: max_year,
146                month: 6,
147                day: 11,
148                rd: RataDie::new(i32::MAX as i64 - 30),
149                saturating: false,
150            },
151            TestCase {
152                year: max_year,
153                month: 7,
154                day: 9,
155                rd: RataDie::new(i32::MAX as i64 - 2),
156                saturating: false,
157            },
158            TestCase {
159                year: max_year,
160                month: 7,
161                day: 10,
162                rd: RataDie::new(i32::MAX as i64 - 1),
163                saturating: false,
164            },
165            TestCase {
166                // Latest date that can be represented before causing a maximum overflow
167                year: max_year,
168                month: 7,
169                day: 11,
170                rd: RataDie::new(i32::MAX as i64),
171                saturating: false,
172            },
173            TestCase {
174                year: max_year,
175                month: 7,
176                day: 12,
177                rd: RataDie::new(i32::MAX as i64 + 1),
178                saturating: false,
179            },
180            TestCase {
181                year: i32::MIN,
182                month: 1,
183                day: 2,
184                rd: RataDie::new(-784352296669),
185                saturating: false,
186            },
187            TestCase {
188                year: i32::MIN,
189                month: 1,
190                day: 1,
191                rd: RataDie::new(-784352296670),
192                saturating: false,
193            },
194            TestCase {
195                year: i32::MIN,
196                month: 1,
197                day: 1,
198                rd: RataDie::new(-784352296671),
199                saturating: true,
200            },
201            TestCase {
202                year: i32::MAX,
203                month: 12,
204                day: 30,
205                rd: RataDie::new(784352295938),
206                saturating: false,
207            },
208            TestCase {
209                year: i32::MAX,
210                month: 12,
211                day: 31,
212                rd: RataDie::new(784352295939),
213                saturating: false,
214            },
215            TestCase {
216                year: i32::MAX,
217                month: 12,
218                day: 31,
219                rd: RataDie::new(784352295940),
220                saturating: true,
221            },
222        ];
223
224        for case in cases {
225            let date = Date::try_new_iso(case.year, case.month, case.day).unwrap();
226            if !case.saturating {
227                assert_eq!(date.to_rata_die(), case.rd, "{case:?}");
228            }
229            assert_eq!(Date::from_rata_die(case.rd, Iso), date, "{case:?}");
230        }
231    }
232
233    // Calculates the minimum possible year representable using a large negative fixed date
234    #[test]
235    fn min_year() {
236        assert_eq!(
237            Date::from_rata_die(RataDie::big_negative(), Iso)
238                .year()
239                .era()
240                .unwrap()
241                .year,
242            i32::MIN
243        );
244    }
245
246    #[test]
247    fn test_day_of_week() {
248        // June 23, 2021 is a Wednesday
249        assert_eq!(
250            Date::try_new_iso(2021, 6, 23).unwrap().day_of_week(),
251            Weekday::Wednesday,
252        );
253        // Feb 2, 1983 was a Wednesday
254        assert_eq!(
255            Date::try_new_iso(1983, 2, 2).unwrap().day_of_week(),
256            Weekday::Wednesday,
257        );
258        // Jan 21, 2021 was a Tuesday
259        assert_eq!(
260            Date::try_new_iso(2020, 1, 21).unwrap().day_of_week(),
261            Weekday::Tuesday,
262        );
263    }
264
265    #[test]
266    fn test_day_of_year() {
267        // June 23, 2021 was day 174
268        assert_eq!(Date::try_new_iso(2021, 6, 23).unwrap().day_of_year().0, 174,);
269        // June 23, 2020 was day 175
270        assert_eq!(Date::try_new_iso(2020, 6, 23).unwrap().day_of_year().0, 175,);
271        // Feb 2, 1983 was a Wednesday
272        assert_eq!(Date::try_new_iso(1983, 2, 2).unwrap().day_of_year().0, 33,);
273    }
274
275    #[test]
276    fn test_offset() {
277        let today = Date::try_new_iso(2021, 6, 23).unwrap();
278        let today_plus_5000 = Date::try_new_iso(2035, 3, 2).unwrap();
279        let offset = today
280            .try_added_with_options(DateDuration::for_days(5000), Default::default())
281            .unwrap();
282        assert_eq!(offset, today_plus_5000);
283
284        let today = Date::try_new_iso(2021, 6, 23).unwrap();
285        let today_minus_5000 = Date::try_new_iso(2007, 10, 15).unwrap();
286        let offset = today
287            .try_added_with_options(DateDuration::for_days(-5000), Default::default())
288            .unwrap();
289        assert_eq!(offset, today_minus_5000);
290    }
291
292    #[test]
293    fn test_offset_at_month_boundary() {
294        let today = Date::try_new_iso(2020, 2, 28).unwrap();
295        let today_plus_2 = Date::try_new_iso(2020, 3, 1).unwrap();
296        let offset = today
297            .try_added_with_options(DateDuration::for_days(2), Default::default())
298            .unwrap();
299        assert_eq!(offset, today_plus_2);
300
301        let today = Date::try_new_iso(2020, 2, 28).unwrap();
302        let today_plus_3 = Date::try_new_iso(2020, 3, 2).unwrap();
303        let offset = today
304            .try_added_with_options(DateDuration::for_days(3), Default::default())
305            .unwrap();
306        assert_eq!(offset, today_plus_3);
307
308        let today = Date::try_new_iso(2020, 2, 28).unwrap();
309        let today_plus_1 = Date::try_new_iso(2020, 2, 29).unwrap();
310        let offset = today
311            .try_added_with_options(DateDuration::for_days(1), Default::default())
312            .unwrap();
313        assert_eq!(offset, today_plus_1);
314
315        let today = Date::try_new_iso(2019, 2, 28).unwrap();
316        let today_plus_2 = Date::try_new_iso(2019, 3, 2).unwrap();
317        let offset = today
318            .try_added_with_options(DateDuration::for_days(2), Default::default())
319            .unwrap();
320        assert_eq!(offset, today_plus_2);
321
322        let today = Date::try_new_iso(2019, 2, 28).unwrap();
323        let today_plus_1 = Date::try_new_iso(2019, 3, 1).unwrap();
324        let offset = today
325            .try_added_with_options(DateDuration::for_days(1), Default::default())
326            .unwrap();
327        assert_eq!(offset, today_plus_1);
328
329        let today = Date::try_new_iso(2020, 3, 1).unwrap();
330        let today_minus_1 = Date::try_new_iso(2020, 2, 29).unwrap();
331        let offset = today
332            .try_added_with_options(DateDuration::for_days(-1), Default::default())
333            .unwrap();
334        assert_eq!(offset, today_minus_1);
335    }
336
337    #[test]
338    fn test_offset_handles_negative_month_offset() {
339        let today = Date::try_new_iso(2020, 3, 1).unwrap();
340        let today_minus_2_months = Date::try_new_iso(2020, 1, 1).unwrap();
341        let offset = today
342            .try_added_with_options(DateDuration::for_months(-2), Default::default())
343            .unwrap();
344        assert_eq!(offset, today_minus_2_months);
345
346        let today = Date::try_new_iso(2020, 3, 1).unwrap();
347        let today_minus_4_months = Date::try_new_iso(2019, 11, 1).unwrap();
348        let offset = today
349            .try_added_with_options(DateDuration::for_months(-4), Default::default())
350            .unwrap();
351        assert_eq!(offset, today_minus_4_months);
352
353        let today = Date::try_new_iso(2020, 3, 1).unwrap();
354        let today_minus_24_months = Date::try_new_iso(2018, 3, 1).unwrap();
355        let offset = today
356            .try_added_with_options(DateDuration::for_months(-24), Default::default())
357            .unwrap();
358        assert_eq!(offset, today_minus_24_months);
359
360        let today = Date::try_new_iso(2020, 3, 1).unwrap();
361        let today_minus_27_months = Date::try_new_iso(2017, 12, 1).unwrap();
362        let offset = today
363            .try_added_with_options(DateDuration::for_months(-27), Default::default())
364            .unwrap();
365        assert_eq!(offset, today_minus_27_months);
366    }
367
368    #[test]
369    fn test_offset_handles_out_of_bound_month_offset() {
370        let today = Date::try_new_iso(2021, 1, 31).unwrap();
371        // since 2021/02/31 isn't a valid date, `offset_date` auto-adjusts by constraining to the last day in February
372        let today_plus_1_month = Date::try_new_iso(2021, 2, 28).unwrap();
373        let offset = today
374            .try_added_with_options(DateDuration::for_months(1), Default::default())
375            .unwrap();
376        assert_eq!(offset, today_plus_1_month);
377
378        let today = Date::try_new_iso(2021, 1, 31).unwrap();
379        // since 2021/02/31 isn't a valid date, `offset_date` auto-adjusts by constraining to the last day in February
380        // and then adding the days
381        let today_plus_1_month_1_day = Date::try_new_iso(2021, 3, 1).unwrap();
382        let offset = today
383            .try_added_with_options(
384                DateDuration {
385                    months: 1,
386                    days: 1,
387                    ..Default::default()
388                },
389                Default::default(),
390            )
391            .unwrap();
392        assert_eq!(offset, today_plus_1_month_1_day);
393    }
394
395    #[test]
396    fn test_iso_to_from_rd() {
397        // Reminder: ISO year 0 is Gregorian year 1 BCE.
398        // Year 0 is a leap year due to the 400-year rule.
399        fn check(rd: i64, year: i32, month: u8, day: u8) {
400            let rd = RataDie::new(rd);
401
402            assert_eq!(
403                Date::from_rata_die(rd, Iso),
404                Date::try_new_iso(year, month, day).unwrap(),
405                "RD: {rd:?}"
406            );
407        }
408        check(-1828, -5, 12, 30);
409        check(-1827, -5, 12, 31); // leap year
410        check(-1826, -4, 1, 1);
411        check(-1462, -4, 12, 30);
412        check(-1461, -4, 12, 31);
413        check(-1460, -3, 1, 1);
414        check(-1459, -3, 1, 2);
415        check(-732, -2, 12, 30);
416        check(-731, -2, 12, 31);
417        check(-730, -1, 1, 1);
418        check(-367, -1, 12, 30);
419        check(-366, -1, 12, 31);
420        check(-365, 0, 1, 1); // leap year
421        check(-364, 0, 1, 2);
422        check(-1, 0, 12, 30);
423        check(0, 0, 12, 31);
424        check(1, 1, 1, 1);
425        check(2, 1, 1, 2);
426        check(364, 1, 12, 30);
427        check(365, 1, 12, 31);
428        check(366, 2, 1, 1);
429        check(1459, 4, 12, 29);
430        check(1460, 4, 12, 30);
431        check(1461, 4, 12, 31); // leap year
432        check(1462, 5, 1, 1);
433    }
434}