icu_calendar/cal/
chinese.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 Chinese calendar.
6//!
7//! ```rust
8//! use icu::calendar::{cal::Chinese, Date};
9//!
10//! let chinese = Chinese::new();
11//! let chinese_date = Date::try_new_chinese_with_calendar(2023, 6, 6, chinese)
12//!     .expect("Failed to initialize Chinese Date instance.");
13//!
14//! assert_eq!(chinese_date.cyclic_year().related_iso, 2023);
15//! assert_eq!(chinese_date.cyclic_year().year, 40);
16//! assert_eq!(chinese_date.month().ordinal, 6);
17//! assert_eq!(chinese_date.day_of_month().0, 6);
18//! ```
19
20use crate::cal::chinese_based::{ChineseBasedPrecomputedData, ChineseBasedWithDataLoading};
21use crate::cal::iso::{Iso, IsoDateInner};
22use crate::calendar_arithmetic::PrecomputedDataSource;
23use crate::calendar_arithmetic::{ArithmeticDate, CalendarArithmetic};
24use crate::error::DateError;
25use crate::provider::chinese_based::CalendarChineseV1;
26use crate::AsCalendar;
27use crate::{types, Calendar, Date, DateDuration, DateDurationUnit};
28use calendrical_calculations::chinese_based;
29use calendrical_calculations::rata_die::RataDie;
30use core::cmp::Ordering;
31use icu_provider::prelude::*;
32
33/// The [Chinese Calendar](https://en.wikipedia.org/wiki/Chinese_calendar)
34///
35/// The Chinese Calendar is a lunisolar calendar used traditionally in China as well as in other
36/// countries particularly in, but not limited to, East Asia. It is often used today to track important
37/// cultural events and holidays like the Chinese Lunar New Year.
38///
39/// This type can be used with [`Date`] to represent dates in the Chinese calendar.
40///
41/// # Months
42///
43/// The Chinese calendar is an astronomical calendar which uses the phases of the moon to track months.
44/// Each month starts on the date of the new moon as observed from China, meaning that months last 29
45/// or 30 days.
46///
47/// One year in the Chinese calendar is typically 12 lunar months; however, because 12 lunar months does
48/// not line up to one solar year, the Chinese calendar will add an intercalary leap month approximately
49/// every three years to keep Chinese calendar months in line with the solar year.
50///
51/// Leap months can happen after any month; the month in which a leap month occurs is based on the alignment
52/// of months with 24 solar terms into which the solar year is divided.
53///
54/// # Year and Era codes
55///
56/// Unlike the Gregorian calendar, the Chinese calendar does not traditionally count years in an infinitely
57/// increasing sequence. Instead, 10 "celestial stems" and 12 "terrestrial branches" are combined to form a
58/// cycle of year names which repeats every 60 years. However, for the purposes of calendar calculations and
59/// conversions, this calendar also counts years based on the ISO (Gregorian) calendar. This "related ISO year"
60/// marks the ISO year in which a Chinese year begins.
61///
62/// Because the Chinese calendar does not traditionally count years, era codes are not used in this calendar.
63///
64/// For more information, suggested reading materials include:
65/// * _Calendrical Calculations_ by Reingold & Dershowitz
66/// * _The Mathematics of the Chinese Calendar_ by Helmer Aslaksen <https://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.139.9311&rep=rep1&type=pdf>
67/// * Wikipedia: <https://en.wikipedia.org/wiki/Chinese_calendar>
68///
69/// # Month codes
70///
71/// This calendar is a lunisolar calendar. It supports regular month codes `"M01" - "M12"` as well
72/// as leap month codes `"M01L" - "M12L"`.
73///
74/// This calendar is currently in a preview state: formatting for this calendar is not
75/// going to be perfect.
76#[derive(Clone, Debug, Default)]
77pub struct Chinese {
78    data: Option<DataPayload<CalendarChineseV1>>,
79}
80
81/// The inner date type used for representing [`Date`]s of [`Chinese`]. See [`Date`] and [`Chinese`] for more details.
82#[derive(Debug, Eq, PartialEq, PartialOrd, Ord)]
83pub struct ChineseDateInner(ArithmeticDate<Chinese>);
84
85// we want these impls without the `C: Copy/Clone` bounds
86impl Copy for ChineseDateInner {}
87impl Clone for ChineseDateInner {
88    fn clone(&self) -> Self {
89        *self
90    }
91}
92
93// These impls just make custom derives on types containing C
94// work. They're basically no-ops
95impl PartialEq for Chinese {
96    fn eq(&self, _: &Self) -> bool {
97        true
98    }
99}
100impl Eq for Chinese {}
101#[allow(clippy::non_canonical_partial_ord_impl)] // this is intentional
102impl PartialOrd for Chinese {
103    fn partial_cmp(&self, _: &Self) -> Option<Ordering> {
104        Some(Ordering::Equal)
105    }
106}
107
108impl Ord for Chinese {
109    fn cmp(&self, _: &Self) -> Ordering {
110        Ordering::Equal
111    }
112}
113
114impl Chinese {
115    /// Creates a new [`Chinese`] with some precomputed calendrical calculations.
116    ///
117    /// ✨ *Enabled with the `compiled_data` Cargo feature.*
118    ///
119    /// [📚 Help choosing a constructor](icu_provider::constructors)
120    #[cfg(feature = "compiled_data")]
121    pub const fn new() -> Self {
122        Self {
123            data: Some(DataPayload::from_static_ref(
124                crate::provider::Baked::SINGLETON_CALENDAR_CHINESE_V1,
125            )),
126        }
127    }
128
129    icu_provider::gen_buffer_data_constructors!(() -> error: DataError,
130        functions: [
131            new: skip,
132            try_new_with_buffer_provider,
133            try_new_unstable,
134            Self,
135    ]);
136
137    #[doc = icu_provider::gen_buffer_unstable_docs!(UNSTABLE, Self::new)]
138    pub fn try_new_unstable<D: DataProvider<CalendarChineseV1> + ?Sized>(
139        provider: &D,
140    ) -> Result<Self, DataError> {
141        Ok(Self {
142            data: Some(provider.load(Default::default())?.payload),
143        })
144    }
145
146    /// Construct a new [`Chinese`] without any precomputed calendrical calculations.
147    pub fn new_always_calculating() -> Self {
148        Chinese { data: None }
149    }
150
151    pub(crate) const DEBUG_NAME: &'static str = "Chinese";
152}
153
154impl crate::cal::scaffold::UnstableSealed for Chinese {}
155impl Calendar for Chinese {
156    type DateInner = ChineseDateInner;
157    type Year = types::CyclicYear;
158
159    // Construct a date from era/month codes and fields
160    fn from_codes(
161        &self,
162        era: Option<&str>,
163        year: i32,
164        month_code: types::MonthCode,
165        day: u8,
166    ) -> Result<Self::DateInner, DateError> {
167        match era {
168            None => {}
169            _ => return Err(DateError::UnknownEra),
170        }
171
172        let year = self.get_precomputed_data().load_or_compute_info(year);
173
174        let Some(month) = year.parse_month_code(month_code) else {
175            return Err(DateError::UnknownMonthCode(month_code));
176        };
177
178        year.validate_md(month, day)?;
179
180        Ok(ChineseDateInner(ArithmeticDate::new_unchecked(
181            year, month, day,
182        )))
183    }
184
185    fn from_rata_die(&self, rd: RataDie) -> Self::DateInner {
186        let iso = Iso.from_rata_die(rd);
187        let y = self
188            .get_precomputed_data()
189            .load_or_compute_info_for_rd(rd, iso.0);
190        let (m, d) = y.md_from_rd(rd);
191        ChineseDateInner(ArithmeticDate::new_unchecked(y, m, d))
192    }
193
194    fn to_rata_die(&self, date: &Self::DateInner) -> RataDie {
195        date.0.year.rd_from_md(date.0.month, date.0.day)
196    }
197
198    fn from_iso(&self, iso: IsoDateInner) -> Self::DateInner {
199        let rd = Iso.to_rata_die(&iso);
200        let y = self
201            .get_precomputed_data()
202            .load_or_compute_info_for_rd(rd, iso.0);
203        let (m, d) = y.md_from_rd(rd);
204        ChineseDateInner(ArithmeticDate::new_unchecked(y, m, d))
205    }
206
207    fn to_iso(&self, date: &Self::DateInner) -> IsoDateInner {
208        Iso.from_rata_die(self.to_rata_die(date))
209    }
210
211    // Count the number of months in a given year, specified by providing a date
212    // from that year
213    fn days_in_year(&self, date: &Self::DateInner) -> u16 {
214        date.0.days_in_year()
215    }
216
217    fn days_in_month(&self, date: &Self::DateInner) -> u8 {
218        date.0.days_in_month()
219    }
220
221    #[doc(hidden)] // unstable
222    fn offset_date(&self, date: &mut Self::DateInner, offset: DateDuration<Self>) {
223        date.0.offset_date(offset, &self.get_precomputed_data());
224    }
225
226    #[doc(hidden)] // unstable
227    #[allow(clippy::field_reassign_with_default)]
228    /// Calculate `date2 - date` as a duration
229    ///
230    /// `calendar2` is the calendar object associated with `date2`. In case the specific calendar objects
231    /// differ on date, the date for the first calendar is used, and `date2` may be converted if necessary.
232    fn until(
233        &self,
234        date1: &Self::DateInner,
235        date2: &Self::DateInner,
236        _calendar2: &Self,
237        _largest_unit: DateDurationUnit,
238        _smallest_unit: DateDurationUnit,
239    ) -> DateDuration<Self> {
240        date1.0.until(date2.0, _largest_unit, _smallest_unit)
241    }
242
243    /// Obtain a name for the calendar for debug printing
244    fn debug_name(&self) -> &'static str {
245        Self::DEBUG_NAME
246    }
247
248    fn year_info(&self, date: &Self::DateInner) -> Self::Year {
249        let year = date.0.year;
250        types::CyclicYear {
251            year: (year.related_iso - 4).rem_euclid(60) as u8 + 1,
252            related_iso: year.related_iso,
253        }
254    }
255
256    fn extended_year(&self, date: &Self::DateInner) -> i32 {
257        chinese_based::extended_from_iso::<chinese_based::Chinese>(date.0.year.related_iso)
258    }
259
260    fn is_in_leap_year(&self, date: &Self::DateInner) -> bool {
261        Self::provided_year_is_leap(date.0.year)
262    }
263
264    /// The calendar-specific month code represented by `date`;
265    /// since the Chinese calendar has leap months, an "L" is appended to the month code for
266    /// leap months. For example, in a year where an intercalary month is added after the second
267    /// month, the month codes for ordinal months 1, 2, 3, 4, 5 would be "M01", "M02", "M02L", "M03", "M04".
268    fn month(&self, date: &Self::DateInner) -> types::MonthInfo {
269        date.0.year.month(date.0.month)
270    }
271
272    /// The calendar-specific day-of-month represented by `date`
273    fn day_of_month(&self, date: &Self::DateInner) -> types::DayOfMonth {
274        date.0.day_of_month()
275    }
276
277    /// Information of the day of the year
278    fn day_of_year(&self, date: &Self::DateInner) -> types::DayOfYear {
279        types::DayOfYear(date.0.year.day_of_year(date.0.month, date.0.day))
280    }
281
282    fn calendar_algorithm(&self) -> Option<crate::preferences::CalendarAlgorithm> {
283        Some(crate::preferences::CalendarAlgorithm::Chinese)
284    }
285
286    fn months_in_year(&self, date: &Self::DateInner) -> u8 {
287        date.0.months_in_year()
288    }
289}
290
291impl<A: AsCalendar<Calendar = Chinese>> Date<A> {
292    /// Construct a new Chinese date from a `year`, `month`, and `day`.
293    /// `year` represents the [ISO](crate::Iso) year that roughly matches the Chinese year;
294    /// `month` represents the month of the year ordinally (ex. if it is a leap year, the last month will be 13, not 12);
295    /// `day` indicates the day of month
296    ///
297    /// This date will not use any precomputed calendrical calculations,
298    /// one that loads such data from a provider will be added in the future (#3933)
299    ///
300    /// ```rust
301    /// use icu::calendar::{cal::Chinese, Date};
302    ///
303    /// let chinese = Chinese::new_always_calculating();
304    ///
305    /// let date_chinese =
306    ///     Date::try_new_chinese_with_calendar(2023, 6, 11, chinese)
307    ///         .expect("Failed to initialize Chinese Date instance.");
308    ///
309    /// assert_eq!(date_chinese.cyclic_year().related_iso, 2023);
310    /// assert_eq!(date_chinese.cyclic_year().year, 40);
311    /// assert_eq!(date_chinese.month().ordinal, 6);
312    /// assert_eq!(date_chinese.day_of_month().0, 11);
313    /// ```
314    pub fn try_new_chinese_with_calendar(
315        related_iso_year: i32,
316        month: u8,
317        day: u8,
318        calendar: A,
319    ) -> Result<Date<A>, DateError> {
320        let year = calendar
321            .as_calendar()
322            .get_precomputed_data()
323            .load_or_compute_info(related_iso_year);
324        year.validate_md(month, day)?;
325        Ok(Date::from_raw(
326            ChineseDateInner(ArithmeticDate::new_unchecked(year, month, day)),
327            calendar,
328        ))
329    }
330}
331
332type ChineseCB = calendrical_calculations::chinese_based::Chinese;
333impl ChineseBasedWithDataLoading for Chinese {
334    type CB = ChineseCB;
335    fn get_precomputed_data(&self) -> ChineseBasedPrecomputedData<Self::CB> {
336        ChineseBasedPrecomputedData::new(self.data.as_ref().map(|d| d.get()))
337    }
338}
339
340#[cfg(test)]
341mod test {
342    use super::*;
343    use crate::types::MonthCode;
344    use calendrical_calculations::{iso::fixed_from_iso, rata_die::RataDie};
345    use tinystr::tinystr;
346
347    /// Run a test twice, with two calendars
348    fn do_twice(
349        chinese_calculating: &Chinese,
350        chinese_cached: &Chinese,
351        test: impl Fn(crate::Ref<Chinese>, &'static str),
352    ) {
353        test(crate::Ref(chinese_calculating), "calculating");
354        test(crate::Ref(chinese_cached), "cached");
355    }
356
357    #[test]
358    fn test_chinese_from_rd() {
359        #[derive(Debug)]
360        struct TestCase {
361            rd: i64,
362            expected_year: i32,
363            expected_month: u8,
364            expected_day: u8,
365        }
366
367        let cases = [
368            TestCase {
369                rd: -964192,
370                expected_year: -2,
371                expected_month: 1,
372                expected_day: 1,
373            },
374            TestCase {
375                rd: -963838,
376                expected_year: -1,
377                expected_month: 1,
378                expected_day: 1,
379            },
380            TestCase {
381                rd: -963129,
382                expected_year: 0,
383                expected_month: 13,
384                expected_day: 1,
385            },
386            TestCase {
387                rd: -963100,
388                expected_year: 0,
389                expected_month: 13,
390                expected_day: 30,
391            },
392            TestCase {
393                rd: -963099,
394                expected_year: 1,
395                expected_month: 1,
396                expected_day: 1,
397            },
398            TestCase {
399                rd: 738700,
400                expected_year: 4660,
401                expected_month: 6,
402                expected_day: 12,
403            },
404            TestCase {
405                rd: fixed_from_iso(2319, 2, 20).to_i64_date(),
406                expected_year: 2319 + 2636,
407                expected_month: 13,
408                expected_day: 30,
409            },
410            TestCase {
411                rd: fixed_from_iso(2319, 2, 21).to_i64_date(),
412                expected_year: 2319 + 2636 + 1,
413                expected_month: 1,
414                expected_day: 1,
415            },
416            TestCase {
417                rd: 738718,
418                expected_year: 4660,
419                expected_month: 6,
420                expected_day: 30,
421            },
422            TestCase {
423                rd: 738747,
424                expected_year: 4660,
425                expected_month: 7,
426                expected_day: 29,
427            },
428            TestCase {
429                rd: 738748,
430                expected_year: 4660,
431                expected_month: 8,
432                expected_day: 1,
433            },
434            TestCase {
435                rd: 738865,
436                expected_year: 4660,
437                expected_month: 11,
438                expected_day: 29,
439            },
440            TestCase {
441                rd: 738895,
442                expected_year: 4660,
443                expected_month: 12,
444                expected_day: 29,
445            },
446            TestCase {
447                rd: 738925,
448                expected_year: 4660,
449                expected_month: 13,
450                expected_day: 30,
451            },
452        ];
453
454        let chinese_calculating = Chinese::new_always_calculating();
455        let chinese_cached = Chinese::new();
456        for case in cases {
457            let rata_die = RataDie::new(case.rd);
458
459            do_twice(
460                &chinese_calculating,
461                &chinese_cached,
462                |chinese, calendar_type| {
463                    let chinese = Date::from_rata_die(rata_die, chinese);
464                    assert_eq!(
465                        case.expected_year,
466                        chinese.extended_year(),
467                        "[{calendar_type}] Chinese from RD failed, case: {case:?}"
468                    );
469                    assert_eq!(
470                        case.expected_month,
471                        chinese.month().ordinal,
472                        "[{calendar_type}] Chinese from RD failed, case: {case:?}"
473                    );
474                    assert_eq!(
475                        case.expected_day,
476                        chinese.day_of_month().0,
477                        "[{calendar_type}] Chinese from RD failed, case: {case:?}"
478                    );
479                },
480            );
481        }
482    }
483
484    #[test]
485    fn test_rd_from_chinese() {
486        #[derive(Debug)]
487        struct TestCase {
488            year: i32,
489            month: u8,
490            day: u8,
491            expected: i64,
492        }
493
494        let cases = [
495            TestCase {
496                year: 2023,
497                month: 6,
498                day: 6,
499                // June 23 2023
500                expected: 738694,
501            },
502            TestCase {
503                year: -2636,
504                month: 1,
505                day: 1,
506                expected: -963099,
507            },
508        ];
509
510        let chinese_calculating = Chinese::new_always_calculating();
511        let chinese_cached = Chinese::new();
512        for case in cases {
513            do_twice(
514                &chinese_calculating,
515                &chinese_cached,
516                |chinese, calendar_type| {
517                    let date = Date::try_new_chinese_with_calendar(
518                        case.year, case.month, case.day, chinese,
519                    )
520                    .unwrap();
521                    let rd = date.to_rata_die().to_i64_date();
522                    let expected = case.expected;
523                    assert_eq!(rd, expected, "[{calendar_type}] RD from Chinese failed, with expected: {expected} and calculated: {rd}, for test case: {case:?}");
524                },
525            );
526        }
527    }
528
529    #[test]
530    fn test_rd_chinese_roundtrip() {
531        let mut rd = -1963020;
532        let max_rd = 1963020;
533        let mut iters = 0;
534        let max_iters = 560;
535        let chinese_calculating = Chinese::new_always_calculating();
536        let chinese_cached = Chinese::new();
537        while rd < max_rd && iters < max_iters {
538            let rata_die = RataDie::new(rd);
539
540            do_twice(
541                &chinese_calculating,
542                &chinese_cached,
543                |chinese, calendar_type| {
544                    let chinese = Date::from_rata_die(rata_die, chinese);
545                    let result = chinese.to_rata_die();
546                    assert_eq!(result, rata_die, "[{calendar_type}] Failed roundtrip RD -> Chinese -> RD for RD: {rata_die:?}, with calculated: {result:?} from Chinese date:\n{chinese:?}");
547                },
548            );
549            rd += 7043;
550            iters += 1;
551        }
552    }
553
554    #[test]
555    fn test_chinese_epoch() {
556        let iso = Date::try_new_iso(-2636, 2, 15).unwrap();
557
558        do_twice(
559            &Chinese::new_always_calculating(),
560            &Chinese::new(),
561            |chinese, _calendar_type| {
562                let chinese = iso.to_calendar(chinese);
563
564                assert_eq!(chinese.cyclic_year().related_iso, -2636);
565                assert_eq!(chinese.month().ordinal, 1);
566                assert_eq!(chinese.month().standard_code.0, "M01");
567                assert_eq!(chinese.day_of_month().0, 1);
568                assert_eq!(chinese.cyclic_year().year, 1);
569                assert_eq!(chinese.cyclic_year().related_iso, -2636);
570            },
571        )
572    }
573
574    #[test]
575    fn test_iso_to_chinese_negative_years() {
576        #[derive(Debug)]
577        struct TestCase {
578            iso_year: i32,
579            iso_month: u8,
580            iso_day: u8,
581            expected_year: i32,
582            expected_month: u8,
583            expected_day: u8,
584        }
585
586        let cases = [
587            TestCase {
588                iso_year: -2636,
589                iso_month: 2,
590                iso_day: 14,
591                expected_year: -2637,
592                expected_month: 13,
593                expected_day: 30,
594            },
595            TestCase {
596                iso_year: -2636,
597                iso_month: 1,
598                iso_day: 15,
599                expected_year: -2637,
600                expected_month: 12,
601                expected_day: 30,
602            },
603        ];
604
605        let chinese_calculating = Chinese::new_always_calculating();
606        let chinese_cached = Chinese::new();
607
608        for case in cases {
609            let iso = Date::try_new_iso(case.iso_year, case.iso_month, case.iso_day).unwrap();
610            do_twice(
611                &chinese_calculating,
612                &chinese_cached,
613                |chinese, calendar_type| {
614                    let chinese = iso.to_calendar(chinese);
615                    assert_eq!(
616                        case.expected_year,
617                        chinese.cyclic_year().related_iso,
618                        "[{calendar_type}] ISO to Chinese failed for case: {case:?}"
619                    );
620                    assert_eq!(
621                        case.expected_month,
622                        chinese.month().ordinal,
623                        "[{calendar_type}] ISO to Chinese failed for case: {case:?}"
624                    );
625                    assert_eq!(
626                        case.expected_day,
627                        chinese.day_of_month().0,
628                        "[{calendar_type}] ISO to Chinese failed for case: {case:?}"
629                    );
630                },
631            );
632        }
633    }
634
635    #[test]
636    fn test_chinese_leap_months() {
637        let expected = [
638            (1933, 6),
639            (1938, 8),
640            (1984, 11),
641            (2009, 6),
642            (2017, 7),
643            (2028, 6),
644        ];
645        let chinese_calculating = Chinese::new_always_calculating();
646        let chinese_cached = Chinese::new();
647
648        for case in expected {
649            let year = case.0;
650            let expected_month = case.1;
651            let iso = Date::try_new_iso(year, 6, 1).unwrap();
652            do_twice(
653                &chinese_calculating,
654                &chinese_cached,
655                |chinese, calendar_type| {
656                    let chinese_date = iso.to_calendar(chinese);
657                    assert!(
658                        chinese_date.is_in_leap_year(),
659                        "[{calendar_type}] {year} should be a leap year"
660                    );
661                    let new_year = chinese_date.inner.0.year.new_year();
662                    assert_eq!(
663                        expected_month,
664                        calendrical_calculations::chinese_based::get_leap_month_from_new_year::<
665                            calendrical_calculations::chinese_based::Chinese,
666                        >(new_year),
667                        "[{calendar_type}] {year} have leap month {expected_month}"
668                    );
669                },
670            );
671        }
672    }
673
674    #[test]
675    fn test_month_days() {
676        let year =
677            ChineseBasedPrecomputedData::<<Chinese as ChineseBasedWithDataLoading>::CB>::default()
678                .load_or_compute_info(2023);
679        let cases = [
680            (1, 29),
681            (2, 30),
682            (3, 29),
683            (4, 29),
684            (5, 30),
685            (6, 30),
686            (7, 29),
687            (8, 30),
688            (9, 30),
689            (10, 29),
690            (11, 30),
691            (12, 29),
692            (13, 30),
693        ];
694        for case in cases {
695            let days_in_month = Chinese::days_in_provided_month(year, case.0);
696            assert_eq!(
697                case.1, days_in_month,
698                "month_days test failed for case: {case:?}"
699            );
700        }
701    }
702
703    #[test]
704    fn test_ordinal_to_month_code() {
705        #[derive(Debug)]
706        struct TestCase {
707            year: i32,
708            month: u8,
709            day: u8,
710            expected_code: &'static str,
711        }
712
713        let cases = [
714            TestCase {
715                year: 2023,
716                month: 1,
717                day: 9,
718                expected_code: "M12",
719            },
720            TestCase {
721                year: 2023,
722                month: 2,
723                day: 9,
724                expected_code: "M01",
725            },
726            TestCase {
727                year: 2023,
728                month: 3,
729                day: 9,
730                expected_code: "M02",
731            },
732            TestCase {
733                year: 2023,
734                month: 4,
735                day: 9,
736                expected_code: "M02L",
737            },
738            TestCase {
739                year: 2023,
740                month: 5,
741                day: 9,
742                expected_code: "M03",
743            },
744            TestCase {
745                year: 2023,
746                month: 6,
747                day: 9,
748                expected_code: "M04",
749            },
750            TestCase {
751                year: 2023,
752                month: 7,
753                day: 9,
754                expected_code: "M05",
755            },
756            TestCase {
757                year: 2023,
758                month: 8,
759                day: 9,
760                expected_code: "M06",
761            },
762            TestCase {
763                year: 2023,
764                month: 9,
765                day: 9,
766                expected_code: "M07",
767            },
768            TestCase {
769                year: 2023,
770                month: 10,
771                day: 9,
772                expected_code: "M08",
773            },
774            TestCase {
775                year: 2023,
776                month: 11,
777                day: 9,
778                expected_code: "M09",
779            },
780            TestCase {
781                year: 2023,
782                month: 12,
783                day: 9,
784                expected_code: "M10",
785            },
786            TestCase {
787                year: 2024,
788                month: 1,
789                day: 9,
790                expected_code: "M11",
791            },
792            TestCase {
793                year: 2024,
794                month: 2,
795                day: 9,
796                expected_code: "M12",
797            },
798            TestCase {
799                year: 2024,
800                month: 2,
801                day: 10,
802                expected_code: "M01",
803            },
804        ];
805
806        let chinese_calculating = Chinese::new_always_calculating();
807        let chinese_cached = Chinese::new();
808
809        for case in cases {
810            let iso = Date::try_new_iso(case.year, case.month, case.day).unwrap();
811            do_twice(
812                &chinese_calculating,
813                &chinese_cached,
814                |chinese, calendar_type| {
815                    let chinese = iso.to_calendar(chinese);
816                    let result_code = chinese.month().standard_code.0;
817                    let expected_code = case.expected_code.to_string();
818                    assert_eq!(
819                        expected_code, result_code,
820                        "[{calendar_type}] Month codes did not match for test case: {case:?}"
821                    );
822                },
823            );
824        }
825    }
826
827    #[test]
828    fn test_month_code_to_ordinal() {
829        // construct using ::default() to force recomputation
830        let year =
831            ChineseBasedPrecomputedData::<<Chinese as ChineseBasedWithDataLoading>::CB>::default()
832                .load_or_compute_info(2023);
833        let codes = [
834            (1, tinystr!(4, "M01")),
835            (2, tinystr!(4, "M02")),
836            (3, tinystr!(4, "M02L")),
837            (4, tinystr!(4, "M03")),
838            (5, tinystr!(4, "M04")),
839            (6, tinystr!(4, "M05")),
840            (7, tinystr!(4, "M06")),
841            (8, tinystr!(4, "M07")),
842            (9, tinystr!(4, "M08")),
843            (10, tinystr!(4, "M09")),
844            (11, tinystr!(4, "M10")),
845            (12, tinystr!(4, "M11")),
846            (13, tinystr!(4, "M12")),
847        ];
848        for ordinal_code_pair in codes {
849            let code = MonthCode(ordinal_code_pair.1);
850            let ordinal = year.parse_month_code(code);
851            assert_eq!(
852                ordinal,
853                Some(ordinal_code_pair.0),
854                "Code to ordinal failed for year: {}, code: {code}",
855                year.related_iso
856            );
857        }
858    }
859
860    #[test]
861    fn check_invalid_month_code_to_ordinal() {
862        let non_leap_year = 4659;
863        let leap_year = 4660;
864        let invalid_codes = [
865            (non_leap_year, tinystr!(4, "M2")),
866            (leap_year, tinystr!(4, "M0")),
867            (non_leap_year, tinystr!(4, "J01")),
868            (leap_year, tinystr!(4, "3M")),
869            (non_leap_year, tinystr!(4, "M04L")),
870            (leap_year, tinystr!(4, "M04L")),
871            (non_leap_year, tinystr!(4, "M13")),
872            (leap_year, tinystr!(4, "M13")),
873        ];
874        for (year, code) in invalid_codes {
875            // construct using ::default() to force recomputation
876            let year = ChineseBasedPrecomputedData::<
877                <Chinese as ChineseBasedWithDataLoading>::CB,
878            >::default()
879            .load_or_compute_info(year);
880            let code = MonthCode(code);
881            let ordinal = year.parse_month_code(code);
882            assert_eq!(
883                ordinal, None,
884                "Invalid month code failed for year: {}, code: {code}",
885                year.related_iso
886            );
887        }
888    }
889
890    #[test]
891    fn test_iso_chinese_roundtrip() {
892        let chinese_calculating = Chinese::new_always_calculating();
893        let chinese_cached = Chinese::new();
894
895        for i in -1000..=1000 {
896            let year = i;
897            let month = i as u8 % 12 + 1;
898            let day = i as u8 % 28 + 1;
899            let iso = Date::try_new_iso(year, month, day).unwrap();
900            do_twice(
901                &chinese_calculating,
902                &chinese_cached,
903                |chinese, calendar_type| {
904                    let chinese = iso.to_calendar(chinese);
905                    let result = chinese.to_calendar(Iso);
906                    assert_eq!(iso, result, "[{calendar_type}] ISO to Chinese roundtrip failed!\nIso: {iso:?}\nChinese: {chinese:?}\nResult: {result:?}");
907                },
908            );
909        }
910    }
911
912    #[test]
913    fn test_consistent_with_icu() {
914        #[derive(Debug)]
915        struct TestCase {
916            iso_year: i32,
917            iso_month: u8,
918            iso_day: u8,
919            expected_rel_iso: i32,
920            expected_cyclic: u8,
921            expected_month: u8,
922            expected_day: u8,
923        }
924
925        let cases = [
926            TestCase {
927                iso_year: -2332,
928                iso_month: 3,
929                iso_day: 1,
930                expected_rel_iso: -2332,
931                expected_cyclic: 5,
932                expected_month: 1,
933                expected_day: 16,
934            },
935            TestCase {
936                iso_year: -2332,
937                iso_month: 2,
938                iso_day: 15,
939                expected_rel_iso: -2332,
940                expected_cyclic: 5,
941                expected_month: 1,
942                expected_day: 1,
943            },
944            TestCase {
945                // This test case fails to match ICU
946                iso_year: -2332,
947                iso_month: 2,
948                iso_day: 14,
949                expected_rel_iso: -2333,
950                expected_cyclic: 4,
951                expected_month: 13,
952                expected_day: 30,
953            },
954            TestCase {
955                // This test case fails to match ICU
956                iso_year: -2332,
957                iso_month: 1,
958                iso_day: 17,
959                expected_rel_iso: -2333,
960                expected_cyclic: 4,
961                expected_month: 13,
962                expected_day: 2,
963            },
964            TestCase {
965                // This test case fails to match ICU
966                iso_year: -2332,
967                iso_month: 1,
968                iso_day: 16,
969                expected_rel_iso: -2333,
970                expected_cyclic: 4,
971                expected_month: 13,
972                expected_day: 1,
973            },
974            TestCase {
975                iso_year: -2332,
976                iso_month: 1,
977                iso_day: 15,
978                expected_rel_iso: -2333,
979                expected_cyclic: 4,
980                expected_month: 12,
981                expected_day: 29,
982            },
983            TestCase {
984                iso_year: -2332,
985                iso_month: 1,
986                iso_day: 1,
987                expected_rel_iso: -2333,
988                expected_cyclic: 4,
989                expected_month: 12,
990                expected_day: 15,
991            },
992            TestCase {
993                iso_year: -2333,
994                iso_month: 1,
995                iso_day: 16,
996                expected_rel_iso: -2334,
997                expected_cyclic: 3,
998                expected_month: 12,
999                expected_day: 19,
1000            },
1001        ];
1002
1003        let chinese_calculating = Chinese::new_always_calculating();
1004        let chinese_cached = Chinese::new();
1005
1006        for case in cases {
1007            let iso = Date::try_new_iso(case.iso_year, case.iso_month, case.iso_day).unwrap();
1008
1009            do_twice(
1010                &chinese_calculating,
1011                &chinese_cached,
1012                |chinese, calendar_type| {
1013                    let chinese = iso.to_calendar(chinese);
1014                    let chinese_rel_iso = chinese.cyclic_year().related_iso;
1015                    let chinese_cyclic = chinese.cyclic_year().year;
1016                    let chinese_month = chinese.month().ordinal;
1017                    let chinese_day = chinese.day_of_month().0;
1018
1019                    assert_eq!(
1020                        chinese_rel_iso, case.expected_rel_iso,
1021                        "[{calendar_type}] Related ISO failed for test case: {case:?}"
1022                    );
1023                    assert_eq!(
1024                        chinese_cyclic, case.expected_cyclic,
1025                        "[{calendar_type}] Cyclic year failed for test case: {case:?}"
1026                    );
1027                    assert_eq!(
1028                        chinese_month, case.expected_month,
1029                        "[{calendar_type}] Month failed for test case: {case:?}"
1030                    );
1031                    assert_eq!(
1032                        chinese_day, case.expected_day,
1033                        "[{calendar_type}] Day failed for test case: {case:?}"
1034                    );
1035                },
1036            );
1037        }
1038    }
1039}