Coverage Report

Created: 2025-07-18 06:22

/rust/registry/src/index.crates.io-6f17d22bba15001f/icu_calendar-1.5.2/src/chinese.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 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
42
use crate::any_calendar::AnyCalendarKind;
43
use crate::calendar_arithmetic::CalendarArithmetic;
44
use crate::calendar_arithmetic::PrecomputedDataSource;
45
use crate::chinese_based::{
46
    chinese_based_ordinal_lunar_month_from_code, ChineseBasedDateInner,
47
    ChineseBasedPrecomputedData, ChineseBasedWithDataLoading, ChineseBasedYearInfo,
48
};
49
use crate::iso::Iso;
50
use crate::provider::chinese_based::ChineseCacheV1Marker;
51
use crate::types::{Era, FormattableYear};
52
use crate::AsCalendar;
53
use crate::{types, Calendar, CalendarError, Date, DateDuration, DateDurationUnit, DateTime, Time};
54
use core::cmp::Ordering;
55
use core::num::NonZeroU8;
56
use icu_provider::prelude::*;
57
use 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)]
108
pub 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)]
114
pub struct ChineseDateInner(ChineseBasedDateInner<Chinese>);
115
116
type Inner = ChineseBasedDateInner<Chinese>;
117
118
// we want these impls without the `C: Copy/Clone` bounds
119
impl Copy for ChineseDateInner {}
120
impl Clone for ChineseDateInner {
121
0
    fn clone(&self) -> Self {
122
0
        *self
123
0
    }
124
}
125
126
// These impls just make custom derives on types containing C
127
// work. They're basically no-ops
128
impl PartialEq for Chinese {
129
0
    fn eq(&self, _: &Self) -> bool {
130
0
        true
131
0
    }
132
}
133
impl Eq for Chinese {}
134
#[allow(clippy::non_canonical_partial_ord_impl)] // this is intentional
135
impl PartialOrd for Chinese {
136
0
    fn partial_cmp(&self, _: &Self) -> Option<Ordering> {
137
0
        Some(Ordering::Equal)
138
0
    }
139
}
140
141
impl Ord for Chinese {
142
0
    fn cmp(&self, _: &Self) -> Ordering {
143
0
        Ordering::Equal
144
0
    }
145
}
146
147
impl 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
0
    pub const fn new() -> Self {
155
0
        Self {
156
0
            data: Some(DataPayload::from_static_ref(
157
0
                crate::provider::Baked::SINGLETON_CALENDAR_CHINESECACHE_V1,
158
0
            )),
159
0
        }
160
0
    }
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
0
    pub fn try_new_unstable<D: DataProvider<ChineseCacheV1Marker> + ?Sized>(
174
0
        provider: &D,
175
0
    ) -> Result<Self, CalendarError> {
176
0
        Ok(Self {
177
0
            data: Some(provider.load(Default::default())?.take_payload()?),
178
        })
179
0
    }
Unexecuted instantiation: <icu_calendar::chinese::Chinese>::try_new_unstable::<icu_provider::any::DowncastingAnyProvider<icu_provider_adapters::empty::EmptyDataProvider>>
Unexecuted instantiation: <icu_calendar::chinese::Chinese>::try_new_unstable::<_>
180
181
    /// Construct a new [`Chinese`] without any precomputed calendrical calculations.
182
0
    pub fn new_always_calculating() -> Self {
183
0
        Chinese { data: None }
184
0
    }
185
186
    pub(crate) const DEBUG_NAME: &'static str = "Chinese";
