icu_calendar/cal/
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::{cal::Gregorian, Date};
9//!
10//! let date_iso = Date::try_new_iso(1970, 1, 2)
11//!     .expect("Failed to initialize ISO Date instance.");
12//! let date_gregorian = Date::new_from_iso(date_iso, Gregorian);
13//!
14//! assert_eq!(date_gregorian.era_year().year, 1970);
15//! assert_eq!(date_gregorian.month().ordinal, 1);
16//! assert_eq!(date_gregorian.day_of_month().0, 2);
17//! ```
18
19use crate::cal::iso::{Iso, IsoDateInner};
20use crate::calendar_arithmetic::ArithmeticDate;
21use crate::error::{year_check, DateError};
22use crate::{types, Calendar, Date, DateDuration, DateDurationUnit, RangeError};
23use calendrical_calculations::rata_die::RataDie;
24use tinystr::tinystr;
25
26/// The [(proleptic) Gregorian Calendar](https://en.wikipedia.org/wiki/Proleptic_Gregorian_calendar)
27///
28/// The Gregorian calendar is a solar calendar used by most of the world, with twelve months.
29///
30/// This type can be used with [`Date`] to represent dates in this calendar.
31///
32/// # Era codes
33///
34/// This calendar uses two era codes: `bce` (alias `bc`), and `ce` (alias `ad`), corresponding to the BCE and CE eras.
35///
36/// # Month codes
37///
38/// This calendar supports 12 solar month codes (`"M01" - "M12"`)
39#[derive(Copy, Clone, Debug, Default)]
40#[allow(clippy::exhaustive_structs)] // this type is stable
41pub struct Gregorian;
42
43#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, PartialOrd, Ord)]
44/// The inner date type used for representing [`Date`]s of [`Gregorian`]. See [`Date`] and [`Gregorian`] for more details.
45pub struct GregorianDateInner(pub(crate) IsoDateInner);
46
47impl crate::cal::scaffold::UnstableSealed for Gregorian {}
48impl Calendar for Gregorian {
49    type DateInner = GregorianDateInner;
50    type Year = types::EraYear;
51    fn from_codes(
52        &self,
53        era: Option<&str>,
54        year: i32,
55        month_code: types::MonthCode,
56        day: u8,
57    ) -> Result<Self::DateInner, DateError> {
58        let year = match era {
59            Some("bce" | "bc") | None => 1 - year_check(year, 1..)?,
60            Some("ad" | "ce") => year_check(year, 1..)?,
61            Some(_) => return Err(DateError::UnknownEra),
62        };
63
64        ArithmeticDate::new_from_codes(self, year, month_code, day)
65            .map(IsoDateInner)
66            .map(GregorianDateInner)
67    }
68
69    fn from_rata_die(&self, rd: RataDie) -> Self::DateInner {
70        GregorianDateInner(Iso.from_rata_die(rd))
71    }
72
73    fn to_rata_die(&self, date: &Self::DateInner) -> RataDie {
74        Iso.to_rata_die(&date.0)
75    }
76
77    fn from_iso(&self, iso: IsoDateInner) -> GregorianDateInner {
78        GregorianDateInner(iso)
79    }
80
81    fn to_iso(&self, date: &Self::DateInner) -> IsoDateInner {
82        date.0
83    }
84
85    fn months_in_year(&self, date: &Self::DateInner) -> u8 {
86        Iso.months_in_year(&date.0)
87    }
88
89    fn days_in_year(&self, date: &Self::DateInner) -> u16 {
90        Iso.days_in_year(&date.0)
91    }
92
93    fn days_in_month(&self, date: &Self::DateInner) -> u8 {
94        Iso.days_in_month(&date.0)
95    }
96
97    fn offset_date(&self, date: &mut Self::DateInner, offset: DateDuration<Self>) {
98        Iso.offset_date(&mut date.0, offset.cast_unit())
99    }
100
101    #[allow(clippy::field_reassign_with_default)] // it's more clear this way
102    fn until(
103        &self,
104        date1: &Self::DateInner,
105        date2: &Self::DateInner,
106        _calendar2: &Self,
107        largest_unit: DateDurationUnit,
108        smallest_unit: DateDurationUnit,
109    ) -> DateDuration<Self> {
110        Iso.until(&date1.0, &date2.0, &Iso, largest_unit, smallest_unit)
111            .cast_unit()
112    }
113    /// The calendar-specific year represented by `date`
114    fn year_info(&self, date: &Self::DateInner) -> Self::Year {
115        let extended_year = self.extended_year(date);
116        if extended_year > 0 {
117            types::EraYear {
118                era: tinystr!(16, "ce"),
119                era_index: Some(1),
120                year: extended_year,
121                ambiguity: match extended_year {
122                    ..=999 => types::YearAmbiguity::EraAndCenturyRequired,
123                    1000..=1949 => types::YearAmbiguity::CenturyRequired,
124                    1950..=2049 => types::YearAmbiguity::Unambiguous,
125                    2050.. => types::YearAmbiguity::CenturyRequired,
126                },
127            }
128        } else {
129            types::EraYear {
130                era: tinystr!(16, "bce"),
131                era_index: Some(0),
132                year: 1_i32.saturating_sub(extended_year),
133                ambiguity: types::YearAmbiguity::EraAndCenturyRequired,
134            }
135        }
136    }
137
138    fn extended_year(&self, date: &Self::DateInner) -> i32 {
139        Iso.extended_year(&date.0)
140    }
141
142    fn is_in_leap_year(&self, date: &Self::DateInner) -> bool {
143        Iso.is_in_leap_year(&date.0)
144    }
145
146    /// The calendar-specific month represented by `date`
147    fn month(&self, date: &Self::DateInner) -> types::MonthInfo {
148        Iso.month(&date.0)
149    }
150
151    /// The calendar-specific day-of-month represented by `date`
152    fn day_of_month(&self, date: &Self::DateInner) -> types::DayOfMonth {
153        Iso.day_of_month(&date.0)
154    }
155
156    /// Information of the day of the year
157    fn day_of_year(&self, date: &Self::DateInner) -> types::DayOfYear {
158        date.0 .0.day_of_year()
159    }
160
161    fn debug_name(&self) -> &'static str {
162        "Gregorian"
163    }
164
165    fn calendar_algorithm(&self) -> Option<crate::preferences::CalendarAlgorithm> {
166        Some(crate::preferences::CalendarAlgorithm::Gregory)
167    }
168}
169
170impl Date<Gregorian> {
171    /// Construct a new Gregorian Date.
172    ///
173    /// Years are specified as ISO years.
174    ///
175    /// ```rust
176    /// use icu::calendar::Date;
177    ///
178    /// // Conversion from ISO to Gregorian
179    /// let date_gregorian = Date::try_new_gregorian(1970, 1, 2)
180    ///     .expect("Failed to initialize Gregorian Date instance.");
181    ///
182    /// assert_eq!(date_gregorian.era_year().year, 1970);
183    /// assert_eq!(date_gregorian.month().ordinal, 1);
184    /// assert_eq!(date_gregorian.day_of_month().0, 2);
185    /// ```
186    pub fn try_new_gregorian(year: i32, month: u8, day: u8) -> Result<Date<Gregorian>, RangeError> {
187        Date::try_new_iso(year, month, day).map(|d| Date::new_from_iso(d, Gregorian))
188    }
189}
190
191#[cfg(test)]
192mod test {
193    use calendrical_calculations::rata_die::RataDie;
194
195    use super::*;
196
197    #[derive(Debug)]
198    struct TestCase {
199        rd: RataDie,
200        iso_year: i32,
201        iso_month: u8,
202        iso_day: u8,
203        expected_year: i32,
204        expected_era: &'static str,
205        expected_month: u8,
206        expected_day: u8,
207    }
208
209    fn check_test_case(case: TestCase) {
210        let iso_from_rd = Date::from_rata_die(case.rd, Iso);
211        let greg_date_from_rd = Date::from_rata_die(case.rd, Gregorian);
212        assert_eq!(greg_date_from_rd.era_year().year, case.expected_year,
213            "Failed year check from RD: {case:?}\nISO: {iso_from_rd:?}\nGreg: {greg_date_from_rd:?}");
214        assert_eq!(
215            greg_date_from_rd.era_year().era,
216            case.expected_era,
217            "Failed era check from RD: {case:?}\nISO: {iso_from_rd:?}\nGreg: {greg_date_from_rd:?}"
218        );
219        assert_eq!(greg_date_from_rd.month().ordinal, case.expected_month,
220            "Failed month check from RD: {case:?}\nISO: {iso_from_rd:?}\nGreg: {greg_date_from_rd:?}");
221        assert_eq!(
222            greg_date_from_rd.day_of_month().0,
223            case.expected_day,
224            "Failed day check from RD: {case:?}\nISO: {iso_from_rd:?}\nGreg: {greg_date_from_rd:?}"
225        );
226
227        let iso_date_man: Date<Iso> =
228            Date::try_new_iso(case.iso_year, case.iso_month, case.iso_day)
229                .expect("Failed to initialize ISO date for {case:?}");
230        let greg_date_man: Date<Gregorian> = Date::new_from_iso(iso_date_man, Gregorian);
231        assert_eq!(iso_from_rd, iso_date_man,
232            "ISO from RD not equal to ISO generated from manually-input ymd\nCase: {case:?}\nRD: {iso_from_rd:?}\nMan: {iso_date_man:?}");
233        assert_eq!(greg_date_from_rd, greg_date_man,
234            "Greg. date from RD not equal to Greg. generated from manually-input ymd\nCase: {case:?}\nRD: {greg_date_from_rd:?}\nMan: {greg_date_man:?}");
235    }
236
237    #[test]
238    fn test_gregorian_ce() {
239        // Tests that the Gregorian calendar gives the correct expected
240        // day, month, and year for positive years (AD/CE/gregory era)
241
242        let cases = [
243            TestCase {
244                rd: RataDie::new(1),
245                iso_year: 1,
246                iso_month: 1,
247                iso_day: 1,
248                expected_year: 1,
249                expected_era: "ce",
250                expected_month: 1,
251                expected_day: 1,
252            },
253            TestCase {
254                rd: RataDie::new(181),
255                iso_year: 1,
256                iso_month: 6,
257                iso_day: 30,
258                expected_year: 1,
259                expected_era: "ce",
260                expected_month: 6,
261                expected_day: 30,
262            },
263            TestCase {
264                rd: RataDie::new(1155),
265                iso_year: 4,
266                iso_month: 2,
267                iso_day: 29,
268                expected_year: 4,
269                expected_era: "ce",
270                expected_month: 2,
271                expected_day: 29,
272            },
273            TestCase {
274                rd: RataDie::new(1344),
275                iso_year: 4,
276                iso_month: 9,
277                iso_day: 5,
278                expected_year: 4,
279                expected_era: "ce",
280                expected_month: 9,
281                expected_day: 5,
282            },
283            TestCase {
284                rd: RataDie::new(36219),
285                iso_year: 100,
286                iso_month: 3,
287                iso_day: 1,
288                expected_year: 100,
289                expected_era: "ce",
290                expected_month: 3,
291                expected_day: 1,
292            },
293        ];
294
295        for case in cases {
296            check_test_case(case);
297        }
298    }
299
300    #[test]
301    fn test_gregorian_bce() {
302        // Tests that the Gregorian calendar gives the correct expected
303        // day, month, and year for negative years (BC/BCE era)
304
305        let cases = [
306            TestCase {
307                rd: RataDie::new(0),
308                iso_year: 0,
309                iso_month: 12,
310                iso_day: 31,
311                expected_year: 1,
312                expected_era: "bce",
313                expected_month: 12,
314                expected_day: 31,
315            },
316            TestCase {
317                rd: RataDie::new(-365), // This is a leap year
318                iso_year: 0,
319                iso_month: 1,
320                iso_day: 1,
321                expected_year: 1,
322                expected_era: "bce",
323                expected_month: 1,
324                expected_day: 1,
325            },
326            TestCase {
327                rd: RataDie::new(-366),
328                iso_year: -1,
329                iso_month: 12,
330                iso_day: 31,
331                expected_year: 2,
332                expected_era: "bce",
333                expected_month: 12,
334                expected_day: 31,
335            },
336            TestCase {
337                rd: RataDie::new(-1461),
338                iso_year: -4,
339                iso_month: 12,
340                iso_day: 31,
341                expected_year: 5,
342                expected_era: "bce",
343                expected_month: 12,
344                expected_day: 31,
345            },
346            TestCase {
347                rd: RataDie::new(-1826),
348                iso_year: -4,
349                iso_month: 1,
350                iso_day: 1,
351                expected_year: 5,
352                expected_era: "bce",
353                expected_month: 1,
354                expected_day: 1,
355            },
356        ];
357
358        for case in cases {
359            check_test_case(case);
360        }
361    }
362
363    #[test]
364    fn check_gregorian_directionality() {
365        // Tests that for a large range of RDs, if a RD
366        // is less than another, the corresponding YMD should also be less
367        // than the other, without exception.
368        for i in -100..100 {
369            for j in -100..100 {
370                let iso_i = Date::from_rata_die(RataDie::new(i), Iso);
371                let iso_j = Date::from_rata_die(RataDie::new(j), Iso);
372
373                let greg_i = iso_i.to_calendar(Gregorian);
374                let greg_j = iso_j.to_calendar(Gregorian);
375
376                assert_eq!(
377                    i.cmp(&j),
378                    iso_i.cmp(&iso_j),
379                    "ISO directionality inconsistent with directionality for i: {i}, j: {j}"
380                );
381                assert_eq!(
382                    i.cmp(&j),
383                    greg_i.cmp(&greg_j),
384                    "Gregorian directionality inconsistent with directionality for i: {i}, j: {j}"
385                );
386            }
387        }
388    }
389}