Coverage Report

Created: 2025-08-12 06:35

/rust/registry/src/index.crates.io-6f17d22bba15001f/icu_calendar-1.5.2/src/roc.rs
Line
Count
Source (jump to first uncovered line)
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
34
use crate::{
35
    calendar_arithmetic::ArithmeticDate, iso::IsoDateInner, types, AnyCalendarKind, Calendar,
36
    CalendarError, Date, DateTime, Iso, Time,
37
};
38
use calendrical_calculations::helpers::i64_to_saturated_i32;
39
use tinystr::tinystr;
40
41
/// Year of the beginning of the Taiwanese (ROC/Minguo) calendar.
42
/// 1912 ISO = ROC 1
43
const 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
70
pub 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)]
74
pub struct RocDateInner(IsoDateInner);
75
76
impl Calendar for Roc {
77
    type DateInner = RocDateInner;
78
79
0
    fn date_from_codes(
80
0
        &self,
81
0
        era: crate::types::Era,
82
0
        year: i32,
83
0
        month_code: crate::types::MonthCode,
84
0
        day: u8,
85
0
    ) -> Result<Self::DateInner, crate::Error> {
86
0
        let year = if era.0 == tinystr!(16, "roc") {
87
0
            if year <= 0 {
88
0
                return Err(CalendarError::OutOfRange);
89
0
            }
90
0
            year + ROC_ERA_OFFSET
91
0
        } else if era.0 == tinystr!(16, "roc-inverse") {
92
0
            if year <= 0 {
93
0
                return Err(CalendarError::OutOfRange);
94
0
            }
95
0
            1 - year + ROC_ERA_OFFSET
96
        } else {
97
0
            return Err(CalendarError::UnknownEra(era.0, self.debug_name()));
98
        };
99
100
0
        ArithmeticDate::new_from_codes(self, year, month_code, day)
101
0
            .map(IsoDateInner)
102
0
            .map(RocDateInner)
103
0
    }
104
105
0
    fn date_from_iso(&self, iso: crate::Date<crate::Iso>) -> Self::DateInner {
106
0
        RocDateInner(*iso.inner())
107
0
    }
108
109
0
    fn date_to_iso(&self, date: &Self::DateInner) -> crate::Date<crate::Iso> {
110
0
        Date::from_raw(date.0, Iso)
111
0
    }
112
113
0
    fn months_in_year(&self, date: &Self::DateInner) -> u8 {
114
0
        Iso.months_in_year(&date.0)
115
0
    }
116
117
0
    fn days_in_year(&self, date: &Self::DateInner) -> u16 {
118
0
        Iso.days_in_year(&date.0)
119
0
    }
120
121
0
    fn days_in_month(&self, date: &Self::DateInner) -> u8 {
122
0
        Iso.days_in_month(&date.0)
123
0
    }
124
125
0
    fn offset_date(&self, date: &mut Self::DateInner, offset: crate::DateDuration<Self>) {
126
0
        Iso.offset_date(&mut date.0, offset.cast_unit())
127
0
    }
128
129
0
    fn until(
130
0
        &self,
131
0
        date1: &Self::DateInner,
132
0
        date2: &Self::DateInner,
133
0
        _calendar2: &Self,
134
0
        largest_unit: crate::DateDurationUnit,
135
0
        smallest_unit: crate::DateDurationUnit,
136
0
    ) -> crate::DateDuration<Self> {
137
0
        Iso.until(&date1.0, &date2.0, &Iso, largest_unit, smallest_unit)
138
0
            .cast_unit()
139
0
    }
140
141
0
    fn debug_name(&self) -> &'static str {
142
0
        "ROC"
143
0
    }
144
145
0
    fn year(&self, date: &Self::DateInner) -> crate::types::FormattableYear {
146
0
        year_as_roc(date.0 .0.year as i64)
147
0
    }
148
149
0
    fn is_in_leap_year(&self, date: &Self::DateInner) -> bool {
150
0
        Iso.is_in_leap_year(&date.0)
151
0
    }
152
153
0
    fn month(&self, date: &Self::DateInner) -> crate::types::FormattableMonth {
154
0
        Iso.month(&date.0)
155
0
    }
156
157
0
    fn day_of_month(&self, date: &Self::DateInner) -> crate::types::DayOfMonth {
158
0
        Iso.day_of_month(&date.0)
159
0
    }
160
161
0
    fn day_of_year_info(&self, date: &Self::DateInner) -> crate::types::DayOfYearInfo {
162
0
        let prev_year = date.0 .0.year.saturating_sub(1);
163
0
        let next_year = date.0 .0.year.saturating_add(1);
164
0
        types::DayOfYearInfo {
165
0
            day_of_year: Iso::day_of_year(date.0),
166
0
            days_in_year: Iso::days_in_year_direct(date.0 .0.year),
167
0
            prev_year: year_as_roc(prev_year as i64),
168
0
            days_in_prev_year: Iso::days_in_year_direct(prev_year),
169
0
            next_year: year_as_roc(next_year as i64),
170
0
        }
171
0
    }
172
173
    /// The [`AnyCalendarKind`] corresponding to this calendar
174
0
    fn any_calendar_kind(&self) -> Option<AnyCalendarKind> {
175
0
        Some(AnyCalendarKind::Roc)
176
0
    }
177
}
178
179
impl 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
0
    pub fn try_new_roc_date(year: i32, month: u8, day: u8) -> Result<Date<Roc>, CalendarError> {
207
0
        let iso_year = year.saturating_add(ROC_ERA_OFFSET);
208
0
        Date::try_new_iso_date(iso_year, month, day).map(|d| Date::new_from_iso(d, Roc))
209
0
    }
210
}
211
212
impl 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
0
    pub fn try_new_roc_datetime(
242
0
        year: i32,
243
0
        month: u8,
244
0
        day: u8,
245
0
        hour: u8,
246
0
        minute: u8,
247
0
        second: u8,
248
0
    ) -> Result<DateTime<Roc>, CalendarError> {
249
0
        Ok(DateTime {
250
0
            date: Date::try_new_roc_date(year, month, day)?,
251
0
            time: Time::try_new(hour, minute, second, 0)?,
252
        })
253
0
    }
254
}
255
256
0
pub(crate) fn year_as_roc(year: i64) -> types::FormattableYear {
257
0
    let year_i32 = i64_to_saturated_i32(year);
258
0
    let offset_i64 = ROC_ERA_OFFSET as i64;
259
0
    if year > offset_i64 {
260
0
        types::FormattableYear {
261
0
            era: types::Era(tinystr!(16, "roc")),
262
0
            number: year_i32.saturating_sub(ROC_ERA_OFFSET),
263
0
            cyclic: None,
264
0
            related_iso: Some(year_i32),
265
0
        }
266
    } else {
267
0
        types::FormattableYear {
268
0
            era: types::Era(tinystr!(16, "roc-inverse")),
269
0
            number: (ROC_ERA_OFFSET + 1).saturating_sub(year_i32),
270
0
            cyclic: None,
271
0
            related_iso: Some(year_i32),
272
0
        }
273
    }
274
0
}
275
276
#[cfg(test)]
277
mod 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
}