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
5//! This module contains types and implementations for the Republic of China calendar.
6//!
7//! ```rust
8//! use icu::calendar::{cal::Roc, Date};
9//!
10//! let date_iso = Date::try_new_iso(1970, 1, 2)
11//!     .expect("Failed to initialize ISO Date instance.");
12//! let date_roc = Date::new_from_iso(date_iso, Roc);
13//!
14//! assert_eq!(date_roc.era_year().year, 59);
15//! assert_eq!(date_roc.month().ordinal, 1);
16//! assert_eq!(date_roc.day_of_month().0, 2);
17//! ```
18
19use crate::error::year_check;
20use crate::{
21    cal::iso::IsoDateInner, calendar_arithmetic::ArithmeticDate, error::DateError, types, Calendar,
22    Date, Iso, RangeError,
23};
24use calendrical_calculations::rata_die::RataDie;
25use tinystr::tinystr;
26
27/// Year of the beginning of the Taiwanese (ROC/Minguo) calendar.
28/// 1912 ISO = ROC 1
29const ROC_ERA_OFFSET: i32 = 1911;
30
31/// The [Republic of China Calendar](https://en.wikipedia.org/wiki/Republic_of_China_calendar)
32///
33/// The ROC calendar is a solar calendar used in Taiwan and Penghu, as well as by overseas diaspora from
34/// those locations. Months and days are identical to the [`Gregorian`](super::Gregorian) calendar, while years are counted
35/// with 1912, the year of the establishment of the Republic of China, as year 1 of the ROC/Minguo/民国/民國 era.
36///
37/// The ROC calendar should not be confused with the Chinese traditional lunar calendar
38/// (see [`Chinese`](crate::cal::Chinese)).
39///
40/// # Era codes
41///
42/// This calendar uses two era codes: `roc`, corresponding to years in the 民國 era (CE year 1912 and
43/// after), and `broc`, corresponding to years before the 民國 era (CE year 1911 and before).
44///
45///
46/// # Month codes
47///
48/// This calendar supports 12 solar month codes (`"M01" - "M12"`)
49#[derive(Copy, Clone, Debug, Default)]
50#[allow(clippy::exhaustive_structs)] // this type is stable
51pub struct Roc;
52
53/// The inner date type used for representing [`Date`]s of [`Roc`]. See [`Date`] and [`Roc`] for more info.
54#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, PartialOrd, Ord)]
55pub struct RocDateInner(IsoDateInner);
56
57impl crate::cal::scaffold::UnstableSealed for Roc {}
58impl Calendar for Roc {
59    type DateInner = RocDateInner;
60    type Year = types::EraYear;
61
62    fn from_codes(
63        &self,
64        era: Option<&str>,
65        year: i32,
66        month_code: crate::types::MonthCode,
67        day: u8,
68    ) -> Result<Self::DateInner, DateError> {
69        let year = match era {
70            Some("roc") | None => ROC_ERA_OFFSET + year_check(year, 1..)?,
71            Some("broc") => ROC_ERA_OFFSET + 1 - year_check(year, 1..)?,
72            Some(_) => return Err(DateError::UnknownEra),
73        };
74
75        ArithmeticDate::new_from_codes(self, year, month_code, day)
76            .map(IsoDateInner)
77            .map(RocDateInner)
78    }
79
80    fn from_rata_die(&self, rd: RataDie) -> Self::DateInner {
81        RocDateInner(Iso.from_rata_die(rd))
82    }
83
84    fn to_rata_die(&self, date: &Self::DateInner) -> RataDie {
85        Iso.to_rata_die(&date.0)
86    }
87
88    fn from_iso(&self, iso: IsoDateInner) -> Self::DateInner {
89        RocDateInner(iso)
90    }
91
92    fn to_iso(&self, date: &Self::DateInner) -> IsoDateInner {
93        date.0
94    }
95
96    fn months_in_year(&self, date: &Self::DateInner) -> u8 {
97        Iso.months_in_year(&date.0)
98    }
99
100    fn days_in_year(&self, date: &Self::DateInner) -> u16 {
101        Iso.days_in_year(&date.0)
102    }
103
104    fn days_in_month(&self, date: &Self::DateInner) -> u8 {
105        Iso.days_in_month(&date.0)
106    }
107
108    fn offset_date(&self, date: &mut Self::DateInner, offset: crate::DateDuration<Self>) {
109        Iso.offset_date(&mut date.0, offset.cast_unit())
110    }
111
112    fn until(
113        &self,
114        date1: &Self::DateInner,
115        date2: &Self::DateInner,
116        _calendar2: &Self,
117        largest_unit: crate::DateDurationUnit,
118        smallest_unit: crate::DateDurationUnit,
119    ) -> crate::DateDuration<Self> {
120        Iso.until(&date1.0, &date2.0, &Iso, largest_unit, smallest_unit)
121            .cast_unit()
122    }
123
124    fn debug_name(&self) -> &'static str {
125        "ROC"
126    }
127
128    fn year_info(&self, date: &Self::DateInner) -> Self::Year {
129        let extended_year = self.extended_year(date);
130        if extended_year > 0 {
131            types::EraYear {
132                era: tinystr!(16, "roc"),
133                era_index: Some(1),
134                year: extended_year,
135                ambiguity: types::YearAmbiguity::CenturyRequired,
136            }
137        } else {
138            types::EraYear {
139                era: tinystr!(16, "broc"),
140                era_index: Some(0),
141                year: 1 - extended_year,
142                ambiguity: types::YearAmbiguity::EraAndCenturyRequired,
143            }
144        }
145    }
146
147    fn extended_year(&self, date: &Self::DateInner) -> i32 {
148        Iso.extended_year(&date.0) - ROC_ERA_OFFSET
149    }
150
151    fn is_in_leap_year(&self, date: &Self::DateInner) -> bool {
152        Iso.is_in_leap_year(&date.0)
153    }
154
155    fn month(&self, date: &Self::DateInner) -> crate::types::MonthInfo {
156        Iso.month(&date.0)
157    }
158
159    fn day_of_month(&self, date: &Self::DateInner) -> crate::types::DayOfMonth {
160        Iso.day_of_month(&date.0)
161    }
162
163    fn day_of_year(&self, date: &Self::DateInner) -> types::DayOfYear {
164        Iso.day_of_year(&date.0)
165    }
166
167    fn calendar_algorithm(&self) -> Option<crate::preferences::CalendarAlgorithm> {
168        Some(crate::preferences::CalendarAlgorithm::Roc)
169    }
170}
171
172impl Date<Roc> {
173    /// Construct a new Republic of China calendar Date.
174    ///
175    /// Years are specified in the "roc" era. This function accepts an extended year in that era, so dates
176    /// before Minguo are negative and year 0 is 1 Before Minguo. To specify dates using explicit era
177    /// codes, use [`Date::try_new_from_codes()`].
178    ///
179    /// ```rust
180    /// use icu::calendar::Date;
181    /// use icu::calendar::cal::Gregorian;
182    /// use tinystr::tinystr;
183    ///
184    /// // Create a new ROC Date
185    /// let date_roc = Date::try_new_roc(1, 2, 3)
186    ///     .expect("Failed to initialize ROC Date instance.");
187    ///
188    /// assert_eq!(date_roc.era_year().era, tinystr!(16, "roc"));
189    /// assert_eq!(date_roc.era_year().year, 1, "ROC year check failed!");
190    /// assert_eq!(date_roc.month().ordinal, 2, "ROC month check failed!");
191    /// assert_eq!(date_roc.day_of_month().0, 3, "ROC day of month check failed!");
192    ///
193    /// // Convert to an equivalent Gregorian date
194    /// let date_gregorian = date_roc.to_calendar(Gregorian);
195    ///
196    /// assert_eq!(date_gregorian.era_year().year, 1912, "Gregorian from ROC year check failed!");
197    /// assert_eq!(date_gregorian.month().ordinal, 2, "Gregorian from ROC month check failed!");
198    /// assert_eq!(date_gregorian.day_of_month().0, 3, "Gregorian from ROC day of month check failed!");
199    pub fn try_new_roc(year: i32, month: u8, day: u8) -> Result<Date<Roc>, RangeError> {
200        let iso_year = year.saturating_add(ROC_ERA_OFFSET);
201        Date::try_new_iso(iso_year, month, day).map(|d| Date::new_from_iso(d, Roc))
202    }
203}
204
205#[cfg(test)]
206mod test {
207
208    use super::*;
209    use calendrical_calculations::rata_die::RataDie;
210
211    #[derive(Debug)]
212    struct TestCase {
213        rd: RataDie,
214        iso_year: i32,
215        iso_month: u8,
216        iso_day: u8,
217        expected_year: i32,
218        expected_era: &'static str,
219        expected_month: u8,
220        expected_day: u8,
221    }
222
223    fn check_test_case(case: TestCase) {
224        let iso_from_rd = Date::from_rata_die(case.rd, Iso);
225        let roc_from_rd = Date::from_rata_die(case.rd, Roc);
226        assert_eq!(
227            roc_from_rd.era_year().year,
228            case.expected_year,
229            "Failed year check from RD: {case:?}\nISO: {iso_from_rd:?}\nROC: {roc_from_rd:?}"
230        );
231        assert_eq!(
232            roc_from_rd.era_year().era,
233            case.expected_era,
234            "Failed era check from RD: {case:?}\nISO: {iso_from_rd:?}\nROC: {roc_from_rd:?}"
235        );
236        assert_eq!(
237            roc_from_rd.extended_year(),
238            if case.expected_era == "roc" {
239                case.expected_year
240            } else {
241                1 - case.expected_year
242            },
243            "Failed year check from RD: {case:?}\nISO: {iso_from_rd:?}\nROC: {roc_from_rd:?}"
244        );
245        assert_eq!(
246            roc_from_rd.month().ordinal,
247            case.expected_month,
248            "Failed month check from RD: {case:?}\nISO: {iso_from_rd:?}\nROC: {roc_from_rd:?}"
249        );
250        assert_eq!(roc_from_rd.day_of_month().0, case.expected_day,
251            "Failed day_of_month check from RD: {case:?}\nISO: {iso_from_rd:?}\nROC: {roc_from_rd:?}");
252
253        let iso_from_case = Date::try_new_iso(case.iso_year, case.iso_month, case.iso_day)
254            .expect("Failed to initialize ISO date for {case:?}");
255        let roc_from_case = Date::new_from_iso(iso_from_case, Roc);
256        assert_eq!(iso_from_rd, iso_from_case,
257            "ISO from RD not equal to ISO generated from manually-input ymd\nCase: {case:?}\nRD: {iso_from_rd:?}\nManual: {iso_from_case:?}");
258        assert_eq!(roc_from_rd, roc_from_case,
259            "ROC date from RD not equal to ROC generated from manually-input ymd\nCase: {case:?}\nRD: {roc_from_rd:?}\nManual: {roc_from_case:?}");
260    }
261
262    #[test]
263    fn test_roc_current_era() {
264        // Tests that the ROC calendar gives the correct expected day, month, and year for years >= 1912
265        // (years in the ROC/minguo era)
266        //
267        // Jan 1. 1912 CE = RD 697978
268
269        let cases = [
270            TestCase {
271                rd: RataDie::new(697978),
272                iso_year: 1912,
273                iso_month: 1,
274                iso_day: 1,
275                expected_year: 1,
276                expected_era: "roc",
277                expected_month: 1,
278                expected_day: 1,
279            },
280            TestCase {
281                rd: RataDie::new(698037),
282                iso_year: 1912,
283                iso_month: 2,
284                iso_day: 29,
285                expected_year: 1,
286                expected_era: "roc",
287                expected_month: 2,
288                expected_day: 29,
289            },
290            TestCase {
291                rd: RataDie::new(698524),
292                iso_year: 1913,
293                iso_month: 6,
294                iso_day: 30,
295                expected_year: 2,
296                expected_era: "roc",
297                expected_month: 6,
298                expected_day: 30,
299            },
300            TestCase {
301                rd: RataDie::new(738714),
302                iso_year: 2023,
303                iso_month: 7,
304                iso_day: 13,
305                expected_year: 112,
306                expected_era: "roc",
307                expected_month: 7,
308                expected_day: 13,
309            },
310        ];
311
312        for case in cases {
313            check_test_case(case);
314        }
315    }
316
317    #[test]
318    fn test_roc_prior_era() {
319        // Tests that the ROC calendar gives the correct expected day, month, and year for years <= 1911
320        // (years in the ROC/minguo era)
321        //
322        // Jan 1. 1912 CE = RD 697978
323        let cases = [
324            TestCase {
325                rd: RataDie::new(697977),
326                iso_year: 1911,
327                iso_month: 12,
328                iso_day: 31,
329                expected_year: 1,
330                expected_era: "broc",
331                expected_month: 12,
332                expected_day: 31,
333            },
334            TestCase {
335                rd: RataDie::new(697613),
336                iso_year: 1911,
337                iso_month: 1,
338                iso_day: 1,
339                expected_year: 1,
340                expected_era: "broc",
341                expected_month: 1,
342                expected_day: 1,
343            },
344            TestCase {
345                rd: RataDie::new(697612),
346                iso_year: 1910,
347                iso_month: 12,
348                iso_day: 31,
349                expected_year: 2,
350                expected_era: "broc",
351                expected_month: 12,
352                expected_day: 31,
353            },
354            TestCase {
355                rd: RataDie::new(696576),
356                iso_year: 1908,
357                iso_month: 2,
358                iso_day: 29,
359                expected_year: 4,
360                expected_era: "broc",
361                expected_month: 2,
362                expected_day: 29,
363            },
364            TestCase {
365                rd: RataDie::new(1),
366                iso_year: 1,
367                iso_month: 1,
368                iso_day: 1,
369                expected_year: 1911,
370                expected_era: "broc",
371                expected_month: 1,
372                expected_day: 1,
373            },
374            TestCase {
375                rd: RataDie::new(0),
376                iso_year: 0,
377                iso_month: 12,
378                iso_day: 31,
379                expected_year: 1912,
380                expected_era: "broc",
381                expected_month: 12,
382                expected_day: 31,
383            },
384        ];
385
386        for case in cases {
387            check_test_case(case);
388        }
389    }
390
391    #[test]
392    fn test_roc_directionality_near_epoch() {
393        // Tests that for a large range of RDs near the beginning of the minguo era (CE 1912),
394        // the comparison between those two RDs should be equal to the comparison between their
395        // corresponding YMD.
396        let rd_epoch_start = 697978;
397        for i in (rd_epoch_start - 100)..=(rd_epoch_start + 100) {
398            for j in (rd_epoch_start - 100)..=(rd_epoch_start + 100) {
399                let iso_i = Date::from_rata_die(RataDie::new(i), Iso);
400                let iso_j = Date::from_rata_die(RataDie::new(j), Iso);
401
402                let roc_i = Date::from_rata_die(RataDie::new(i), Roc);
403                let roc_j = Date::from_rata_die(RataDie::new(j), Roc);
404
405                assert_eq!(
406                    i.cmp(&j),
407                    iso_i.cmp(&iso_j),
408                    "ISO directionality inconsistent with directionality for i: {i}, j: {j}"
409                );
410                assert_eq!(
411                    i.cmp(&j),
412                    roc_i.cmp(&roc_j),
413                    "ROC directionality inconsistent with directionality for i: {i}, j: {j}"
414                );
415            }
416        }
417    }
418
419    #[test]
420    fn test_roc_directionality_near_rd_zero() {
421        // Same as `test_directionality_near_epoch`, but with a focus around RD 0
422        for i in -100..=100 {
423            for j in -100..100 {
424                let iso_i = Date::from_rata_die(RataDie::new(i), Iso);
425                let iso_j = Date::from_rata_die(RataDie::new(j), Iso);
426
427                let roc_i = Date::from_rata_die(RataDie::new(i), Roc);
428                let roc_j = Date::from_rata_die(RataDie::new(j), Roc);
429
430                assert_eq!(
431                    i.cmp(&j),
432                    iso_i.cmp(&iso_j),
433                    "ISO directionality inconsistent with directionality for i: {i}, j: {j}"
434                );
435                assert_eq!(
436                    i.cmp(&j),
437                    roc_i.cmp(&roc_j),
438                    "ROC directionality inconsistent with directionality for i: {i}, j: {j}"
439                );
440            }
441        }
442    }
443}