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