187
}
188
189
impl Calendar for Chinese {
190
    type DateInner = ChineseDateInner;
191
192
    // Construct a date from era/month codes and fields
193
0
    fn date_from_codes(
194
0
        &self,
195
0
        era: types::Era,
196
0
        year: i32,
197
0
        month_code: types::MonthCode,
198
0
        day: u8,
199
0
    ) -> Result<Self::DateInner, CalendarError> {
200
0
        let year_info = self.get_precomputed_data().load_or_compute_info(year);
201
202
0
        let month = if let Some(ordinal) =
203
0
            chinese_based_ordinal_lunar_month_from_code(month_code, year_info)
204
        {
205
0
            ordinal
206
        } else {
207
0
            return Err(CalendarError::UnknownMonthCode(
208
0
                month_code.0,
209
0
                self.debug_name(),
210
0
            ));
211
        };
212
213
0
        if era.0 != tinystr!(16, "chinese") {
214
0
            return Err(CalendarError::UnknownEra(era.0, self.debug_name()));
215
0
        }
216
0
217
0
        let arithmetic = Inner::new_from_ordinals(year, month, day, year_info);
218
0
        Ok(ChineseDateInner(ChineseBasedDateInner(arithmetic?)))
219
0
    }
220
221
    // Construct the date from an ISO date
222
0
    fn date_from_iso(&self, iso: Date<Iso>) -> Self::DateInner {
223
0
        let fixed = Iso::fixed_from_iso(iso.inner);
224
0
        ChineseDateInner(Inner::chinese_based_date_from_fixed(
225
0
            self,
226
0
            fixed,
227
0
            iso.inner.0,
228
0
        ))
229
0
    }
230
231
    // Obtain an ISO date from a Chinese date
232
0
    fn date_to_iso(&self, date: &Self::DateInner) -> Date<Iso> {
233
0
        let fixed = Inner::fixed_from_chinese_based_date_inner(date.0);
234
0
        Iso::iso_from_fixed(fixed)
235
0
    }
236
237
    //Count the number of months in a given year, specified by providing a date
238
    // from that year
239
0
    fn days_in_year(&self, date: &Self::DateInner) -> u16 {
240
0
        date.0.days_in_year_inner()
241
0
    }
242
243
0
    fn days_in_month(&self, date: &Self::DateInner) -> u8 {
244
0
        date.0.days_in_month_inner()
245
0
    }
246
247
    #[doc(hidden)] // unstable
248
0
    fn offset_date(&self, date: &mut Self::DateInner, offset: DateDuration<Self>) {
249
0
        date.0 .0.offset_date(offset, &self.get_precomputed_data());
250
0
    }
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
0
    fn until(
259
0
        &self,
260
0
        date1: &Self::DateInner,
261
0
        date2: &Self::DateInner,
262
0
        _calendar2: &Self,
263
0
        _largest_unit: DateDurationUnit,
264
0
        _smallest_unit: DateDurationUnit,
265
0
    ) -> DateDuration<Self> {
266
0
        date1.0 .0.until(date2.0 .0, _largest_unit, _smallest_unit)
267
0
    }
268
269
    /// Obtain a name for the calendar for debug printing
270
0
    fn debug_name(&self) -> &'static str {
271
0
        Self::DEBUG_NAME
272
0
    }
273
274
    /// The calendar-specific year represented by `date`
275
0
    fn year(&self, date: &Self::DateInner) -> types::FormattableYear {
276
0
        Self::format_chinese_year(date.0 .0.year, Some(date.0 .0.year_info))
277
0
    }
278
279
0
    fn is_in_leap_year(&self, date: &Self::DateInner) -> bool {
280
0
        Self::is_leap_year(date.0 .0.year, date.0 .0.year_info)
281
0
    }
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
0
    fn month(&self, date: &Self::DateInner) -> types::FormattableMonth {
288
0
        date.0.month()
289
0
    }
290
291
    /// The calendar-specific day-of-month represented by `date`
292
0
    fn day_of_month(&self, date: &Self::DateInner) -> types::DayOfMonth {
293
0
        types::DayOfMonth(date.0 .0.day as u32)
294
0
    }
295
296
    /// Information of the day of the year
