icu_calendar/cal/
east_asian_traditional.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
5use crate::cal::iso::Iso;
6use crate::calendar_arithmetic::DateFieldsResolver;
7use crate::calendar_arithmetic::{ArithmeticDate, ToExtendedYear};
8use crate::error::{
9    DateError, DateFromFieldsError, EcmaReferenceYearError, MonthCodeError, UnknownEraError,
10};
11use crate::options::{DateAddOptions, DateDifferenceOptions};
12use crate::options::{DateFromFieldsOptions, Overflow};
13use crate::types::ValidMonthCode;
14use crate::AsCalendar;
15use crate::{types, Calendar, Date};
16use calendrical_calculations::chinese_based::{
17    self, ChineseBased, YearBounds, WELL_BEHAVED_ASTRONOMICAL_RANGE,
18};
19use calendrical_calculations::rata_die::RataDie;
20use icu_locale_core::preferences::extensions::unicode::keywords::CalendarAlgorithm;
21use icu_provider::prelude::*;
22
23#[path = "east_asian_traditional/china_data.rs"]
24mod china_data;
25#[path = "east_asian_traditional/korea_data.rs"]
26mod korea_data;
27#[path = "east_asian_traditional/qing_data.rs"]
28mod qing_data;
29#[path = "east_asian_traditional/simple.rs"]
30mod simple;
31
32/// The traditional East-Asian lunisolar calendar.
33///
34/// This calendar used traditionally in China as well as in other countries in East Asia is
35/// often used today to track important cultural events and holidays like the Lunar New Year.
36///
37/// The type parameter specifies a particular set of calculation rules and local
38/// time information, which differs by country and over time.
39/// It must implement the currently-unstable `Rules` trait, at the moment this crate exports two stable
40/// implementors of `Rules`: [`China`] and [`Korea`]. Please comment on [this issue](https://github.com/unicode-org/icu4x/issues/6962)
41/// if you would like to see this trait stabilized.
42///
43/// This corresponds to the `"chinese"` and `"dangi"` [CLDR calendars](https://unicode.org/reports/tr35/#UnicodeCalendarIdentifier)
44/// respectively, when used with the [`China`] and [`Korea`] [`Rules`] types.
45///
46/// # Year and Era codes
47///
48/// Unlike most calendars, the traditional East-Asian calendar does not traditionally count years in an infinitely
49/// increasing sequence. Instead, 10 "celestial stems" and 12 "terrestrial branches" are combined to form a
50/// cycle of year names which repeats every 60 years. However, for the purposes of calendar calculations and
51/// conversions, this calendar also counts years based on the [`Gregorian`](crate::cal::Gregorian) (ISO) calendar.
52/// This "related ISO year" marks the Gregorian year in which a traditional East-Asian year begins.
53///
54/// Because the traditional East-Asian calendar does not traditionally count years, era codes are not used in this calendar.
55///
56/// For more information, suggested reading materials include:
57/// * _Calendrical Calculations_ by Reingold & Dershowitz
58/// * _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>
59/// * Wikipedia: <https://en.wikipedia.org/wiki/Chinese_calendar>
60///
61/// # Months and days
62///
63/// The 12 months (`M01`-`M12`) don't use names in modern usage, instead they are referred to as
64/// e.g. δΈ‰ζœˆ (third month) using Chinese characters.
65///
66/// As a lunar calendar, the lengths of the months depend on the lunar cycle (a month starts on the day of
67/// local new moon), and will be either 29 or 30 days. As 12 such months fall short of a solar year, a leap
68/// month is inserted roughly every 3 years; this can be after any month (e.g. `M02L`).
69///
70/// Both the lengths of the months and the occurence of leap months are determined by the
71/// concrete [`Rules`] implementation.
72///
73/// The length of the year is 353-355 days, and the length of the leap year 383-385 days.
74///
75/// # Calendar drift
76///
77/// As leap months are determined with respect to the solar year, this calendar stays anchored
78/// to the seasons.
79#[derive(Clone, Debug, Default, Copy, PartialEq, Eq, PartialOrd, Ord)]
80#[allow(clippy::exhaustive_structs)] // newtype
81pub struct EastAsianTraditional<R>(pub R);
82
83/// The rules for the [`EastAsianTraditional`] calendar.
84///
85/// The calendar depends on both astronomical calculations and local time.
86/// The rules for how to perform these calculations, as well as how local
87/// time is determined differ between countries and have changed over time.
88///
89/// This crate currently provides [`Rules`] for [`China`] and [`Korea`].
90///
91/// <div class="stab unstable">
92/// 🚫 This trait is sealed; it should not be implemented by user code. If an API requests an item that implements this
93/// trait, please consider using a type from the implementors listed below.
94///
95/// It is still possible to implement this trait in userland (since `UnstableSealed` is public),
96/// do not do so unless you are prepared for things to occasionally break.
97/// </div>
98pub trait Rules: Clone + core::fmt::Debug + crate::cal::scaffold::UnstableSealed {
99    /// Returns data about the given year.
100    fn year_data(&self, related_iso: i32) -> EastAsianTraditionalYearData;
101
102    /// Returns an ECMA reference year that contains the given month-day combination.
103    ///
104    /// If the day is out of range, it will return a year that contains the given month
105    /// and the maximum day possible for that month. See [the spec][spec] for the
106    /// precise algorithm used.
107    ///
108    /// This API only matters when using [`MissingFieldsStrategy::Ecma`] to compute
109    /// a date without providing a year in [`Date::try_from_fields()`]. The default impl
110    /// will just error, and custom calendars who do not care about ECMA/Temporal
111    /// reference years do not need to override this.
112    ///
113    /// [spec]: https://tc39.es/proposal-temporal/#sec-temporal-nonisomonthdaytoisoreferencedate
114    /// [`MissingFieldsStrategy::Ecma`]: crate::options::MissingFieldsStrategy::Ecma
115    fn ecma_reference_year(
116        &self,
117        // TODO: Consider accepting ValidMonthCode
118        _month_code: (u8, bool),
119        _day: u8,
120    ) -> Result<i32, EcmaReferenceYearError> {
121        Err(EcmaReferenceYearError::Unimplemented)
122    }
123
124    /// The debug name for the calendar defined by these [`Rules`].
125    fn debug_name(&self) -> &'static str {
126        "Chinese (custom)"
127    }
128
129    /// The BCP-47 [`CalendarAlgorithm`] for the calendar defined by these [`Rules`], if defined.
130    fn calendar_algorithm(&self) -> Option<CalendarAlgorithm> {
131        None
132    }
133}
134
135/// The [Chinese](https://en.wikipedia.org/wiki/Chinese_calendar) variant of the [`EastAsianTraditional`] calendar.
136///
137/// This type agrees with the official data published by the
138/// [Purple Mountain Observatory for the years 1900-2025], as well as with
139/// the data published by the [Hong Kong Observatory for the years 1901-2100].
140///
141/// For years since 1912, this uses the [GB/T 33661-2017] rules.
142/// As accurate computation is computationally expensive, years until
143/// 2100 are precomputed, and after that this type regresses to a simplified
144/// calculation. If accuracy beyond 2100 is required, clients
145/// can implement their own [`Rules`] type containing more precomputed data.
146/// We note that the calendar is inherently uncertain for some future dates.
147///
148/// Before 1912 [different rules](https://ytliu.epizy.com/Shixian/Shixian_summary.html)
149/// were used. This type produces correct data for the years 1900-1912, and
150/// falls back to a simplified calculation before 1900. If accuracy is
151/// required before 1900, clients can implement their own [`Rules`] type
152/// using data such as from the excellent compilation by [Yuk Tung Liu].
153///
154/// The precise behavior of this calendar may change in the future if:
155/// - New ground truth is established by published government sources
156/// - We decide to tweak the simplified calculation
157/// - We decide to expand or reduce the range where we are correctly handling past dates.
158///
159/// [Purple Mountain Observatory for the years 1900-2025]: http://www.pmo.cas.cn/xwdt2019/kpdt2019/202203/P020250414456381274062.pdf
160/// [Hong Kong Observatory for the years 1901-2100]: https://www.hko.gov.hk/en/gts/time/conversion.htm
161/// [GB/T 33661-2017]: China::gb_t_33661_2017
162/// [Yuk Tung Liu]: https://ytliu0.github.io/ChineseCalendar/table.html
163pub type ChineseTraditional = EastAsianTraditional<China>;
164
165/// The [`Rules`] used in China.
166///
167/// See [`ChineseTraditional`] for more information.
168#[derive(Copy, Clone, Debug, Default)]
169#[non_exhaustive]
170pub struct China;
171
172impl China {
173    /// Computes [`EastAsianTraditionalYearData`] according to [GB/T 33661-2017],
174    /// as implemented by [`calendrical_calculations::chinese_based::Chinese`].
175    ///
176    /// The rules specified in [GB/T 33661-2017] have only been used
177    /// since 1912, applying them proleptically to years before 1912 will not
178    /// necessarily match historical calendars.
179    ///
180    /// Note that for future years there is a small degree of uncertainty, as
181    /// [GB/T 33661-2017] depends on the uncertain future [difference between UT1
182    /// and UTC](https://en.wikipedia.org/wiki/Leap_second#Future).
183    /// As noted by
184    /// [Yuk Tung Liu](https://ytliu0.github.io/ChineseCalendar/computation.html#modern),
185    /// years as early as 2057, 2089, and 2097 have lunar events very close to
186    /// local midnight, which might affect the start of a (single) month if additional
187    /// leap seconds are introduced.
188    ///
189    /// [GB/T 33661-2017]: https://openstd.samr.gov.cn/bzgk/gb/newGbInfo?hcno=E107EA4DE9725EDF819F33C60A44B296
190    pub fn gb_t_33661_2017(related_iso: i32) -> EastAsianTraditionalYearData {
191        EastAsianTraditionalYearData::calendrical_calculations::<chinese_based::Chinese>(
192            related_iso,
193        )
194    }
195}
196
197impl crate::cal::scaffold::UnstableSealed for China {}
198impl Rules for China {
199    fn year_data(&self, related_iso: i32) -> EastAsianTraditionalYearData {
200        if let Some(year) = EastAsianTraditionalYearData::lookup(
201            related_iso,
202            china_data::STARTING_YEAR,
203            china_data::DATA,
204        ) {
205            year
206        } else if related_iso > china_data::STARTING_YEAR {
207            EastAsianTraditionalYearData::simple(simple::UTC_PLUS_8, related_iso)
208        } else if let Some(year) = EastAsianTraditionalYearData::lookup(
209            related_iso,
210            qing_data::STARTING_YEAR,
211            qing_data::DATA,
212        ) {
213            year
214        } else {
215            EastAsianTraditionalYearData::simple(simple::BEIJING_UTC_OFFSET, related_iso)
216        }
217    }
218
219    fn ecma_reference_year(
220        &self,
221        month_code: (u8, bool),
222        day: u8,
223    ) -> Result<i32, EcmaReferenceYearError> {
224        let (number, is_leap) = month_code;
225        // Computed by `generate_reference_years`
226        let extended_year = match (number, is_leap, day > 29) {
227            (1, false, false) => 1972,
228            (1, false, true) => 1970,
229            (1, true, false) => 1898,
230            (1, true, true) => 1898,
231            (2, false, false) => 1972,
232            (2, false, true) => 1972,
233            (2, true, false) => 1947,
234            (2, true, true) => 1830,
235            (3, false, false) => 1972,
236            (3, false, true) => 1966,
237            (3, true, false) => 1966,
238            (3, true, true) => 1955,
239            (4, false, false) => 1972,
240            (4, false, true) => 1970,
241            (4, true, false) => 1963,
242            (4, true, true) => 1944,
243            (5, false, false) => 1972,
244            (5, false, true) => 1972,
245            (5, true, false) => 1971,
246            (5, true, true) => 1952,
247            (6, false, false) => 1972,
248            (6, false, true) => 1971,
249            (6, true, false) => 1960,
250            (6, true, true) => 1941,
251            (7, false, false) => 1972,
252            (7, false, true) => 1972,
253            (7, true, false) => 1968,
254            (7, true, true) => 1938,
255            (8, false, false) => 1972,
256            (8, false, true) => 1971,
257            (8, true, false) => 1957,
258            (8, true, true) => 1691,
259            (9, false, false) => 1972,
260            (9, false, true) => 1972,
261            (9, true, false) => 2014,
262            (9, true, true) => 1843,
263            (10, false, false) => 1972,
264            (10, false, true) => 1972,
265            (10, true, false) => 1984,
266            (10, true, true) => 1737,
267            // Dec 31, 1972 is 1972-M11-26, dates after that
268            // are in the next year
269            (11, false, false) if day > 26 => 1971,
270            (11, false, false) => 1972,
271            (11, false, true) => 1969,
272            (11, true, false) => 2033,
273            (11, true, true) => 1889,
274            (12, false, false) => 1971,
275            (12, false, true) => 1971,
276            (12, true, false) => 1878,
277            (12, true, true) => 1783,
278            _ => return Err(EcmaReferenceYearError::MonthCodeNotInCalendar),
279        };
280        Ok(extended_year)
281    }
282
283    fn calendar_algorithm(&self) -> Option<CalendarAlgorithm> {
284        Some(CalendarAlgorithm::Chinese)
285    }
286
287    fn debug_name(&self) -> &'static str {
288        "Chinese"
289    }
290}
291
292/// The [Korean](https://en.wikipedia.org/wiki/Korean_calendar) variant of the [`EastAsianTraditional`] calendar.
293///
294/// This type agrees with the official data published by the
295/// [Korea Astronomy and Space Science Institute for the years 1900-2050].
296///
297/// For years since 1912, this uses [adapted GB/T 33661-2017] rules,
298/// using Korea time instead of Beijing Time.
299/// As accurate computation is computationally expensive, years until
300/// 2100 are precomputed, and after that this type regresses to a simplified
301/// calculation. If accuracy beyond 2100 is required, clients
302/// can implement their own [`Rules`] type containing more precomputed data.
303/// We note that the calendar is inherently uncertain for some future dates.
304///
305/// Before 1912 [different rules](https://ytliu.epizy.com/Shixian/Shixian_summary.html)
306/// were used (those of Qing-dynasty China). This type produces correct data
307/// for the years 1900-1912, and falls back to a simplified calculation
308/// before 1900. If accuracy is required before 1900, clients can implement
309/// their own [`Rules`] type using data such as from the excellent compilation
310/// by [Yuk Tung Liu].
311///
312/// The precise behavior of this calendar may change in the future if:
313/// - New ground truth is established by published government sources
314/// - We decide to tweak the simplified calculation
315/// - We decide to expand or reduce the range where we are correctly handling past dates.
316///
317/// [Korea Astronomy and Space Science Institute for the years 1900-2050]: https://astro.kasi.re.kr/life/pageView/5
318/// [adapted GB/T 33661-2017]: Korea::adapted_gb_t_33661_2017
319/// [GB/T 33661-2017]: China::gb_t_33661_2017
320/// [Yuk Tung Liu]: https://ytliu0.github.io/ChineseCalendar/table.html
321///
322/// ```rust
323/// use icu::calendar::cal::{ChineseTraditional, KoreanTraditional};
324/// use icu::calendar::Date;
325///
326/// let iso_a = Date::try_new_iso(2012, 4, 23).unwrap();
327/// let korean_a = iso_a.to_calendar(KoreanTraditional::new());
328/// let chinese_a = iso_a.to_calendar(ChineseTraditional::new());
329///
330/// assert_eq!(korean_a.month().standard_code.0, "M03L");
331/// assert_eq!(chinese_a.month().standard_code.0, "M04");
332///
333/// let iso_b = Date::try_new_iso(2012, 5, 23).unwrap();
334/// let korean_b = iso_b.to_calendar(KoreanTraditional::new());
335/// let chinese_b = iso_b.to_calendar(ChineseTraditional::new());
336///
337/// assert_eq!(korean_b.month().standard_code.0, "M04");
338/// assert_eq!(chinese_b.month().standard_code.0, "M04L");
339/// ```
340pub type KoreanTraditional = EastAsianTraditional<Korea>;
341
342/// The [`Rules`] used in Korea.
343///
344/// See [`KoreanTraditional`] for more information.
345#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
346#[non_exhaustive]
347pub struct Korea;
348
349impl Korea {
350    /// A version of [`China::gb_t_33661_2017`] adapted for Korean time.
351    ///
352    /// See [`China::gb_t_33661_2017`] for caveats.
353    pub fn adapted_gb_t_33661_2017(related_iso: i32) -> EastAsianTraditionalYearData {
354        EastAsianTraditionalYearData::calendrical_calculations::<chinese_based::Dangi>(related_iso)
355    }
356}
357
358impl KoreanTraditional {
359    /// Creates a new [`KoreanTraditional`] calendar.
360    pub const fn new() -> Self {
361        Self(Korea)
362    }
363
364    /// Use [`Self::new`].
365    #[cfg(feature = "serde")]
366    #[doc = icu_provider::gen_buffer_unstable_docs!(BUFFER,Self::new)]
367    #[deprecated(since = "2.1.0", note = "use `Self::new()")]
368    pub fn try_new_with_buffer_provider(
369        _provider: &(impl icu_provider::buf::BufferProvider + ?Sized),
370    ) -> Result<Self, DataError> {
371        Ok(Self::new())
372    }
373
374    /// Use [`Self::new`].
375    #[doc = icu_provider::gen_buffer_unstable_docs!(UNSTABLE, Self::new)]
376    #[deprecated(since = "2.1.0", note = "use `Self::new()")]
377    pub fn try_new_unstable<D: ?Sized>(_provider: &D) -> Result<Self, DataError> {
378        Ok(Self::new())
379    }
380
381    /// Use [`Self::new`].
382    #[deprecated(since = "2.1.0", note = "use `Self::new()")]
383    pub fn new_always_calculating() -> Self {
384        Self::new()
385    }
386}
387
388impl crate::cal::scaffold::UnstableSealed for Korea {}
389impl Rules for Korea {
390    fn year_data(&self, related_iso: i32) -> EastAsianTraditionalYearData {
391        if let Some(year) = EastAsianTraditionalYearData::lookup(
392            related_iso,
393            korea_data::STARTING_YEAR,
394            korea_data::DATA,
395        ) {
396            year
397        } else if related_iso > korea_data::STARTING_YEAR {
398            EastAsianTraditionalYearData::simple(simple::UTC_PLUS_9, related_iso)
399        } else if let Some(year) = EastAsianTraditionalYearData::lookup(
400            related_iso,
401            qing_data::STARTING_YEAR,
402            qing_data::DATA,
403        ) {
404            // Korea used Qing-dynasty rules before 1912
405            // https://github.com/unicode-org/icu4x/issues/6455#issuecomment-3282175550
406            year
407        } else {
408            EastAsianTraditionalYearData::simple(simple::BEIJING_UTC_OFFSET, related_iso)
409        }
410    }
411
412    fn ecma_reference_year(
413        &self,
414        month_code: (u8, bool),
415        day: u8,
416    ) -> Result<i32, EcmaReferenceYearError> {
417        let (number, is_leap) = month_code;
418        // Computed by `generate_reference_years`
419        let extended_year = match (number, is_leap, day > 29) {
420            (1, false, false) => 1972,
421            (1, false, true) => 1970,
422            (1, true, false) => 1898,
423            (1, true, true) => 1898,
424            (2, false, false) => 1972,
425            (2, false, true) => 1972,
426            (2, true, false) => 1947,
427            (2, true, true) => 1830,
428            (3, false, false) => 1972,
429            (3, false, true) => 1968,
430            (3, true, false) => 1966,
431            (3, true, true) => 1955,
432            (4, false, false) => 1972,
433            (4, false, true) => 1970,
434            (4, true, false) => 1963,
435            (4, true, true) => 1944,
436            (5, false, false) => 1972,
437            (5, false, true) => 1972,
438            (5, true, false) => 1971,
439            (5, true, true) => 1952,
440            (6, false, false) => 1972,
441            (6, false, true) => 1971,
442            (6, true, false) => 1960,
443            (6, true, true) => 1941,
444            (7, false, false) => 1972,
445            (7, false, true) => 1972,
446            (7, true, false) => 1968,
447            (7, true, true) => 1938,
448            (8, false, false) => 1972,
449            (8, false, true) => 1971,
450            (8, true, false) => 1957,
451            (8, true, true) => 1691,
452            (9, false, false) => 1972,
453            (9, false, true) => 1972,
454            (9, true, false) => 2014,
455            (9, true, true) => 1843,
456            (10, false, false) => 1972,
457            (10, false, true) => 1972,
458            (10, true, false) => 1984,
459            (10, true, true) => 1737,
460            // Dec 31, 1972 is 1972-M11-26, dates after that
461            // are in the next year
462            (11, false, false) if day > 26 => 1971,
463            (11, false, false) => 1972,
464            (11, false, true) => 1969,
465            (11, true, false) => 2033,
466            (11, true, true) => 1889,
467            (12, false, false) => 1971,
468            (12, false, true) => 1971,
469            (12, true, false) => 1878,
470            (12, true, true) => 1783,
471            _ => return Err(EcmaReferenceYearError::MonthCodeNotInCalendar),
472        };
473        Ok(extended_year)
474    }
475
476    fn calendar_algorithm(&self) -> Option<CalendarAlgorithm> {
477        Some(CalendarAlgorithm::Dangi)
478    }
479    fn debug_name(&self) -> &'static str {
480        "Korean"
481    }
482}
483
484impl<A: AsCalendar<Calendar = KoreanTraditional>> Date<A> {
485    /// This method uses an ordinal month, which is probably not what you want.
486    ///
487    /// Use [`Date::try_new_from_codes`]
488    #[deprecated(since = "2.1.0", note = "use `Date::try_new_from_codes`")]
489    pub fn try_new_dangi_with_calendar(
490        related_iso_year: i32,
491        ordinal_month: u8,
492        day: u8,
493        calendar: A,
494    ) -> Result<Date<A>, DateError> {
495        ArithmeticDate::try_from_ymd(
496            calendar.as_calendar().0.year_data(related_iso_year),
497            ordinal_month,
498            day,
499        )
500        .map(ChineseDateInner)
501        .map(|inner| Date::from_raw(inner, calendar))
502        .map_err(Into::into)
503    }
504}
505
506/// The inner date type used for representing [`Date`]s of [`EastAsianTraditional`].
507#[derive(Debug, Clone)]
508pub struct ChineseDateInner<R: Rules>(ArithmeticDate<EastAsianTraditional<R>>);
509
510impl<R: Rules> Copy for ChineseDateInner<R> {}
511impl<R: Rules> PartialEq for ChineseDateInner<R> {
512    fn eq(&self, other: &Self) -> bool {
513        self.0 == other.0
514    }
515}
516impl<R: Rules> Eq for ChineseDateInner<R> {}
517impl<R: Rules> PartialOrd for ChineseDateInner<R> {
518    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
519        Some(self.cmp(other))
520    }
521}
522impl<R: Rules> Ord for ChineseDateInner<R> {
523    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
524        self.0.cmp(&other.0)
525    }
526}
527
528impl ChineseTraditional {
529    /// Creates a new [`ChineseTraditional`] calendar.
530    pub const fn new() -> Self {
531        EastAsianTraditional(China)
532    }
533
534    #[cfg(feature = "serde")]
535    #[doc = icu_provider::gen_buffer_unstable_docs!(BUFFER,Self::new)]
536    #[deprecated(since = "2.1.0", note = "use `Self::new()")]
537    pub fn try_new_with_buffer_provider(
538        _provider: &(impl icu_provider::buf::BufferProvider + ?Sized),
539    ) -> Result<Self, DataError> {
540        Ok(Self::new())
541    }
542
543    #[doc = icu_provider::gen_buffer_unstable_docs!(UNSTABLE, Self::new)]
544    #[deprecated(since = "2.1.0", note = "use `Self::new()")]
545    pub fn try_new_unstable<D: ?Sized>(_provider: &D) -> Result<Self, DataError> {
546        Ok(Self::new())
547    }
548
549    /// Use [`Self::new()`].
550    #[deprecated(since = "2.1.0", note = "use `Self::new()")]
551    pub fn new_always_calculating() -> Self {
552        Self::new()
553    }
554}
555
556impl<R: Rules> DateFieldsResolver for EastAsianTraditional<R> {
557    type YearInfo = EastAsianTraditionalYearData;
558
559    fn days_in_provided_month(year: EastAsianTraditionalYearData, month: u8) -> u8 {
560        29 + year.packed.month_has_30_days(month) as u8
561    }
562
563    /// Returns the number of months in a given year, which is 13 in a leap year, and 12 in a common year.
564    fn months_in_provided_year(year: EastAsianTraditionalYearData) -> u8 {
565        12 + year.packed.leap_month().is_some() as u8
566    }
567
568    #[inline]
569    fn year_info_from_era(
570        &self,
571        _era: &[u8],
572        _era_year: i32,
573    ) -> Result<Self::YearInfo, UnknownEraError> {
574        // This calendar has no era codes
575        Err(UnknownEraError)
576    }
577
578    #[inline]
579    fn year_info_from_extended(&self, extended_year: i32) -> Self::YearInfo {
580        self.0.year_data(extended_year)
581    }
582
583    #[inline]
584    fn reference_year_from_month_day(
585        &self,
586        month_code: types::ValidMonthCode,
587        day: u8,
588    ) -> Result<Self::YearInfo, EcmaReferenceYearError> {
589        self.0
590            .ecma_reference_year(month_code.to_tuple(), day)
591            .map(|y| self.0.year_data(y))
592    }
593
594    fn ordinal_month_from_code(
595        &self,
596        year: &Self::YearInfo,
597        month_code: types::ValidMonthCode,
598        options: DateFromFieldsOptions,
599    ) -> Result<u8, MonthCodeError> {
600        // 14 is a sentinel value, greater than all other months, for the purpose of computation only;
601        // it is impossible to actually have 14 months in a year.
602        let leap_month = year.packed.leap_month().unwrap_or(14);
603
604        // leap_month identifies the ordinal month number of the leap month,
605        // so its month number will be leap_month - 1
606        if month_code == ValidMonthCode::new_unchecked(leap_month - 1, true) {
607            return Ok(leap_month);
608        }
609
610        let (number @ 1..13, leap) = month_code.to_tuple() else {
611            return Err(MonthCodeError::NotInCalendar);
612        };
613
614        if leap && options.overflow != Some(Overflow::Constrain) {
615            // wrong leap month and not constraining
616            return Err(MonthCodeError::NotInYear);
617        }
618
619        // add one if there was a leap month before
620        Ok(number + (number >= leap_month) as u8)
621    }
622
623    fn month_code_from_ordinal(&self, year: &Self::YearInfo, ordinal_month: u8) -> ValidMonthCode {
624        // 14 is a sentinel value, greater than all other months, for the purpose of computation only;
625        // it is impossible to actually have 14 months in a year.
626        let leap_month = year.packed.leap_month().unwrap_or(14);
627        ValidMonthCode::new_unchecked(
628            // subtract one if there was a leap month before
629            ordinal_month - (ordinal_month >= leap_month) as u8,
630            ordinal_month == leap_month,
631        )
632    }
633}
634
635impl<R: Rules> crate::cal::scaffold::UnstableSealed for EastAsianTraditional<R> {}
636impl<R: Rules> Calendar for EastAsianTraditional<R> {
637    type DateInner = ChineseDateInner<R>;
638    type Year = types::CyclicYear;
639    type DifferenceError = core::convert::Infallible;
640
641    fn from_codes(
642        &self,
643        era: Option<&str>,
644        year: i32,
645        month_code: types::MonthCode,
646        day: u8,
647    ) -> Result<Self::DateInner, DateError> {
648        ArithmeticDate::from_codes(era, year, month_code, day, self).map(ChineseDateInner)
649    }
650
651    #[cfg(feature = "unstable")]
652    fn from_fields(
653        &self,
654        fields: types::DateFields,
655        options: DateFromFieldsOptions,
656    ) -> Result<Self::DateInner, DateFromFieldsError> {
657        ArithmeticDate::from_fields(fields, options, self).map(ChineseDateInner)
658    }
659
660    fn from_rata_die(&self, rd: RataDie) -> Self::DateInner {
661        let iso = Iso.from_rata_die(rd);
662        let year = {
663            let candidate = self.0.year_data(iso.0.year);
664
665            if rd >= candidate.new_year() {
666                candidate
667            } else {
668                self.0.year_data(iso.0.year - 1)
669            }
670        };
671
672        // Clamp the RD to our year
673        let rd = rd.clamp(
674            year.new_year(),
675            year.new_year() + year.packed.days_in_year() as i64,
676        );
677
678        let day_of_year = (rd - year.new_year()) as u16;
679
680        // We divide by 30, not 29, to account for the case where all months before this
681        // were length 30 (possible near the beginning of the year)
682        let mut month = (day_of_year / 30) as u8 + 1;
683        let mut last_day_of_month = year.packed.last_day_of_month(month);
684        let mut last_day_of_prev_month = year.packed.last_day_of_month(month - 1);
685
686        while day_of_year >= last_day_of_month {
687            month += 1;
688            last_day_of_prev_month = last_day_of_month;
689            last_day_of_month = year.packed.last_day_of_month(month);
690        }
691
692        let day = (day_of_year + 1 - last_day_of_prev_month) as u8;
693
694        ChineseDateInner(ArithmeticDate::new_unchecked(year, month, day))
695    }
696
697    fn to_rata_die(&self, date: &Self::DateInner) -> RataDie {
698        date.0.year.new_year()
699            + date.0.year.packed.last_day_of_month(date.0.month - 1) as i64
700            + (date.0.day - 1) as i64
701    }
702
703    fn has_cheap_iso_conversion(&self) -> bool {
704        false
705    }
706
707    // Count the number of months in a given year, specified by providing a date
708    // from that year
709    fn days_in_year(&self, date: &Self::DateInner) -> u16 {
710        date.0.year.packed.days_in_year()
711    }
712
713    fn days_in_month(&self, date: &Self::DateInner) -> u8 {
714        Self::days_in_provided_month(date.0.year, date.0.month)
715    }
716
717    #[cfg(feature = "unstable")]
718    fn add(
719        &self,
720        date: &Self::DateInner,
721        duration: types::DateDuration,
722        options: DateAddOptions,
723    ) -> Result<Self::DateInner, DateError> {
724        date.0.added(duration, self, options).map(ChineseDateInner)
725    }
726
727    #[cfg(feature = "unstable")]
728    fn until(
729        &self,
730        date1: &Self::DateInner,
731        date2: &Self::DateInner,
732        options: DateDifferenceOptions,
733    ) -> Result<types::DateDuration, Self::DifferenceError> {
734        Ok(date1.0.until(&date2.0, self, options))
735    }
736
737    /// Obtain a name for the calendar for debug printing
738    fn debug_name(&self) -> &'static str {
739        self.0.debug_name()
740    }
741
742    fn year_info(&self, date: &Self::DateInner) -> Self::Year {
743        let year = date.0.year;
744        types::CyclicYear {
745            year: (year.related_iso - 4).rem_euclid(60) as u8 + 1,
746            related_iso: year.related_iso,
747        }
748    }
749
750    fn is_in_leap_year(&self, date: &Self::DateInner) -> bool {
751        date.0.year.packed.leap_month().is_some()
752    }
753
754    /// The calendar-specific month code represented by `date`;
755    /// since the Chinese calendar has leap months, an "L" is appended to the month code for
756    /// leap months. For example, in a year where an intercalary month is added after the second
757    /// month, the month codes for ordinal months 1, 2, 3, 4, 5 would be "M01", "M02", "M02L", "M03", "M04".
758    fn month(&self, date: &Self::DateInner) -> types::MonthInfo {
759        types::MonthInfo::for_code_and_ordinal(
760            self.month_code_from_ordinal(&date.0.year, date.0.month),
761            date.0.month,
762        )
763    }
764
765    /// The calendar-specific day-of-month represented by `date`
766    fn day_of_month(&self, date: &Self::DateInner) -> types::DayOfMonth {
767        types::DayOfMonth(date.0.day)
768    }
769
770    /// Information of the day of the year
771    fn day_of_year(&self, date: &Self::DateInner) -> types::DayOfYear {
772        types::DayOfYear(date.0.year.packed.last_day_of_month(date.0.month - 1) + date.0.day as u16)
773    }
774
775    fn calendar_algorithm(&self) -> Option<CalendarAlgorithm> {
776        self.0.calendar_algorithm()
777    }
778
779    fn months_in_year(&self, date: &Self::DateInner) -> u8 {
780        Self::months_in_provided_year(date.0.year)
781    }
782}
783
784impl<A: AsCalendar<Calendar = ChineseTraditional>> Date<A> {
785    /// This method uses an ordinal month, which is probably not what you want.
786    ///
787    /// Use [`Date::try_new_from_codes`]
788    #[deprecated(since = "2.1.0", note = "use `Date::try_new_from_codes`")]
789    pub fn try_new_chinese_with_calendar(
790        related_iso_year: i32,
791        ordinal_month: u8,
792        day: u8,
793        calendar: A,
794    ) -> Result<Date<A>, DateError> {
795        ArithmeticDate::try_from_ymd(
796            calendar.as_calendar().0.year_data(related_iso_year),
797            ordinal_month,
798            day,
799        )
800        .map(ChineseDateInner)
801        .map(|inner| Date::from_raw(inner, calendar))
802        .map_err(Into::into)
803    }
804}
805
806/// Information about a [`EastAsianTraditional`] year.
807#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
808// TODO(#3933): potentially make this smaller
809pub struct EastAsianTraditionalYearData {
810    /// Contains:
811    /// - length of each month in the year
812    /// - whether or not there is a leap month, and which month it is
813    /// - the date of Chinese New Year in the related ISO year
814    packed: PackedEastAsianTraditionalYearData,
815    related_iso: i32,
816}
817
818impl ToExtendedYear for EastAsianTraditionalYearData {
819    fn to_extended_year(&self) -> i32 {
820        self.related_iso
821    }
822}
823
824impl EastAsianTraditionalYearData {
825    /// Creates [`EastAsianTraditionalYearData`] from the given parts.
826    ///
827    /// `start_day` is the date for the first day of the year, see [`Date::to_rata_die`]
828    /// to obtain a [`RataDie`] from a [`Date`] in an arbitrary calendar.
829    ///
830    /// `leap_month` is the ordinal number of the leap month, for example if a year
831    /// has months 1, 2, 3, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, the `leap_month`
832    /// would be `Some(4)`.
833    ///
834    /// `month_lengths[n - 1]` is true if the nth month has 30 days, and false otherwise.
835    /// The leap month does not necessarily have the same number of days as the previous
836    /// month, which is why this has length 13. In non-leap years, the last value is ignored.
837    pub fn new(
838        related_iso: i32,
839        start_day: RataDie,
840        month_lengths: [bool; 13],
841        leap_month: Option<u8>,
842    ) -> Self {
843        Self {
844            packed: PackedEastAsianTraditionalYearData::new(
845                related_iso,
846                month_lengths,
847                leap_month,
848                start_day,
849            ),
850            related_iso,
851        }
852    }
853
854    fn lookup(
855        related_iso: i32,
856        starting_year: i32,
857        data: &[PackedEastAsianTraditionalYearData],
858    ) -> Option<Self> {
859        Some(related_iso)
860            .and_then(|e| usize::try_from(e.checked_sub(starting_year)?).ok())
861            .and_then(|i| data.get(i))
862            .map(|&packed| Self {
863                related_iso,
864                packed,
865            })
866    }
867
868    fn calendrical_calculations<CB: ChineseBased>(
869        related_iso: i32,
870    ) -> EastAsianTraditionalYearData {
871        let mid_year = calendrical_calculations::gregorian::fixed_from_gregorian(related_iso, 7, 1);
872        let year_bounds = YearBounds::compute::<CB>(mid_year);
873
874        let YearBounds {
875            new_year,
876            next_new_year,
877            ..
878        } = year_bounds;
879        let (month_lengths, leap_month) =
880            chinese_based::month_structure_for_year::<CB>(new_year, next_new_year);
881
882        EastAsianTraditionalYearData {
883            packed: PackedEastAsianTraditionalYearData::new(
884                related_iso,
885                month_lengths,
886                leap_month,
887                new_year,
888            ),
889            related_iso,
890        }
891    }
892
893    /// Get the new year R.D.    
894    fn new_year(self) -> RataDie {
895        self.packed.new_year(self.related_iso)
896    }
897}
898
899/// The struct containing compiled ChineseData
900///
901/// Bit structure (little endian: note that shifts go in the opposite direction!)
902///
903/// ```text
904/// Bit:             0   1   2   3   4   5   6   7
905/// Byte 0:          [  month lengths .............
906/// Byte 1:         .. month lengths ] | [ leap month index ..
907/// Byte 2:          ] | [   NY offset       ] | unused
908/// ```
909///
910/// Where the New Year Offset is the offset from ISO Jan 19 of that year for Chinese New Year,
911/// the month lengths are stored as 1 = 30, 0 = 29 for each month including the leap month.
912/// The largest possible offset is 33, which requires 6 bits of storage.
913///
914/// <div class="stab unstable">
915/// 🚧 This code is considered unstable; it may change at any time, in breaking or non-breaking ways,
916/// including in SemVer minor releases. While the serde representation of data structs is guaranteed
917/// to be stable, their Rust representation might not be. Use with caution.
918/// </div>
919#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
920struct PackedEastAsianTraditionalYearData(u8, u8, u8);
921
922impl PackedEastAsianTraditionalYearData {
923    /// The first day on which Chinese New Year may occur
924    ///
925    /// According to Reingold & Dershowitz, ch 19.6, Chinese New Year occurs on Jan 21 - Feb 21 inclusive.
926    ///
927    /// Our simple approximation sometimes returns Feb 22.
928    ///
929    /// We allow it to occur as early as January 19 which is the earliest the second new moon
930    /// could occur after the Winter Solstice if the solstice is pinned to December 20.
931    const fn earliest_ny(related_iso: i32) -> RataDie {
932        calendrical_calculations::gregorian::fixed_from_gregorian(related_iso, 1, 19)
933    }
934
935    /// It clamps some values to avoid debug assertions on calendrical invariants.
936    const fn new(
937        related_iso: i32,
938        month_lengths: [bool; 13],
939        leap_month: Option<u8>,
940        new_year: RataDie,
941    ) -> Self {
942        // These assertions are API correctness assertions and even bad calendar arithmetic
943        // should not produce this
944        if let Some(l) = leap_month {
945            debug_assert!(2 <= l && l <= 13, "Leap month indices must be 2 <= i <= 13");
946        } else {
947            debug_assert!(
948                !month_lengths[12],
949                "Last month length should not be set for non-leap years"
950            )
951        }
952
953        let ny_offset = new_year.since(Self::earliest_ny(related_iso));
954
955        #[cfg(debug_assertions)]
956        let out_of_valid_astronomical_range = WELL_BEHAVED_ASTRONOMICAL_RANGE.start.to_i64_date()
957            > new_year.to_i64_date()
958            || new_year.to_i64_date() > WELL_BEHAVED_ASTRONOMICAL_RANGE.end.to_i64_date();
959
960        // Assert the offset is in range, but allow it to be out of
961        // range when out_of_valid_astronomical_range=true
962        #[cfg(debug_assertions)]
963        debug_assert!(
964            ny_offset >= 0 || out_of_valid_astronomical_range,
965            "Year offset too small to store"
966        );
967        // The maximum new-year's offset we have found is 34
968        #[cfg(debug_assertions)]
969        debug_assert!(
970            ny_offset < 35 || out_of_valid_astronomical_range,
971            "Year offset too big to store"
972        );
973
974        // Just clamp to something we can represent when things get of range.
975        //
976        // This will typically happen when out_of_valid_astronomical_range
977        // is true.
978        //
979        // We can store up to 6 bytes for ny_offset, even if our
980        // maximum asserted value is otherwise 33.
981        let ny_offset = ny_offset & (0x40 - 1);
982
983        let mut all = 0u32; // last byte unused
984
985        let mut month = 0;
986        while month < month_lengths.len() {
987            #[allow(clippy::indexing_slicing)] // const iteration
988            if month_lengths[month] {
989                all |= 1 << month as u32;
990            }
991            month += 1;
992        }
993        let leap_month_idx = if let Some(leap_month_idx) = leap_month {
994            leap_month_idx
995        } else {
996            0
997        };
998        all |= (leap_month_idx as u32) << (8 + 5);
999        all |= (ny_offset as u32) << (16 + 1);
1000        let le = all.to_le_bytes();
1001        Self(le[0], le[1], le[2])
1002    }
1003
1004    fn new_year(self, related_iso: i32) -> RataDie {
1005        Self::earliest_ny(related_iso) + (self.2 as i64 >> 1)
1006    }
1007
1008    fn leap_month(self) -> Option<u8> {
1009        let bits = (self.1 >> 5) + ((self.2 & 0b1) << 3);
1010
1011        (bits != 0).then_some(bits)
1012    }
1013
1014    // Whether a particular month has 30 days (month is 1-indexed)
1015    fn month_has_30_days(self, month: u8) -> bool {
1016        let months = u16::from_le_bytes([self.0, self.1]);
1017        months & (1 << (month - 1) as u16) != 0
1018    }
1019
1020    // month is 1-indexed, but 0 is a valid input, producing 0
1021    fn last_day_of_month(self, month: u8) -> u16 {
1022        let months = u16::from_le_bytes([self.0, self.1]);
1023        // month is 1-indexed, so `29 * month` includes the current month
1024        let mut prev_month_lengths = 29 * month as u16;
1025        // month is 1-indexed, so `1 << month` is a mask with all zeroes except
1026        // for a 1 at the bit index at the next month. Subtracting 1 from it gets us
1027        // a bitmask for all months up to now
1028        let long_month_bits = months & ((1 << month as u16) - 1);
1029        prev_month_lengths += long_month_bits.count_ones().try_into().unwrap_or(0);
1030        prev_month_lengths
1031    }
1032
1033    fn days_in_year(self) -> u16 {
1034        self.last_day_of_month(12 + self.leap_month().is_some() as u8)
1035    }
1036}
1037
1038#[cfg(test)]
1039mod test {
1040    use super::*;
1041    use crate::options::{DateFromFieldsOptions, Overflow};
1042    use crate::types::DateFields;
1043    use calendrical_calculations::{gregorian::fixed_from_gregorian, rata_die::RataDie};
1044    use std::collections::BTreeMap;
1045
1046    #[test]
1047    fn test_chinese_from_rd() {
1048        #[derive(Debug)]
1049        struct TestCase {
1050            rd: i64,
1051            expected_year: i32,
1052            expected_month: u8,
1053            expected_day: u8,
1054        }
1055
1056        let cases = [
1057            TestCase {
1058                rd: -964192,
1059                expected_year: -2639,
1060                expected_month: 1,
1061                expected_day: 1,
1062            },
1063            TestCase {
1064                rd: -963838,
1065                expected_year: -2638,
1066                expected_month: 1,
1067                expected_day: 1,
1068            },
1069            TestCase {
1070                rd: -963129,
1071                expected_year: -2637,
1072                expected_month: 13,
1073                expected_day: 1,
1074            },
1075            TestCase {
1076                rd: -963100,
1077                expected_year: -2637,
1078                expected_month: 13,
1079                expected_day: 30,
1080            },
1081            TestCase {
1082                rd: -963099,
1083                expected_year: -2636,
1084                expected_month: 1,
1085                expected_day: 1,
1086            },
1087            TestCase {
1088                rd: 738700,
1089                expected_year: 2023,
1090                expected_month: 6,
1091                expected_day: 12,
1092            },
1093            TestCase {
1094                rd: fixed_from_gregorian(2319, 2, 20).to_i64_date(),
1095                expected_year: 2318,
1096                expected_month: 13,
1097                expected_day: 30,
1098            },
1099            TestCase {
1100                rd: fixed_from_gregorian(2319, 2, 21).to_i64_date(),
1101                expected_year: 2319,
1102                expected_month: 1,
1103                expected_day: 1,
1104            },
1105            TestCase {
1106                rd: 738718,
1107                expected_year: 2023,
1108                expected_month: 6,
1109                expected_day: 30,
1110            },
1111            TestCase {
1112                rd: 738747,
1113                expected_year: 2023,
1114                expected_month: 7,
1115                expected_day: 29,
1116            },
1117            TestCase {
1118                rd: 738748,
1119                expected_year: 2023,
1120                expected_month: 8,
1121                expected_day: 1,
1122            },
1123            TestCase {
1124                rd: 738865,
1125                expected_year: 2023,
1126                expected_month: 11,
1127                expected_day: 29,
1128            },
1129            TestCase {
1130                rd: 738895,
1131                expected_year: 2023,
1132                expected_month: 12,
1133                expected_day: 29,
1134            },
1135            TestCase {
1136                rd: 738925,
1137                expected_year: 2023,
1138                expected_month: 13,
1139                expected_day: 30,
1140            },
1141            TestCase {
1142                rd: 0,
1143                expected_year: 0,
1144                expected_month: 11,
1145                expected_day: 19,
1146            },
1147            TestCase {
1148                rd: -1,
1149                expected_year: 0,
1150                expected_month: 11,
1151                expected_day: 18,
1152            },
1153            TestCase {
1154                rd: -365,
1155                expected_year: -1,
1156                expected_month: 12,
1157                expected_day: 9,
1158            },
1159            TestCase {
1160                rd: 100,
1161                expected_year: 1,
1162                expected_month: 3,
1163                expected_day: 1,
1164            },
1165        ];
1166
1167        for case in cases {
1168            let rata_die = RataDie::new(case.rd);
1169
1170            let chinese = Date::from_rata_die(rata_die, ChineseTraditional::new());
1171            assert_eq!(
1172                case.expected_year,
1173                chinese.extended_year(),
1174                "Chinese from RD failed, case: {case:?}"
1175            );
1176            assert_eq!(
1177                case.expected_month,
1178                chinese.month().ordinal,
1179                "Chinese from RD failed, case: {case:?}"
1180            );
1181            assert_eq!(
1182                case.expected_day,
1183                chinese.day_of_month().0,
1184                "Chinese from RD failed, case: {case:?}"
1185            );
1186        }
1187    }
1188
1189    #[test]
1190    fn test_rd_from_chinese() {
1191        #[derive(Debug)]
1192        struct TestCase {
1193            year: i32,
1194            ordinal_month: u8,
1195            month_code: types::MonthCode,
1196            day: u8,
1197            expected: i64,
1198        }
1199
1200        let cases = [
1201            TestCase {
1202                year: 2023,
1203                ordinal_month: 6,
1204                month_code: types::MonthCode::new_normal(5).unwrap(),
1205                day: 6,
1206                // June 23 2023
1207                expected: 738694,
1208            },
1209            TestCase {
1210                year: -2636,
1211                ordinal_month: 1,
1212                month_code: types::MonthCode::new_normal(1).unwrap(),
1213                day: 1,
1214                expected: -963099,
1215            },
1216        ];
1217
1218        for case in cases {
1219            let date = Date::try_new_from_codes(
1220                None,
1221                case.year,
1222                case.month_code,
1223                case.day,
1224                ChineseTraditional::new(),
1225            )
1226            .unwrap();
1227            #[allow(deprecated)] // should still test
1228            {
1229                assert_eq!(
1230                    Date::try_new_chinese_with_calendar(
1231                        case.year,
1232                        case.ordinal_month,
1233                        case.day,
1234                        ChineseTraditional::new()
1235                    ),
1236                    Ok(date)
1237                );
1238            }
1239            let rd = date.to_rata_die().to_i64_date();
1240            let expected = case.expected;
1241            assert_eq!(rd, expected, "RD from Chinese failed, with expected: {expected} and calculated: {rd}, for test case: {case:?}");
1242        }
1243    }
1244
1245    #[test]
1246    fn test_rd_chinese_roundtrip() {
1247        let mut rd = -1963020;
1248        let max_rd = 1963020;
1249        let mut iters = 0;
1250        let max_iters = 560;
1251        while rd < max_rd && iters < max_iters {
1252            let rata_die = RataDie::new(rd);
1253
1254            let chinese = Date::from_rata_die(rata_die, ChineseTraditional::new());
1255            let result = chinese.to_rata_die();
1256            assert_eq!(result, rata_die, "Failed roundtrip RD -> Chinese -> RD for RD: {rata_die:?}, with calculated: {result:?} from Chinese date:\n{chinese:?}");
1257
1258            rd += 7043;
1259            iters += 1;
1260        }
1261    }
1262
1263    #[test]
1264    fn test_chinese_epoch() {
1265        let iso = Date::try_new_iso(-2636, 2, 15).unwrap();
1266
1267        let chinese = iso.to_calendar(ChineseTraditional::new());
1268
1269        assert_eq!(chinese.cyclic_year().related_iso, -2636);
1270        assert_eq!(chinese.month().ordinal, 1);
1271        assert_eq!(chinese.month().standard_code.0, "M01");
1272        assert_eq!(chinese.day_of_month().0, 1);
1273        assert_eq!(chinese.cyclic_year().year, 1);
1274        assert_eq!(chinese.cyclic_year().related_iso, -2636);
1275    }
1276
1277    #[test]
1278    fn test_iso_to_chinese_negative_years() {
1279        #[derive(Debug)]
1280        struct TestCase {
1281            iso_year: i32,
1282            iso_month: u8,
1283            iso_day: u8,
1284            expected_year: i32,
1285            expected_month: u8,
1286            expected_day: u8,
1287        }
1288
1289        let cases = [
1290            TestCase {
1291                iso_year: -2636,
1292                iso_month: 2,
1293                iso_day: 14,
1294                expected_year: -2637,
1295                expected_month: 13,
1296                expected_day: 30,
1297            },
1298            TestCase {
1299                iso_year: -2636,
1300                iso_month: 1,
1301                iso_day: 15,
1302                expected_year: -2637,
1303                expected_month: 12,
1304                expected_day: 29,
1305            },
1306        ];
1307
1308        for case in cases {
1309            let iso = Date::try_new_iso(case.iso_year, case.iso_month, case.iso_day).unwrap();
1310
1311            let chinese = iso.to_calendar(ChineseTraditional::new());
1312            assert_eq!(
1313                case.expected_year,
1314                chinese.cyclic_year().related_iso,
1315                "ISO to Chinese failed for case: {case:?}"
1316            );
1317            assert_eq!(
1318                case.expected_month,
1319                chinese.month().ordinal,
1320                "ISO to Chinese failed for case: {case:?}"
1321            );
1322            assert_eq!(
1323                case.expected_day,
1324                chinese.day_of_month().0,
1325                "ISO to Chinese failed for case: {case:?}"
1326            );
1327        }
1328    }
1329
1330    #[test]
1331    fn test_chinese_leap_months() {
1332        let expected = [
1333            (1933, 6),
1334            (1938, 8),
1335            (1984, 11),
1336            (2009, 6),
1337            (2017, 7),
1338            (2028, 6),
1339        ];
1340
1341        for case in expected {
1342            let year = case.0;
1343            let expected_month = case.1;
1344            let iso = Date::try_new_iso(year, 6, 1).unwrap();
1345
1346            let chinese_date = iso.to_calendar(ChineseTraditional::new());
1347            assert!(
1348                chinese_date.is_in_leap_year(),
1349                "{year} should be a leap year"
1350            );
1351            let new_year = chinese_date.inner.0.year.new_year();
1352            assert_eq!(
1353                expected_month,
1354                chinese_based::get_leap_month_from_new_year::<chinese_based::Chinese>(new_year),
1355                "{year} have leap month {expected_month}"
1356            );
1357        }
1358    }
1359
1360    #[test]
1361    fn test_month_days() {
1362        let year = ChineseTraditional::new().0.year_data(2023);
1363        let cases = [
1364            (1, 29),
1365            (2, 30),
1366            (3, 29),
1367            (4, 29),
1368            (5, 30),
1369            (6, 30),
1370            (7, 29),
1371            (8, 30),
1372            (9, 30),
1373            (10, 29),
1374            (11, 30),
1375            (12, 29),
1376            (13, 30),
1377        ];
1378        for case in cases {
1379            let days_in_month = EastAsianTraditional::<China>::days_in_provided_month(year, case.0);
1380            assert_eq!(
1381                case.1, days_in_month,
1382                "month_days test failed for case: {case:?}"
1383            );
1384        }
1385    }
1386
1387    #[test]
1388    fn test_ordinal_to_month_code() {
1389        #[derive(Debug)]
1390        struct TestCase {
1391            year: i32,
1392            month: u8,
1393            day: u8,
1394            expected_code: &'static str,
1395        }
1396
1397        let cases = [
1398            TestCase {
1399                year: 2023,
1400                month: 1,
1401                day: 9,
1402                expected_code: "M12",
1403            },
1404            TestCase {
1405                year: 2023,
1406                month: 2,
1407                day: 9,
1408                expected_code: "M01",
1409            },
1410            TestCase {
1411                year: 2023,
1412                month: 3,
1413                day: 9,
1414                expected_code: "M02",
1415            },
1416            TestCase {
1417                year: 2023,
1418                month: 4,
1419                day: 9,
1420                expected_code: "M02L",
1421            },
1422            TestCase {
1423                year: 2023,
1424                month: 5,
1425                day: 9,
1426                expected_code: "M03",
1427            },
1428            TestCase {
1429                year: 2023,
1430                month: 6,
1431                day: 9,
1432                expected_code: "M04",
1433            },
1434            TestCase {
1435                year: 2023,
1436                month: 7,
1437                day: 9,
1438                expected_code: "M05",
1439            },
1440            TestCase {
1441                year: 2023,
1442                month: 8,
1443                day: 9,
1444                expected_code: "M06",
1445            },
1446            TestCase {
1447                year: 2023,
1448                month: 9,
1449                day: 9,
1450                expected_code: "M07",
1451            },
1452            TestCase {
1453                year: 2023,
1454                month: 10,
1455                day: 9,
1456                expected_code: "M08",
1457            },
1458            TestCase {
1459                year: 2023,
1460                month: 11,
1461                day: 9,
1462                expected_code: "M09",
1463            },
1464            TestCase {
1465                year: 2023,
1466                month: 12,
1467                day: 9,
1468                expected_code: "M10",
1469            },
1470            TestCase {
1471                year: 2024,
1472                month: 1,
1473                day: 9,
1474                expected_code: "M11",
1475            },
1476            TestCase {
1477                year: 2024,
1478                month: 2,
1479                day: 9,
1480                expected_code: "M12",
1481            },
1482            TestCase {
1483                year: 2024,
1484                month: 2,
1485                day: 10,
1486                expected_code: "M01",
1487            },
1488        ];
1489
1490        for case in cases {
1491            let iso = Date::try_new_iso(case.year, case.month, case.day).unwrap();
1492            let chinese = iso.to_calendar(ChineseTraditional::new());
1493            let result_code = chinese.month().standard_code.0;
1494            let expected_code = case.expected_code.to_string();
1495            assert_eq!(
1496                expected_code, result_code,
1497                "Month codes did not match for test case: {case:?}"
1498            );
1499        }
1500    }
1501
1502    #[test]
1503    fn test_month_code_to_ordinal() {
1504        let cal = ChineseTraditional::new();
1505        let reject = DateFromFieldsOptions {
1506            overflow: Some(Overflow::Reject),
1507            ..Default::default()
1508        };
1509        let year = cal.year_info_from_extended(2023);
1510        let leap_month = year.packed.leap_month().unwrap();
1511        for ordinal in 1..=13 {
1512            let code = ValidMonthCode::new_unchecked(
1513                ordinal - (ordinal >= leap_month) as u8,
1514                ordinal == leap_month,
1515            );
1516            assert_eq!(
1517                cal.ordinal_month_from_code(&year, code, reject),
1518                Ok(ordinal),
1519                "Code to ordinal failed for year: {}, code: {ordinal}",
1520                year.related_iso
1521            );
1522        }
1523    }
1524
1525    #[test]
1526    fn check_invalid_month_code_to_ordinal() {
1527        let cal = ChineseTraditional::new();
1528        let reject = DateFromFieldsOptions {
1529            overflow: Some(Overflow::Reject),
1530            ..Default::default()
1531        };
1532        for year in [4659, 4660] {
1533            let year = cal.year_info_from_extended(year);
1534            for (code, error) in [
1535                (
1536                    ValidMonthCode::new_unchecked(4, true),
1537                    MonthCodeError::NotInYear,
1538                ),
1539                (
1540                    ValidMonthCode::new_unchecked(13, false),
1541                    MonthCodeError::NotInCalendar,
1542                ),
1543            ] {
1544                assert_eq!(
1545                    cal.ordinal_month_from_code(&year, code, reject),
1546                    Err(error),
1547                    "Invalid month code failed for year: {}, code: {code:?}",
1548                    year.related_iso,
1549                );
1550            }
1551        }
1552    }
1553
1554    #[test]
1555    fn test_iso_chinese_roundtrip() {
1556        for i in -1000..=1000 {
1557            let year = i;
1558            let month = i as u8 % 12 + 1;
1559            let day = i as u8 % 28 + 1;
1560            let iso = Date::try_new_iso(year, month, day).unwrap();
1561            let chinese = iso.to_calendar(ChineseTraditional::new());
1562            let result = chinese.to_calendar(Iso);
1563            assert_eq!(iso, result, "ISO to Chinese roundtrip failed!\nIso: {iso:?}\nChinese: {chinese:?}\nResult: {result:?}");
1564        }
1565    }
1566
1567    fn check_cyclic_and_rel_iso(year: i32) {
1568        let iso = Date::try_new_iso(year, 6, 6).unwrap();
1569        let chinese = iso.to_calendar(ChineseTraditional::new());
1570        let korean = iso.to_calendar(KoreanTraditional::new());
1571        let chinese_year = chinese.cyclic_year();
1572        let korean_year = korean.cyclic_year();
1573        assert_eq!(
1574            chinese_year, korean_year,
1575            "Cyclic year failed for year: {year}"
1576        );
1577        let chinese_rel_iso = chinese_year.related_iso;
1578        let korean_rel_iso = korean_year.related_iso;
1579        assert_eq!(
1580            chinese_rel_iso, korean_rel_iso,
1581            "Rel. ISO year equality failed for year: {year}"
1582        );
1583        assert_eq!(korean_rel_iso, year, "Korean Rel. ISO failed!");
1584    }
1585
1586    #[test]
1587    fn test_cyclic_same_as_chinese_near_present_day() {
1588        for year in 1923..=2123 {
1589            check_cyclic_and_rel_iso(year);
1590        }
1591    }
1592
1593    #[test]
1594    fn test_cyclic_same_as_chinese_near_rd_zero() {
1595        for year in -100..=100 {
1596            check_cyclic_and_rel_iso(year);
1597        }
1598    }
1599
1600    #[test]
1601    fn test_iso_to_korean_roundtrip() {
1602        let mut rd = -1963020;
1603        let max_rd = 1963020;
1604        let mut iters = 0;
1605        let max_iters = 560;
1606        while rd < max_rd && iters < max_iters {
1607            let rata_die = RataDie::new(rd);
1608            let iso = Date::from_rata_die(rata_die, Iso);
1609            let korean = iso.to_calendar(KoreanTraditional::new());
1610            let result = korean.to_calendar(Iso);
1611            assert_eq!(
1612                iso, result,
1613                "Failed roundtrip ISO -> Korean -> ISO for RD: {rd}"
1614            );
1615
1616            rd += 7043;
1617            iters += 1;
1618        }
1619    }
1620
1621    #[test]
1622    fn test_from_fields_constrain() {
1623        let fields = DateFields {
1624            day: Some(31),
1625            month_code: Some(b"M01"),
1626            extended_year: Some(1972),
1627            ..Default::default()
1628        };
1629        let options = DateFromFieldsOptions {
1630            overflow: Some(Overflow::Constrain),
1631            ..Default::default()
1632        };
1633
1634        let cal = ChineseTraditional::new();
1635        let date = Date::try_from_fields(fields, options, cal).unwrap();
1636        assert_eq!(
1637            date.day_of_month().0,
1638            29,
1639            "Day was successfully constrained"
1640        );
1641
1642        // 2022 did not have M01L, the month should be constrained back down
1643        let fields = DateFields {
1644            day: Some(1),
1645            month_code: Some(b"M01L"),
1646            extended_year: Some(2022),
1647            ..Default::default()
1648        };
1649        let date = Date::try_from_fields(fields, options, cal).unwrap();
1650        assert_eq!(
1651            date.month().standard_code.0,
1652            "M01",
1653            "Month was successfully constrained"
1654        );
1655    }
1656
1657    #[test]
1658    fn test_from_fields_regress_7049() {
1659        // We want to make sure that overly large years do not panic
1660        // (we just reject them in Date::try_from_fields)
1661        let fields = DateFields {
1662            extended_year: Some(889192448),
1663            ordinal_month: Some(1),
1664            day: Some(1),
1665            ..Default::default()
1666        };
1667        let options = DateFromFieldsOptions {
1668            overflow: Some(Overflow::Reject),
1669            ..Default::default()
1670        };
1671
1672        let cal = ChineseTraditional::new();
1673        assert!(matches!(
1674            Date::try_from_fields(fields, options, cal).unwrap_err(),
1675            DateFromFieldsError::Range { .. }
1676        ));
1677    }
1678
1679    #[test]
1680    #[ignore] // slow, network
1681    fn test_against_hong_kong_observatory_data() {
1682        use crate::{cal::Gregorian, Date};
1683
1684        let mut related_iso = 1900;
1685        let mut lunar_month = ValidMonthCode::new_unchecked(11, false);
1686
1687        for year in 1901..=2100 {
1688            println!("Validating year {year}...");
1689
1690            for line in ureq::get(&format!(
1691                "https://www.hko.gov.hk/en/gts/time/calendar/text/files/T{year}e.txt"
1692            ))
1693            .call()
1694            .unwrap()
1695            .body_mut()
1696            .read_to_string()
1697            .unwrap()
1698            .split('\n')
1699            {
1700                if !line.starts_with(['1', '2']) {
1701                    // comments or blank lines
1702                    continue;
1703                }
1704
1705                let mut fields = line.split_ascii_whitespace();
1706
1707                let mut gregorian = fields.next().unwrap().split('/');
1708                let gregorian = Date::try_new_gregorian(
1709                    gregorian.next().unwrap().parse().unwrap(),
1710                    gregorian.next().unwrap().parse().unwrap(),
1711                    gregorian.next().unwrap().parse().unwrap(),
1712                )
1713                .unwrap();
1714
1715                let day_or_lunar_month = fields.next().unwrap();
1716
1717                let lunar_day = if fields.next().is_some_and(|s| s.contains("Lunar")) {
1718                    let new_lunar_month = day_or_lunar_month
1719                        // 1st, 2nd, 3rd, nth
1720                        .split_once(['s', 'n', 'r', 't'])
1721                        .unwrap()
1722                        .0
1723                        .parse()
1724                        .unwrap();
1725                    lunar_month = ValidMonthCode::new_unchecked(
1726                        new_lunar_month,
1727                        new_lunar_month == lunar_month.number(),
1728                    );
1729                    if new_lunar_month == 1 {
1730                        related_iso += 1;
1731                    }
1732                    1
1733                } else {
1734                    day_or_lunar_month.parse().unwrap()
1735                };
1736
1737                let chinese = Date::try_new_from_codes(
1738                    None,
1739                    related_iso,
1740                    lunar_month.to_month_code(),
1741                    lunar_day,
1742                    ChineseTraditional::new(),
1743                )
1744                .unwrap();
1745
1746                assert_eq!(
1747                    gregorian,
1748                    chinese.to_calendar(Gregorian),
1749                    "{line}, {chinese:?}"
1750                );
1751            }
1752        }
1753    }
1754
1755    #[test]
1756    #[ignore] // network
1757    fn test_against_kasi_data() {
1758        use crate::{cal::Gregorian, types::MonthCode, Date};
1759
1760        // TODO: query KASI directly
1761        let uri = "https://gist.githubusercontent.com/Manishearth/d8c94a7df22a9eacefc4472a5805322e/raw/e1ea3b0aa52428686bb3a9cd0f262878515e16c1/resolved.json";
1762
1763        #[derive(serde::Deserialize)]
1764        struct Golden(BTreeMap<i32, BTreeMap<MonthCode, MonthData>>);
1765
1766        #[derive(serde::Deserialize)]
1767        struct MonthData {
1768            start_date: String,
1769        }
1770
1771        let json = ureq::get(uri)
1772            .call()
1773            .unwrap()
1774            .body_mut()
1775            .read_to_string()
1776            .unwrap();
1777
1778        let golden = serde_json::from_str::<Golden>(&json).unwrap();
1779
1780        for (&year, months) in &golden.0 {
1781            if year == 1899 || year == 2050 {
1782                continue;
1783            }
1784            for (&month, month_data) in months {
1785                let mut gregorian = month_data.start_date.split('-');
1786                let gregorian = Date::try_new_gregorian(
1787                    gregorian.next().unwrap().parse().unwrap(),
1788                    gregorian.next().unwrap().parse().unwrap(),
1789                    gregorian.next().unwrap().parse().unwrap(),
1790                )
1791                .unwrap();
1792
1793                assert_eq!(
1794                    Date::try_new_from_codes(None, year, month, 1, KoreanTraditional::new())
1795                        .unwrap()
1796                        .to_calendar(Gregorian),
1797                    gregorian
1798                );
1799            }
1800        }
1801    }
1802
1803    #[test]
1804    #[ignore]
1805    fn generate_reference_years() {
1806        generate_reference_years_for(ChineseTraditional::new());
1807        generate_reference_years_for(KoreanTraditional::new());
1808        fn generate_reference_years_for<R: Rules + Copy>(calendar: EastAsianTraditional<R>) {
1809            use crate::Date;
1810
1811            println!("Reference years for {calendar:?}:");
1812            let reference_year_end = Date::from_rata_die(
1813                crate::cal::abstract_gregorian::LAST_DAY_OF_REFERENCE_YEAR,
1814                calendar,
1815            );
1816            let year_1900_start = Date::try_new_gregorian(1900, 1, 1)
1817                .unwrap()
1818                .to_calendar(calendar);
1819            let year_2035_end = Date::try_new_gregorian(2035, 12, 31)
1820                .unwrap()
1821                .to_calendar(calendar);
1822            for month in 1..=12 {
1823                for leap in [false, true] {
1824                    'outer: for long in [false, true] {
1825                        for (start_year, start_month, end_year, end_month, by) in [
1826                            (
1827                                reference_year_end.extended_year(),
1828                                reference_year_end.month().month_number(),
1829                                year_1900_start.extended_year(),
1830                                year_1900_start.month().month_number(),
1831                                -1,
1832                            ),
1833                            (
1834                                reference_year_end.extended_year(),
1835                                reference_year_end.month().month_number(),
1836                                year_2035_end.extended_year(),
1837                                year_2035_end.month().month_number(),
1838                                1,
1839                            ),
1840                            (
1841                                year_1900_start.extended_year(),
1842                                year_1900_start.month().month_number(),
1843                                -10000,
1844                                1,
1845                                -1,
1846                            ),
1847                        ] {
1848                            let mut year = start_year;
1849                            while year * by < end_year * by {
1850                                if year == start_year
1851                                    && month as i32 * by <= start_month as i32 * by
1852                                    || year == end_year
1853                                        && month as i32 * by >= end_month as i32 * by
1854                                {
1855                                    year += by;
1856                                    continue;
1857                                }
1858                                let data = calendar.0.year_data(year);
1859                                let leap_month = data.packed.leap_month().unwrap_or(15);
1860                                let ordinal_month = if leap && month + 1 == leap_month {
1861                                    month + 1
1862                                } else {
1863                                    month + (month + 1 > leap_month) as u8
1864                                };
1865                                if (!long || data.packed.month_has_30_days(ordinal_month))
1866                                    && (!leap || month + 1 == leap_month)
1867                                {
1868                                    println!("({month}, {leap:?}, {long:?}) => {year},");
1869                                    continue 'outer;
1870                                }
1871                                year += by;
1872                            }
1873                        }
1874                        println!("({month}, {leap:?}, {long:?}) => todo!(),")
1875                    }
1876                }
1877            }
1878        }
1879    }
1880
1881    #[test]
1882    fn test_roundtrip_packed() {
1883        fn packed_roundtrip_single(
1884            month_lengths: [bool; 13],
1885            leap_month_idx: Option<u8>,
1886            ny_offset: i64,
1887        ) {
1888            let ny =
1889                calendrical_calculations::gregorian::fixed_from_gregorian(1000, 1, 1) + ny_offset;
1890            let packed =
1891                PackedEastAsianTraditionalYearData::new(1000, month_lengths, leap_month_idx, ny);
1892
1893            assert_eq!(
1894                ny,
1895                packed.new_year(1000),
1896                "Roundtrip with {month_lengths:?}, {leap_month_idx:?}, {ny_offset}"
1897            );
1898            assert_eq!(
1899                leap_month_idx,
1900                packed.leap_month(),
1901                "Roundtrip with {month_lengths:?}, {leap_month_idx:?}, {ny_offset}"
1902            );
1903            assert_eq!(
1904                month_lengths,
1905                core::array::from_fn(|i| packed.month_has_30_days(i as u8 + 1)),
1906                "Roundtrip with {month_lengths:?}, {leap_month_idx:?}, {ny_offset}"
1907            );
1908        }
1909
1910        const SHORT: [bool; 13] = [false; 13];
1911        const LONG: [bool; 13] = [true; 13];
1912        const ALTERNATING1: [bool; 13] = [
1913            false, true, false, true, false, true, false, true, false, true, false, true, false,
1914        ];
1915        const ALTERNATING2: [bool; 13] = [
1916            true, false, true, false, true, false, true, false, true, false, true, false, false,
1917        ];
1918        const RANDOM1: [bool; 13] = [
1919            true, true, false, false, true, true, false, true, true, true, true, false, false,
1920        ];
1921        const RANDOM2: [bool; 13] = [
1922            false, true, true, true, true, false, true, true, true, false, false, true, false,
1923        ];
1924        packed_roundtrip_single(SHORT, None, 18 + 5);
1925        packed_roundtrip_single(SHORT, None, 18 + 10);
1926        packed_roundtrip_single(SHORT, Some(11), 18 + 15);
1927        packed_roundtrip_single(LONG, Some(12), 18 + 15);
1928        packed_roundtrip_single(ALTERNATING1, None, 18 + 2);
1929        packed_roundtrip_single(ALTERNATING1, Some(3), 18 + 5);
1930        packed_roundtrip_single(ALTERNATING2, None, 18 + 9);
1931        packed_roundtrip_single(ALTERNATING2, Some(7), 18 + 26);
1932        packed_roundtrip_single(RANDOM1, None, 18 + 29);
1933        packed_roundtrip_single(RANDOM1, Some(12), 18 + 29);
1934        packed_roundtrip_single(RANDOM1, Some(2), 18 + 21);
1935        packed_roundtrip_single(RANDOM2, None, 18 + 25);
1936        packed_roundtrip_single(RANDOM2, Some(2), 18 + 19);
1937        packed_roundtrip_single(RANDOM2, Some(5), 18 + 2);
1938        packed_roundtrip_single(RANDOM2, Some(12), 18 + 5);
1939    }
1940}