icu_calendar/
gregorian.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 Gregorian calendar.
6//!
7//! ```rust
8//! use icu::calendar::{gregorian::Gregorian, 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//! let date_gregorian = Date::new_from_iso(date_iso, Gregorian);
14//!
15//! // `DateTime` type
16//! let datetime_iso = DateTime::try_new_iso_datetime(1970, 1, 2, 13, 1, 0)
17//!     .expect("Failed to initialize ISO DateTime instance.");
18//! let datetime_gregorian = DateTime::new_from_iso(datetime_iso, Gregorian);
19//!
20//! // `Date` checks
21//! assert_eq!(date_gregorian.year().number, 1970);
22//! assert_eq!(date_gregorian.month().ordinal, 1);
23//! assert_eq!(date_gregorian.day_of_month().0, 2);
24//!
25//! // `DateTime` checks
26//! assert_eq!(datetime_gregorian.date.year().number, 1970);
27//! assert_eq!(datetime_gregorian.date.month().ordinal, 1);
28//! assert_eq!(datetime_gregorian.date.day_of_month().0, 2);
29//! assert_eq!(datetime_gregorian.time.hour.number(), 13);
30//! assert_eq!(datetime_gregorian.time.minute.number(), 1);
31//! assert_eq!(datetime_gregorian.time.second.number(), 0);
32//! ```
33
34use crate::any_calendar::AnyCalendarKind;
35use crate::calendar_arithmetic::ArithmeticDate;
36use crate::iso::{Iso, IsoDateInner};
37use crate::{types, Calendar, CalendarError, Date, DateDuration, DateDurationUnit, DateTime, Time};
38use tinystr::tinystr;
39
40/// The Gregorian Calendar
41///
42/// The [Gregorian calendar] is a solar calendar used by most of the world, with twelve months.
43///
44/// This type can be used with [`Date`] or [`DateTime`] to represent dates in this calendar.
45///
46/// [Gregorian calendar]: https://en.wikipedia.org/wiki/Gregorian_calendar
47///
48/// # Era codes
49///
50/// This calendar supports two era codes: `"bce"`, and `"ce"`, corresponding to the BCE and CE eras
51#[derive(Copy, Clone, Debug, Default)]
52#[allow(clippy::exhaustive_structs)] // this type is stable
53pub struct Gregorian;
54
55#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, PartialOrd, Ord)]
56/// The inner date type used for representing [`Date`]s of [`Gregorian`]. See [`Date`] and [`Gregorian`] for more details.
57pub struct GregorianDateInner(IsoDateInner);
58
59impl Calendar for Gregorian {
60    type DateInner = GregorianDateInner;
61    fn date_from_codes(
62        &self,
63        era: types::Era,
64        year: i32,
65        month_code: types::MonthCode,
66        day: u8,
67    ) -> Result<Self::DateInner, CalendarError> {
68        let year = if era.0 == tinystr!(16, "ce") {
69            if year <= 0 {
70                return Err(CalendarError::OutOfRange);
71            }
72            year
73        } else if era.0 == tinystr!(16, "bce") {
74            if year <= 0 {
75                return Err(CalendarError::OutOfRange);
76            }
77            1 - year
78        } else {
79            return Err(CalendarError::UnknownEra(era.0, self.debug_name()));
80        };
81
82        ArithmeticDate::new_from_codes(self, year, month_code, day)
83            .map(IsoDateInner)
84            .map(GregorianDateInner)
85    }
86
87    fn date_from_iso(&self, iso: Date<Iso>) -> GregorianDateInner {
88        GregorianDateInner(*iso.inner())
89    }
90
91    fn date_to_iso(&self, date: &Self::DateInner) -> Date<Iso> {
92        Date::from_raw(date.0, Iso)
93    }
94
95    fn months_in_year(&self, date: &Self::DateInner) -> u8 {
96        Iso.months_in_year(&date.0)
97    }
98
99    fn days_in_year(&self, date: &Self::DateInner) -> u16 {
100        Iso.days_in_year(&date.0)
101    }
102
103    fn days_in_month(&self, date: &Self::DateInner) -> u8 {
104        Iso.days_in_month(&date.0)
105    }
106
107    fn offset_date(&self, date: &mut Self::DateInner, offset: DateDuration<Self>) {
108        Iso.offset_date(&mut date.0, offset.cast_unit())
109    }
110
111    #[allow(clippy::field_reassign_with_default)] // it's more clear this way
112    fn until(
113        &self,
114        date1: &Self::DateInner,
115        date2: &Self::DateInner,
116        _calendar2: &Self,
117        largest_unit: DateDurationUnit,
118        smallest_unit: DateDurationUnit,
119    ) -> DateDuration<Self> {
120        Iso.until(&date1.0, &date2.0, &Iso, largest_unit, smallest_unit)
121            .cast_unit()
122    }
123
124    /// The calendar-specific year represented by `date`
125    fn year(&self, date: &Self::DateInner) -> types::FormattableYear {
126        year_as_gregorian(date.0 .0.year)
127    }
128
129    fn is_in_leap_year(&self, date: &Self::DateInner) -> bool {
130        Iso.is_in_leap_year(&date.0)
131    }
132
133    /// The calendar-specific month represented by `date`
134    fn month(&self, date: &Self::DateInner) -> types::FormattableMonth {
135        Iso.month(&date.0)
136    }
137
138    /// The calendar-specific day-of-month represented by `date`
139    fn day_of_month(&self, date: &Self::DateInner) -> types::DayOfMonth {
140        Iso.day_of_month(&date.0)
141    }
142
143    /// Information of the day of the year
144    fn day_of_year_info(&self, date: &Self::DateInner) -> types::DayOfYearInfo {
145        let prev_year = date.0 .0.year.saturating_sub(1);
146        let next_year = date.0 .0.year.saturating_add(1);
147        types::DayOfYearInfo {
148            day_of_year: Iso::day_of_year(date.0),
149            days_in_year: Iso::days_in_year_direct(date.0 .0.year),
150            prev_year: year_as_gregorian(prev_year),
151            days_in_prev_year: Iso::days_in_year_direct(prev_year),
152            next_year: year_as_gregorian(next_year),
153        }
154    }
155
156    fn debug_name(&self) -> &'static str {
157        "Gregorian"
158    }
159
160    fn any_calendar_kind(&self) -> Option<AnyCalendarKind> {
161        Some(AnyCalendarKind::Gregorian)
162    }
163}
164
165impl Date<Gregorian> {
166    /// Construct a new Gregorian Date.
167    ///
168    /// Years are specified as ISO years.
169    ///
170    /// ```rust
171    /// use icu::calendar::Date;
172    ///
173    /// // Conversion from ISO to Gregorian
174    /// let date_gregorian = Date::try_new_gregorian_date(1970, 1, 2)
175    ///     .expect("Failed to initialize Gregorian Date instance.");
176    ///
177    /// assert_eq!(date_gregorian.year().number, 1970);
178    /// assert_eq!(date_gregorian.month().ordinal, 1);
179    /// assert_eq!(date_gregorian.day_of_month().0, 2);
180    /// ```
181    pub fn try_new_gregorian_date(
182        year: i32,
183        month: u8,
184        day: u8,
185    ) -> Result<Date<Gregorian>, CalendarError> {
186        Date::try_new_iso_date(year, month, day).map(|d| Date::new_from_iso(d, Gregorian))
187    }
188}
189
190impl DateTime<Gregorian> {
191    /// Construct a new Gregorian datetime from integers.
192    ///
193    /// Years are specified as ISO years.
194    ///
195    /// ```rust
196    /// use icu::calendar::DateTime;
197    ///
198    /// let datetime_gregorian =
199    ///     DateTime::try_new_gregorian_datetime(1970, 1, 2, 13, 1, 0)
200    ///         .expect("Failed to initialize Gregorian DateTime instance.");
201    ///
202    /// assert_eq!(datetime_gregorian.date.year().number, 1970);
203    /// assert_eq!(datetime_gregorian.date.month().ordinal, 1);
204    /// assert_eq!(datetime_gregorian.date.day_of_month().0, 2);
205    /// assert_eq!(datetime_gregorian.time.hour.number(), 13);
206    /// assert_eq!(datetime_gregorian.time.minute.number(), 1);
207    /// assert_eq!(datetime_gregorian.time.second.number(), 0);
208    /// ```
209    pub fn try_new_gregorian_datetime(
210        year: i32,
211        month: u8,
212        day: u8,
213        hour: u8,
214        minute: u8,
215        second: u8,
216    ) -> Result<DateTime<Gregorian>, CalendarError> {
217        Ok(DateTime {
218            date: Date::try_new_gregorian_date(year, month, day)?,
219            time: Time::try_new(hour, minute, second, 0)?,
220        })
221    }
222}
223
224pub(crate) fn year_as_gregorian(year: i32) -> types::FormattableYear {
225    if year > 0 {
226        types::FormattableYear {
227            era: types::Era(tinystr!(16, "ce")),
228            number: year,
229            cyclic: None,
230            related_iso: None,
231        }
232    } else {
233        types::FormattableYear {
234            era: types::Era(tinystr!(16, "bce")),
235            number: 1_i32.saturating_sub(year),
236            cyclic: None,
237            related_iso: None,
238        }
239    }
240}
241
242#[cfg(test)]
243mod test {
244    use calendrical_calculations::rata_die::RataDie;
245
246    use super::*;
247    use types::Era;
248
249    #[test]
250    fn day_of_year_info_max() {
251        #[derive(Debug)]
252        struct MaxCase {
253            year: i32,
254            month: u8,
255            day: u8,
256            next_era_year: i32,
257            era: &'static str,
258        }
259        let cases = [
260            MaxCase {
261                year: i32::MAX,
262                month: 7,
263                day: 11,
264                next_era_year: i32::MAX,
265                era: "ce",
266            },
267            MaxCase {
268                year: i32::MAX,
269                month: 7,
270                day: 12,
271                next_era_year: i32::MAX,
272                era: "ce",
273            },
274            MaxCase {
275                year: i32::MAX,
276                month: 8,
277                day: 10,
278                next_era_year: i32::MAX,
279                era: "ce",
280            },
281            MaxCase {
282                year: i32::MAX - 1,
283                month: 7,
284                day: 11,
285                next_era_year: i32::MAX,
286                era: "ce",
287            },
288            MaxCase {
289                year: -2,
290                month: 1,
291                day: 1,
292                next_era_year: 2,
293                era: "bce",
294            },
295            MaxCase {
296                year: -1,
297                month: 1,
298                day: 1,
299                next_era_year: 1,
300                era: "bce",
301            },
302            MaxCase {
303                year: 0,
304                month: 1,
305                day: 1,
306                next_era_year: 1,
307                era: "ce",
308            },
309            MaxCase {
310                year: 1,
311                month: 1,
312                day: 1,
313                next_era_year: 2,
314                era: "ce",
315            },
316            MaxCase {
317                year: 2000,
318                month: 6,
319                day: 15,
320                next_era_year: 2001,
321                era: "ce",
322            },
323            MaxCase {
324                year: 2020,
325                month: 12,
326                day: 31,
327                next_era_year: 2021,
328                era: "ce",
329            },
330        ];
331
332        for case in cases {
333            let date = Date::try_new_gregorian_date(case.year, case.month, case.day).unwrap();
334
335            assert_eq!(
336                Calendar::day_of_year_info(&Gregorian, &date.inner)
337                    .next_year
338                    .number,
339                case.next_era_year,
340                "{case:?}",
341            );
342            assert_eq!(
343                Calendar::day_of_year_info(&Gregorian, &date.inner)
344                    .next_year
345                    .era
346                    .0,
347                case.era,
348                "{case:?}",
349            );
350        }
351    }
352
353    #[derive(Debug)]
354    struct TestCase {
355        fixed_date: RataDie,
356        iso_year: i32,
357        iso_month: u8,
358        iso_day: u8,
359        expected_year: i32,
360        expected_era: Era,
361        expected_month: u32,
362        expected_day: u32,
363    }
364
365    fn check_test_case(case: TestCase) {
366        let iso_from_fixed: Date<Iso> = Iso::iso_from_fixed(case.fixed_date);
367        let greg_date_from_fixed: Date<Gregorian> = Date::new_from_iso(iso_from_fixed, Gregorian);
368        assert_eq!(greg_date_from_fixed.year().number, case.expected_year,
369            "Failed year check from fixed: {case:?}\nISO: {iso_from_fixed:?}\nGreg: {greg_date_from_fixed:?}");
370        assert_eq!(greg_date_from_fixed.year().era, case.expected_era,
371            "Failed era check from fixed: {case:?}\nISO: {iso_from_fixed:?}\nGreg: {greg_date_from_fixed:?}");
372        assert_eq!(greg_date_from_fixed.month().ordinal, case.expected_month,
373            "Failed month check from fixed: {case:?}\nISO: {iso_from_fixed:?}\nGreg: {greg_date_from_fixed:?}");
374        assert_eq!(greg_date_from_fixed.day_of_month().0, case.expected_day,
375            "Failed day check from fixed: {case:?}\nISO: {iso_from_fixed:?}\nGreg: {greg_date_from_fixed:?}");
376
377        let iso_date_man: Date<Iso> =
378            Date::try_new_iso_date(case.iso_year, case.iso_month, case.iso_day)
379                .expect("Failed to initialize ISO date for {case:?}");
380        let greg_date_man: Date<Gregorian> = Date::new_from_iso(iso_date_man, Gregorian);
381        assert_eq!(iso_from_fixed, iso_date_man,
382            "ISO from fixed not equal to ISO generated from manually-input ymd\nCase: {case:?}\nFixed: {iso_from_fixed:?}\nMan: {iso_date_man:?}");
383        assert_eq!(greg_date_from_fixed, greg_date_man,
384            "Greg. date from fixed not equal to Greg. generated from manually-input ymd\nCase: {case:?}\nFixed: {greg_date_from_fixed:?}\nMan: {greg_date_man:?}");
385    }
386
387    #[test]
388    fn test_gregorian_ce() {
389        // Tests that the Gregorian calendar gives the correct expected
390        // day, month, and year for positive years (AD/CE/gregory era)
391
392        let cases = [
393            TestCase {
394                fixed_date: RataDie::new(1),
395                iso_year: 1,
396                iso_month: 1,
397                iso_day: 1,
398                expected_year: 1,
399                expected_era: Era(tinystr!(16, "ce")),
400                expected_month: 1,
401                expected_day: 1,
402            },
403            TestCase {
404                fixed_date: RataDie::new(181),
405                iso_year: 1,
406                iso_month: 6,
407                iso_day: 30,
408                expected_year: 1,
409                expected_era: Era(tinystr!(16, "ce")),
410                expected_month: 6,
411                expected_day: 30,
412            },
413            TestCase {
414                fixed_date: RataDie::new(1155),
415                iso_year: 4,
416                iso_month: 2,
417                iso_day: 29,
418                expected_year: 4,
419                expected_era: Era(tinystr!(16, "ce")),
420                expected_month: 2,
421                expected_day: 29,
422            },
423            TestCase {
424                fixed_date: RataDie::new(1344),
425                iso_year: 4,
426                iso_month: 9,
427                iso_day: 5,
428                expected_year: 4,
429                expected_era: Era(tinystr!(16, "ce")),
430                expected_month: 9,
431                expected_day: 5,
432            },
433            TestCase {
434                fixed_date: RataDie::new(36219),
435                iso_year: 100,
436                iso_month: 3,
437                iso_day: 1,
438                expected_year: 100,
439                expected_era: Era(tinystr!(16, "ce")),
440                expected_month: 3,
441                expected_day: 1,
442            },
443        ];
444
445        for case in cases {
446            check_test_case(case);
447        }
448    }
449
450    #[test]
451    fn day_of_year_info_min() {
452        #[derive(Debug)]
453        struct MinCase {
454            year: i32,
455            month: u8,
456            day: u8,
457            prev_era_year: i32,
458            era: &'static str,
459        }
460        let cases = [
461            MinCase {
462                year: i32::MIN + 4,
463                month: 1,
464                day: 1,
465                prev_era_year: i32::MAX - 1,
466                era: "bce",
467            },
468            MinCase {
469                year: i32::MIN + 3,
470                month: 12,
471                day: 31,
472                prev_era_year: i32::MAX,
473                era: "bce",
474            },
475            MinCase {
476                year: i32::MIN + 2,
477                month: 2,
478                day: 2,
479                prev_era_year: i32::MAX,
480                era: "bce",
481            },
482            MinCase {
483                year: i32::MIN + 1,
484                month: 1,
485                day: 1,
486                prev_era_year: i32::MAX,
487                era: "bce",
488            },
489            MinCase {
490                year: i32::MIN,
491                month: 1,
492                day: 1,
493                prev_era_year: i32::MAX,
494                era: "bce",
495            },
496            MinCase {
497                year: 3,
498                month: 1,
499                day: 1,
500                prev_era_year: 2,
501                era: "ce",
502            },
503            MinCase {
504                year: 2,
505                month: 1,
506                day: 1,
507                prev_era_year: 1,
508                era: "ce",
509            },
510            MinCase {
511                year: 1,
512                month: 1,
513                day: 1,
514                prev_era_year: 1,
515                era: "bce",
516            },
517            MinCase {
518                year: 0,
519                month: 1,
520                day: 1,
521                prev_era_year: 2,
522                era: "bce",
523            },
524            MinCase {
525                year: -2000,
526                month: 6,
527                day: 15,
528                prev_era_year: 2002,
529                era: "bce",
530            },
531            MinCase {
532                year: 2020,
533                month: 12,
534                day: 31,
535                prev_era_year: 2019,
536                era: "ce",
537            },
538        ];
539
540        for case in cases {
541            let date = Date::try_new_gregorian_date(case.year, case.month, case.day).unwrap();
542
543            assert_eq!(
544                Calendar::day_of_year_info(&Gregorian, &date.inner)
545                    .prev_year
546                    .number,
547                case.prev_era_year,
548                "{case:?}",
549            );
550            assert_eq!(
551                Calendar::day_of_year_info(&Gregorian, &date.inner)
552                    .prev_year
553                    .era
554                    .0,
555                case.era,
556                "{case:?}",
557            );
558        }
559    }
560
561    #[test]
562    fn test_gregorian_bce() {
563        // Tests that the Gregorian calendar gives the correct expected
564        // day, month, and year for negative years (BC/BCE/pre-gregory era)
565
566        let cases = [
567            TestCase {
568                fixed_date: RataDie::new(0),
569                iso_year: 0,
570                iso_month: 12,
571                iso_day: 31,
572                expected_year: 1,
573                expected_era: Era(tinystr!(16, "bce")),
574                expected_month: 12,
575                expected_day: 31,
576            },
577            TestCase {
578                fixed_date: RataDie::new(-365), // This is a leap year
579                iso_year: 0,
580                iso_month: 1,
581                iso_day: 1,
582                expected_year: 1,
583                expected_era: Era(tinystr!(16, "bce")),
584                expected_month: 1,
585                expected_day: 1,
586            },
587            TestCase {
588                fixed_date: RataDie::new(-366),
589                iso_year: -1,
590                iso_month: 12,
591                iso_day: 31,
592                expected_year: 2,
593                expected_era: Era(tinystr!(16, "bce")),
594                expected_month: 12,
595                expected_day: 31,
596            },
597            TestCase {
598                fixed_date: RataDie::new(-1461),
599                iso_year: -4,
600                iso_month: 12,
601                iso_day: 31,
602                expected_year: 5,
603                expected_era: Era(tinystr!(16, "bce")),
604                expected_month: 12,
605                expected_day: 31,
606            },
607            TestCase {
608                fixed_date: RataDie::new(-1826),
609                iso_year: -4,
610                iso_month: 1,
611                iso_day: 1,
612                expected_year: 5,
613                expected_era: Era(tinystr!(16, "bce")),
614                expected_month: 1,
615                expected_day: 1,
616            },
617        ];
618
619        for case in cases {
620            check_test_case(case);
621        }
622    }
623
624    #[test]
625    fn check_gregorian_directionality() {
626        // Tests that for a large range of fixed dates, if a fixed date
627        // is less than another, the corresponding YMD should also be less
628        // than the other, without exception.
629        for i in -100..100 {
630            for j in -100..100 {
631                let iso_i: Date<Iso> = Iso::iso_from_fixed(RataDie::new(i));
632                let iso_j: Date<Iso> = Iso::iso_from_fixed(RataDie::new(j));
633
634                let greg_i: Date<Gregorian> = Date::new_from_iso(iso_i, Gregorian);
635                let greg_j: Date<Gregorian> = Date::new_from_iso(iso_j, Gregorian);
636
637                assert_eq!(
638                    i.cmp(&j),
639                    iso_i.cmp(&iso_j),
640                    "ISO directionality inconsistent with directionality for i: {i}, j: {j}"
641                );
642                assert_eq!(
643                    i.cmp(&j),
644                    greg_i.cmp(&greg_j),
645                    "Gregorian directionality inconsistent with directionality for i: {i}, j: {j}"
646                );
647            }
648        }
649    }
650}