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