icu_calendar/cal/
roc.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::preferences::CalendarAlgorithm;
9use crate::{types, Date, DateError, RangeError};
10use tinystr::tinystr;
11
12/// The [Republic of China Calendar](https://en.wikipedia.org/wiki/Republic_of_China_calendar)
13///
14/// The ROC Calendar is a variant of the [`Gregorian`](crate::cal::Gregorian) calendar
15/// created by the government of the Republic of China. It is identical to the Gregorian
16/// calendar except that is uses the ROC/Minguo/民国/民國 Era (1912 CE) instead of the Common Era.
17///
18/// This implementation extends proleptically for dates before the calendar's creation
19/// in 1 Minguo (1912 CE).
20///
21/// The ROC calendar should not be confused with the [`ChineseTraditional`](crate::cal::ChineseTraditional)
22/// lunisolar calendar.
23///
24/// This corresponds to the `"roc"` [CLDR calendar](https://unicode.org/reports/tr35/#UnicodeCalendarIdentifier).
25///
26/// # Era codes
27///
28/// This calendar uses two era codes: `roc`, corresponding to years in the 民國 era (CE year 1912 and
29/// after), and `broc`, corresponding to years before the 民國 era (CE year 1911 and before).
30#[derive(Copy, Clone, Debug, Default)]
31#[allow(clippy::exhaustive_structs)] // this type is stable
32pub struct Roc;
33
34impl_with_abstract_gregorian!(crate::cal::Roc, RocDateInner, RocEra, _x, RocEra);
35
36#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
37pub(crate) struct RocEra;
38
39impl GregorianYears for RocEra {
40    const EXTENDED_YEAR_OFFSET: i32 = 1911;
41
42    fn extended_from_era_year(
43        &self,
44        era: Option<&[u8]>,
45        year: i32,
46    ) -> Result<i32, UnknownEraError> {
47        match era {
48            None => Ok(year),
49            Some(b"roc") => Ok(year),
50            Some(b"broc") => Ok(1 - year),
51            Some(_) => Err(UnknownEraError),
52        }
53    }
54
55    fn era_year_from_extended(&self, extended_year: i32, _month: u8, _day: u8) -> types::EraYear {
56        if extended_year > 0 {
57            types::EraYear {
58                era: tinystr!(16, "roc"),
59                era_index: Some(1),
60                year: extended_year,
61                extended_year,
62                ambiguity: types::YearAmbiguity::CenturyRequired,
63            }
64        } else {
65            types::EraYear {
66                era: tinystr!(16, "broc"),
67                era_index: Some(0),
68                year: 1 - extended_year,
69                extended_year,
70                ambiguity: types::YearAmbiguity::EraAndCenturyRequired,
71            }
72        }
73    }
74
75    fn debug_name(&self) -> &'static str {
76        "ROC"
77    }
78
79    fn calendar_algorithm(&self) -> Option<CalendarAlgorithm> {
80        Some(CalendarAlgorithm::Roc)
81    }
82}
83
84impl Date<Roc> {
85    /// Construct a new Republic of China calendar Date.
86    ///
87    /// Years are specified in the "roc" era. This function accepts an extended year in that era, so dates
88    /// before Minguo are negative and year 0 is 1 Before Minguo. To specify dates using explicit era
89    /// codes, use [`Date::try_new_from_codes()`].
90    ///
91    /// ```rust
92    /// use icu::calendar::Date;
93    /// use icu::calendar::cal::Gregorian;
94    /// use tinystr::tinystr;
95    ///
96    /// // Create a new ROC Date
97    /// let date_roc = Date::try_new_roc(1, 2, 3)
98    ///     .expect("Failed to initialize ROC Date instance.");
99    ///
100    /// assert_eq!(date_roc.era_year().era, "roc");
101    /// assert_eq!(date_roc.era_year().year, 1, "ROC year check failed!");
102    /// assert_eq!(date_roc.month().ordinal, 2, "ROC month check failed!");
103    /// assert_eq!(date_roc.day_of_month().0, 3, "ROC day of month check failed!");
104    ///
105    /// // Convert to an equivalent Gregorian date
106    /// let date_gregorian = date_roc.to_calendar(Gregorian);
107    ///
108    /// assert_eq!(date_gregorian.era_year().year, 1912, "Gregorian from ROC year check failed!");
109    /// assert_eq!(date_gregorian.month().ordinal, 2, "Gregorian from ROC month check failed!");
110    /// assert_eq!(date_gregorian.day_of_month().0, 3, "Gregorian from ROC day of month check failed!");
111    pub fn try_new_roc(year: i32, month: u8, day: u8) -> Result<Date<Roc>, RangeError> {
112        ArithmeticDate::new_gregorian::<RocEra>(year, month, day)
113            .map(RocDateInner)
114            .map(|i| Date::from_raw(i, Roc))
115    }
116}
117
118#[cfg(test)]
119mod test {
120
121    use super::*;
122    use crate::cal::Iso;
123    use calendrical_calculations::rata_die::RataDie;
124
125    #[derive(Debug)]
126    struct TestCase {
127        rd: RataDie,
128        iso_year: i32,
129        iso_month: u8,
130        iso_day: u8,
131        expected_year: i32,
132        expected_era: &'static str,
133        expected_month: u8,
134        expected_day: u8,
135    }
136
137    fn check_test_case(case: TestCase) {
138        let iso_from_rd = Date::from_rata_die(case.rd, Iso);
139        let roc_from_rd = Date::from_rata_die(case.rd, Roc);
140        assert_eq!(
141            roc_from_rd.era_year().year,
142            case.expected_year,
143            "Failed year check from RD: {case:?}\nISO: {iso_from_rd:?}\nROC: {roc_from_rd:?}"
144        );
145        assert_eq!(
146            roc_from_rd.era_year().era,
147            case.expected_era,
148            "Failed era check from RD: {case:?}\nISO: {iso_from_rd:?}\nROC: {roc_from_rd:?}"
149        );
150        assert_eq!(
151            roc_from_rd.extended_year(),
152            if case.expected_era == "roc" {
153                case.expected_year
154            } else {
155                1 - case.expected_year
156            },
157            "Failed year check from RD: {case:?}\nISO: {iso_from_rd:?}\nROC: {roc_from_rd:?}"
158        );
159        assert_eq!(
160            roc_from_rd.month().ordinal,
161            case.expected_month,
162            "Failed month check from RD: {case:?}\nISO: {iso_from_rd:?}\nROC: {roc_from_rd:?}"
163        );
164        assert_eq!(roc_from_rd.day_of_month().0, case.expected_day,
165            "Failed day_of_month check from RD: {case:?}\nISO: {iso_from_rd:?}\nROC: {roc_from_rd:?}");
166
167        let iso_from_case = Date::try_new_iso(case.iso_year, case.iso_month, case.iso_day)
168            .expect("Failed to initialize ISO date for {case:?}");
169        let roc_from_case = Date::new_from_iso(iso_from_case, Roc);
170        assert_eq!(iso_from_rd, iso_from_case,
171            "ISO from RD not equal to ISO generated from manually-input ymd\nCase: {case:?}\nRD: {iso_from_rd:?}\nManual: {iso_from_case:?}");
172        assert_eq!(roc_from_rd, roc_from_case,
173            "ROC date from RD not equal to ROC generated from manually-input ymd\nCase: {case:?}\nRD: {roc_from_rd:?}\nManual: {roc_from_case:?}");
174    }
175
176    #[test]
177    fn test_roc_current_era() {
178        // Tests that the ROC calendar gives the correct expected day, month, and year for years >= 1912
179        // (years in the ROC/minguo era)
180        //
181        // Jan 1. 1912 CE = RD 697978
182
183        let cases = [
184            TestCase {
185                rd: RataDie::new(697978),
186                iso_year: 1912,
187                iso_month: 1,
188                iso_day: 1,
189                expected_year: 1,
190                expected_era: "roc",
191                expected_month: 1,
192                expected_day: 1,
193            },
194            TestCase {
195                rd: RataDie::new(698037),
196                iso_year: 1912,
197                iso_month: 2,
198                iso_day: 29,
199                expected_year: 1,
200                expected_era: "roc",
201                expected_month: 2,
202                expected_day: 29,
203            },
204            TestCase {
205                rd: RataDie::new(698524),
206                iso_year: 1913,
207                iso_month: 6,
208                iso_day: 30,
209                expected_year: 2,
210                expected_era: "roc",
211                expected_month: 6,
212                expected_day: 30,
213            },
214            TestCase {
215                rd: RataDie::new(738714),
216                iso_year: 2023,
217                iso_month: 7,
218                iso_day: 13,
219                expected_year: 112,
220                expected_era: "roc",
221                expected_month: 7,
222                expected_day: 13,
223            },
224        ];
225
226        for case in cases {
227            check_test_case(case);
228        }
229    }
230
231    #[test]
232    fn test_roc_prior_era() {
233        // Tests that the ROC calendar gives the correct expected day, month, and year for years <= 1911
234        // (years in the ROC/minguo era)
235        //
236        // Jan 1. 1912 CE = RD 697978
237        let cases = [
238            TestCase {
239                rd: RataDie::new(697977),
240                iso_year: 1911,
241                iso_month: 12,
242                iso_day: 31,
243                expected_year: 1,
244                expected_era: "broc",
245                expected_month: 12,
246                expected_day: 31,
247            },
248            TestCase {
249                rd: RataDie::new(697613),
250                iso_year: 1911,
251                iso_month: 1,
252                iso_day: 1,
253                expected_year: 1,
254                expected_era: "broc",
255                expected_month: 1,
256                expected_day: 1,
257            },
258            TestCase {
259                rd: RataDie::new(697612),
260                iso_year: 1910,
261                iso_month: 12,
262                iso_day: 31,
263                expected_year: 2,
264                expected_era: "broc",
265                expected_month: 12,
266                expected_day: 31,
267            },
268            TestCase {
269                rd: RataDie::new(696576),
270                iso_year: 1908,
271                iso_month: 2,
272                iso_day: 29,
273                expected_year: 4,
274                expected_era: "broc",
275                expected_month: 2,
276                expected_day: 29,
277            },
278            TestCase {
279                rd: RataDie::new(1),
280                iso_year: 1,
281                iso_month: 1,
282                iso_day: 1,
283                expected_year: 1911,
284                expected_era: "broc",
285                expected_month: 1,
286                expected_day: 1,
287            },
288            TestCase {
289                rd: RataDie::new(0),
290                iso_year: 0,
291                iso_month: 12,
292                iso_day: 31,
293                expected_year: 1912,
294                expected_era: "broc",
295                expected_month: 12,
296                expected_day: 31,
297            },
298        ];
299
300        for case in cases {
301            check_test_case(case);
302        }
303    }
304
305    #[test]
306    fn test_roc_directionality_near_epoch() {
307        // Tests that for a large range of RDs near the beginning of the minguo era (CE 1912),
308        // the comparison between those two RDs should be equal to the comparison between their
309        // corresponding YMD.
310        let rd_epoch_start = 697978;
311        for i in (rd_epoch_start - 100)..=(rd_epoch_start + 100) {
312            for j in (rd_epoch_start - 100)..=(rd_epoch_start + 100) {
313                let iso_i = Date::from_rata_die(RataDie::new(i), Iso);
314                let iso_j = Date::from_rata_die(RataDie::new(j), Iso);
315
316                let roc_i = Date::from_rata_die(RataDie::new(i), Roc);
317                let roc_j = Date::from_rata_die(RataDie::new(j), Roc);
318
319                assert_eq!(
320                    i.cmp(&j),
321                    iso_i.cmp(&iso_j),
322                    "ISO directionality inconsistent with directionality for i: {i}, j: {j}"
323                );
324                assert_eq!(
325                    i.cmp(&j),
326                    roc_i.cmp(&roc_j),
327                    "ROC directionality inconsistent with directionality for i: {i}, j: {j}"
328                );
329            }
330        }
331    }
332
333    #[test]
334    fn test_roc_directionality_near_rd_zero() {
335        // Same as `test_directionality_near_epoch`, but with a focus around RD 0
336        for i in -100..=100 {
337            for j in -100..100 {
338                let iso_i = Date::from_rata_die(RataDie::new(i), Iso);
339                let iso_j = Date::from_rata_die(RataDie::new(j), Iso);
340
341                let roc_i = Date::from_rata_die(RataDie::new(i), Roc);
342                let roc_j = Date::from_rata_die(RataDie::new(j), Roc);
343
344                assert_eq!(
345                    i.cmp(&j),
346                    iso_i.cmp(&iso_j),
347                    "ISO directionality inconsistent with directionality for i: {i}, j: {j}"
348                );
349                assert_eq!(
350                    i.cmp(&j),
351                    roc_i.cmp(&roc_j),
352                    "ROC directionality inconsistent with directionality for i: {i}, j: {j}"
353                );
354            }
355        }
356    }
357}