297
0
    fn day_of_year_info(&self, date: &Self::DateInner) -> types::DayOfYearInfo {
298
0
        let prev_year = date.0 .0.year.saturating_sub(1);
299
0
        let next_year = date.0 .0.year.saturating_add(1);
300
0
        types::DayOfYearInfo {
301
0
            day_of_year: date.0.day_of_year(),
302
0
            days_in_year: date.0.days_in_year_inner(),
303
0
            prev_year: Self::format_chinese_year(prev_year, None),
304
0
            days_in_prev_year: date.0.days_in_prev_year(),
305
0
            next_year: Self::format_chinese_year(next_year, None),
306
0
        }
307
0
    }
308
309
    /// The [`AnyCalendarKind`] corresponding to this calendar
310
0
    fn any_calendar_kind(&self) -> Option<AnyCalendarKind> {
311
0
        Some(AnyCalendarKind::Chinese)
312
0
    }
313
314
0
    fn months_in_year(&self, date: &Self::DateInner) -> u8 {
315
0
        date.0.months_in_year_inner()
316
0
    }
317
}
318
319
impl<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
0
    pub fn try_new_chinese_date_with_calendar(
344
0
        year: i32,
345
0
        month: u8,
346
0
        day: u8,
347
0
        calendar: A,
348
0
    ) -> Result<Date<A>, CalendarError> {
349
0
        let year_info = calendar
350
0
            .as_calendar()
351
0
            .get_precomputed_data()
352
0
            .load_or_compute_info(year);
353
0
        let arithmetic = Inner::new_from_ordinals(year, month, day, year_info);
354
0
        Ok(Date::from_raw(
355
0
            ChineseDateInner(ChineseBasedDateInner(arithmetic?)),
356
0
            calendar,
357
        ))
358
0
    }
359
}
360
361
impl<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
0
    pub fn try_new_chinese_datetime_with_calendar(
388
0
        year: i32,
389
0
        month: u8,
390
0
        day: u8,
391
0
        hour: u8,
392
0
        minute: u8,
393
0
        second: u8,
394
0
        calendar: A,
395
0
    ) -> Result<DateTime<A>, CalendarError> {
396
0
        Ok(DateTime {
397
0
            date: Date::try_new_chinese_date_with_calendar(year, month, day, calendar)?,
398
0
            time: Time::try_new(hour, minute, second, 0)?,
399
        })
400
0
    }
401
}
402
403
type ChineseCB = calendrical_calculations::chinese_based::Chinese;
404
impl ChineseBasedWithDataLoading for Chinese {
405
    type CB = ChineseCB;
406
0
    fn get_precomputed_data(&self) -> ChineseBasedPrecomputedData<Self::CB> {
407
0
        ChineseBasedPrecomputedData::new(self.data.as_ref().map(|d| d.get()))
408
0
    }
409
}
410
411
impl 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
0
    fn format_chinese_year(
420
0
        year: i32,
421
0
        year_info_option: Option<ChineseBasedYearInfo>,
422
0
    ) -> FormattableYear {
423
0
        let era = Era(tinystr!(16, "chinese"));
424
0
        let number = year;
425
0
        let cyclic = (number - 1).rem_euclid(60) as u8;
426
0
        let cyclic = NonZeroU8::new(cyclic + 1); // 1-indexed
427
0
        let rata_die_in_year = if let Some(info) = year_info_option {
428
0
            info.new_year::<ChineseCB>(year)
429
        } else {
430
0
            Inner::fixed_mid_year_from_year(number)
431
        };
432
0
        let iso_formattable_year = Iso::iso_from_fixed(rata_die_in_year).year();
433
0
        let related_iso = Some(iso_formattable_year.number);
434
0
        types::FormattableYear {
435
0
            era,
436
0
            number,
437
0
            cyclic,
438
0
            related_iso,
439
0
        }
440
0
    }
441
}
442
443
#[cfg(test)]
444
mod 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
}