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 > ROC_ERA_OFFSET {
131            types::EraYear {
132                era: tinystr!(16, "roc"),
133                era_index: Some(1),
134                year: extended_year.saturating_sub(ROC_ERA_OFFSET),
135                ambiguity: types::YearAmbiguity::CenturyRequired,
136            }
137        } else {
138            types::EraYear {
139                era: tinystr!(16, "broc"),
140                era_index: Some(0),
141                year: (ROC_ERA_OFFSET + 1).saturating_sub(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)
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.month().ordinal,
238            case.expected_month,
239            "Failed month check from RD: {case:?}\nISO: {iso_from_rd:?}\nROC: {roc_from_rd:?}"
240        );
241        assert_eq!(roc_from_rd.day_of_month().0, case.expected_day,
242            "Failed day_of_month check from RD: {case:?}\nISO: {iso_from_rd:?}\nROC: {roc_from_rd:?}");
243
244        let iso_from_case = Date::try_new_iso(case.iso_year, case.iso_month, case.iso_day)
245            .expect("Failed to initialize ISO date for {case:?}");
246        let roc_from_case = Date::new_from_iso(iso_from_case, Roc);
247        assert_eq!(iso_from_rd, iso_from_case,
248            "ISO from RD not equal to ISO generated from manually-input ymd\nCase: {case:?}\nRD: {iso_from_rd:?}\nManual: {iso_from_case:?}");
249        assert_eq!(roc_from_rd, roc_from_case,
250            "ROC date from RD not equal to ROC generated from manually-input ymd\nCase: {case:?}\nRD: {roc_from_rd:?}\nManual: {roc_from_case:?}");
251    }
252
253    #[test]
254    fn test_roc_current_era() {
255        // Tests that the ROC calendar gives the correct expected day, month, and year for years >= 1912
256        // (years in the ROC/minguo era)
257        //
258        // Jan 1. 1912 CE = RD 697978
259
260        let cases = [
261            TestCase {
262                rd: RataDie::new(697978),
263                iso_year: 1912,
264                iso_month: 1,
265                iso_day: 1,
266                expected_year: 1,
267                expected_era: "roc",
268                expected_month: 1,
269                expected_day: 1,
270            },
271            TestCase {
272                rd: RataDie::new(698037),
273                iso_year: 1912,
274                iso_month: 2,
275                iso_day: 29,
276                expected_year: 1,
277                expected_era: "roc",
278                expected_month: 2,
279                expected_day: 29,
280            },
281            TestCase {
282                rd: RataDie::new(698524),
283                iso_year: 1913,
284                iso_month: 6,
285                iso_day: 30,
286                expected_year: 2,
287                expected_era: "roc",
288                expected_month: 6,
289                expected_day: 30,
290            },
291            TestCase {
292                rd: RataDie::new(738714),
293                iso_year: 2023,
294                iso_month: 7,
295                iso_day: 13,
296                expected_year: 112,
297                expected_era: "roc",
298                expected_month: 7,
299                expected_day: 13,
300            },
301        ];
302
303        for case in cases {
304            check_test_case(case);
305        }
306    }
307
308    #[test]
309    fn test_roc_prior_era() {
310        // Tests that the ROC calendar gives the correct expected day, month, and year for years <= 1911
311        // (years in the ROC/minguo era)
312        //
313        // Jan 1. 1912 CE = RD 697978
314        let cases = [
315            TestCase {
316                rd: RataDie::new(697977),
317                iso_year: 1911,
318                iso_month: 12,
319                iso_day: 31,
320                expected_year: 1,
321                expected_era: "broc",
322                expected_month: 12,
323                expected_day: 31,
324            },
325            TestCase {
326                rd: RataDie::new(697613),
327                iso_year: 1911,
328                iso_month: 1,
329                iso_day: 1,
330                expected_year: 1,
331                expected_era: "broc",
332                expected_month: 1,
333                expected_day: 1,
334            },
335            TestCase {
336                rd: RataDie::new(697612),
337                iso_year: 1910,
338                iso_month: 12,
339                iso_day: 31,
340                expected_year: 2,
341                expected_era: "broc",
342                expected_month: 12,
343                expected_day: 31,
344            },
345            TestCase {
346                rd: RataDie::new(696576),
347                iso_year: 1908,
348                iso_month: 2,
349                iso_day: 29,
350                expected_year: 4,
351                expected_era: "broc",
352                expected_month: 2,
353                expected_day: 29,
354            },
355            TestCase {
356                rd: RataDie::new(1),
357                iso_year: 1,
358                iso_month: 1,
359                iso_day: 1,
360                expected_year: 1911,
361                expected_era: "broc",
362                expected_month: 1,
363                expected_day: 1,
364            },
365            TestCase {
366                rd: RataDie::new(0),
367                iso_year: 0,
368                iso_month: 12,
369                iso_day: 31,
370                expected_year: 1912,
371                expected_era: "broc",
372                expected_month: 12,
373                expected_day: 31,
374            },
375        ];
376
377        for case in cases {
378            check_test_case(case);
379        }
380    }
381
382    #[test]
383    fn test_roc_directionality_near_epoch() {
384        // Tests that for a large range of RDs near the beginning of the minguo era (CE 1912),
385        // the comparison between those two RDs should be equal to the comparison between their
386        // corresponding YMD.
387        let rd_epoch_start = 697978;
388        for i in (rd_epoch_start - 100)..=(rd_epoch_start + 100) {
389            for j in (rd_epoch_start - 100)..=(rd_epoch_start + 100) {
390                let iso_i = Date::from_rata_die(RataDie::new(i), Iso);
391                let iso_j = Date::from_rata_die(RataDie::new(j), Iso);
392
393                let roc_i = Date::from_rata_die(RataDie::new(i), Roc);
394                let roc_j = Date::from_rata_die(RataDie::new(j), Roc);
395
396                assert_eq!(
397                    i.cmp(&j),
398                    iso_i.cmp(&iso_j),
399                    "ISO directionality inconsistent with directionality for i: {i}, j: {j}"
400                );
401                assert_eq!(
402                    i.cmp(&j),
403                    roc_i.cmp(&roc_j),
404                    "ROC directionality inconsistent with directionality for i: {i}, j: {j}"
405                );
406            }
407        }
408    }
409
410    #[test]
411    fn test_roc_directionality_near_rd_zero() {
412        // Same as `test_directionality_near_epoch`, but with a focus around RD 0
413        for i in -100..=100 {
414            for j in -100..100 {
415                let iso_i = Date::from_rata_die(RataDie::new(i), Iso);
416                let iso_j = Date::from_rata_die(RataDie::new(j), Iso);
417
418                let roc_i = Date::from_rata_die(RataDie::new(i), Roc);
419                let roc_j = Date::from_rata_die(RataDie::new(j), Roc);
420
421                assert_eq!(
422                    i.cmp(&j),
423                    iso_i.cmp(&iso_j),
424                    "ISO directionality inconsistent with directionality for i: {i}, j: {j}"
425                );
426                assert_eq!(
427                    i.cmp(&j),
428                    roc_i.cmp(&roc_j),
429                    "ROC directionality inconsistent with directionality for i: {i}, j: {j}"
430                );
431            }
432        }
433    }
434}