icu_calendar/cal/
dangi.rs

1// This file is part of ICU4X. For terms of use, please see the file
2// called LICENSE at the top level of the ICU4X source tree
3// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ).
4
5//! This module contains types and implementations for the Korean Dangi calendar.
6//!
7//! ```rust
8//! use icu::calendar::cal::Dangi;
9//! use icu::calendar::Date;
10//!
11//! let dangi = Dangi::new();
12//! let dangi_date = Date::try_new_dangi_with_calendar(2023, 6, 6, dangi)
13//!     .expect("Failed to initialize Dangi Date instance.");
14//!
15//! assert_eq!(dangi_date.cyclic_year().related_iso, 2023);
16//! assert_eq!(dangi_date.cyclic_year().year, 40);
17//! assert_eq!(dangi_date.month().ordinal, 6);
18//! assert_eq!(dangi_date.day_of_month().0, 6);
19//! ```
20
21use crate::cal::chinese_based::{ChineseBasedPrecomputedData, ChineseBasedWithDataLoading};
22use crate::cal::iso::{Iso, IsoDateInner};
23use crate::calendar_arithmetic::PrecomputedDataSource;
24use crate::calendar_arithmetic::{ArithmeticDate, CalendarArithmetic};
25use crate::error::DateError;
26use crate::provider::chinese_based::CalendarDangiV1;
27use crate::types::CyclicYear;
28use crate::AsCalendar;
29use crate::{types, Calendar, Date};
30use calendrical_calculations::chinese_based;
31use calendrical_calculations::rata_die::RataDie;
32use core::cmp::Ordering;
33use icu_provider::prelude::*;
34
35/// The [Traditional Korean (Dangi) Calendar](https://en.wikipedia.org/wiki/Korean_calendar)
36///
37/// The Dangi Calendar is a lunisolar calendar used traditionally in North and South Korea.
38/// It is often used today to track important cultural events and holidays like Seollal
39/// (Korean lunar new year). It is similar to the Chinese lunar calendar (see [`Chinese`](super::Chinese)),
40/// except that observations are based in Korea (currently UTC+9) rather than China (UTC+8).
41/// This can cause some differences; for example, 2012 was a leap year, but in the Dangi
42/// calendar the leap month was 3, while in the Chinese calendar the leap month was 4.
43///
44/// This calendar is currently in a preview state: formatting for this calendar is not
45/// going to be perfect.
46///
47/// ```rust
48/// use icu::calendar::cal::{Chinese, Dangi};
49/// use icu::calendar::Date;
50/// use tinystr::tinystr;
51///
52/// let iso_a = Date::try_new_iso(2012, 4, 23).unwrap();
53/// let dangi_a = iso_a.to_calendar(Dangi::new());
54/// let chinese_a = iso_a.to_calendar(Chinese::new());
55///
56/// assert_eq!(dangi_a.month().standard_code.0, tinystr!(4, "M03L"));
57/// assert_eq!(chinese_a.month().standard_code.0, tinystr!(4, "M04"));
58///
59/// let iso_b = Date::try_new_iso(2012, 5, 23).unwrap();
60/// let dangi_b = iso_b.to_calendar(Dangi::new());
61/// let chinese_b = iso_b.to_calendar(Chinese::new());
62///
63/// assert_eq!(dangi_b.month().standard_code.0, tinystr!(4, "M04"));
64/// assert_eq!(chinese_b.month().standard_code.0, tinystr!(4, "M04L"));
65/// ```
66/// # Era codes
67///
68/// This calendar does not use era codes.
69///
70/// # Month codes
71///
72/// This calendar is a lunisolar calendar. It supports regular month codes `"M01" - "M12"` as well
73/// as leap month codes `"M01L" - "M12L"`.
74#[derive(Clone, Debug, Default)]
75pub struct Dangi {
76    data: Option<DataPayload<CalendarDangiV1>>,
77}
78
79/// The inner date type used for representing [`Date`]s of [`Dangi`]. See [`Date`] and [`Dangi`] for more detail.
80#[derive(Debug, Eq, PartialEq, PartialOrd, Ord)]
81pub struct DangiDateInner(ArithmeticDate<Dangi>);
82
83// we want these impls without the `C: Copy/Clone` bounds
84impl Copy for DangiDateInner {}
85impl Clone for DangiDateInner {
86    fn clone(&self) -> Self {
87        *self
88    }
89}
90
91// These impls just make custom derives on types containing C
92// work. They're basically no-ops
93impl PartialEq for Dangi {
94    fn eq(&self, _: &Self) -> bool {
95        true
96    }
97}
98impl Eq for Dangi {}
99#[allow(clippy::non_canonical_partial_ord_impl)] // this is intentional
100impl PartialOrd for Dangi {
101    fn partial_cmp(&self, _: &Self) -> Option<Ordering> {
102        Some(Ordering::Equal)
103    }
104}
105
106impl Ord for Dangi {
107    fn cmp(&self, _: &Self) -> Ordering {
108        Ordering::Equal
109    }
110}
111
112impl Dangi {
113    /// Creates a new [`Dangi`] with some precomputed calendrical calculations.
114    ///
115    /// ✨ *Enabled with the `compiled_data` Cargo feature.*
116    ///
117    /// [📚 Help choosing a constructor](icu_provider::constructors)
118    #[cfg(feature = "compiled_data")]
119    pub const fn new() -> Self {
120        Self {
121            data: Some(DataPayload::from_static_ref(
122                crate::provider::Baked::SINGLETON_CALENDAR_DANGI_V1,
123            )),
124        }
125    }
126
127    icu_provider::gen_buffer_data_constructors!(() -> error: DataError,
128        functions: [
129            new: skip,
130            try_new_with_buffer_provider,
131            try_new_unstable,
132            Self,
133    ]);
134
135    #[doc = icu_provider::gen_buffer_unstable_docs!(UNSTABLE, Self::new)]
136    pub fn try_new_unstable<D: DataProvider<CalendarDangiV1> + ?Sized>(
137        provider: &D,
138    ) -> Result<Self, DataError> {
139        Ok(Self {
140            data: Some(provider.load(Default::default())?.payload),
141        })
142    }
143
144    /// Construct a new [`Dangi`] without any precomputed calendrical calculations.
145    pub fn new_always_calculating() -> Self {
146        Dangi { data: None }
147    }
148
149    pub(crate) const DEBUG_NAME: &'static str = "Dangi";
150}
151
152impl crate::cal::scaffold::UnstableSealed for Dangi {}
153impl Calendar for Dangi {
154    type DateInner = DangiDateInner;
155    type Year = CyclicYear;
156
157    fn from_codes(
158        &self,
159        era: Option<&str>,
160        year: i32,
161        month_code: crate::types::MonthCode,
162        day: u8,
163    ) -> Result<Self::DateInner, DateError> {
164        match era {
165            None => {}
166            _ => return Err(DateError::UnknownEra),
167        }
168
169        let year = self.get_precomputed_data().load_or_compute_info(year);
170
171        let Some(month) = year.parse_month_code(month_code) else {
172            return Err(DateError::UnknownMonthCode(month_code));
173        };
174
175        year.validate_md(month, day)?;
176
177        Ok(DangiDateInner(ArithmeticDate::new_unchecked(
178            year, month, day,
179        )))
180    }
181
182    fn from_rata_die(&self, rd: RataDie) -> Self::DateInner {
183        let iso = Iso.from_rata_die(rd);
184        let y = self
185            .get_precomputed_data()
186            .load_or_compute_info_for_rd(rd, iso.0);
187        let (m, d) = y.md_from_rd(rd);
188        DangiDateInner(ArithmeticDate::new_unchecked(y, m, d))
189    }
190
191    fn to_rata_die(&self, date: &Self::DateInner) -> RataDie {
192        date.0.year.rd_from_md(date.0.month, date.0.day)
193    }
194
195    fn from_iso(&self, iso: IsoDateInner) -> Self::DateInner {
196        let rd = Iso.to_rata_die(&iso);
197        let y = self
198            .get_precomputed_data()
199            .load_or_compute_info_for_rd(rd, iso.0);
200        let (m, d) = y.md_from_rd(rd);
201        DangiDateInner(ArithmeticDate::new_unchecked(y, m, d))
202    }
203
204    fn to_iso(&self, date: &Self::DateInner) -> IsoDateInner {
205        Iso.from_rata_die(self.to_rata_die(date))
206    }
207
208    fn months_in_year(&self, date: &Self::DateInner) -> u8 {
209        date.0.months_in_year()
210    }
211
212    fn days_in_year(&self, date: &Self::DateInner) -> u16 {
213        date.0.days_in_year()
214    }
215
216    fn days_in_month(&self, date: &Self::DateInner) -> u8 {
217        date.0.days_in_month()
218    }
219
220    fn offset_date(&self, date: &mut Self::DateInner, offset: crate::DateDuration<Self>) {
221        date.0.offset_date(offset, &self.get_precomputed_data());
222    }
223
224    fn until(
225        &self,
226        date1: &Self::DateInner,
227        date2: &Self::DateInner,
228        _calendar2: &Self,
229        largest_unit: crate::DateDurationUnit,
230        smallest_unit: crate::DateDurationUnit,
231    ) -> crate::DateDuration<Self> {
232        date1.0.until(date2.0, largest_unit, smallest_unit)
233    }
234
235    fn debug_name(&self) -> &'static str {
236        Self::DEBUG_NAME
237    }
238
239    fn year_info(&self, date: &Self::DateInner) -> Self::Year {
240        let year = date.0.year;
241        CyclicYear {
242            year: (year.related_iso as i64 - 4).rem_euclid(60) as u8 + 1,
243            related_iso: year.related_iso,
244        }
245    }
246
247    fn extended_year(&self, date: &Self::DateInner) -> i32 {
248        chinese_based::extended_from_iso::<chinese_based::Dangi>(date.0.year.related_iso)
249    }
250
251    fn is_in_leap_year(&self, date: &Self::DateInner) -> bool {
252        Self::provided_year_is_leap(date.0.year)
253    }
254
255    fn month(&self, date: &Self::DateInner) -> crate::types::MonthInfo {
256        date.0.year.month(date.0.month)
257    }
258
259    fn day_of_month(&self, date: &Self::DateInner) -> crate::types::DayOfMonth {
260        date.0.day_of_month()
261    }
262
263    fn day_of_year(&self, date: &Self::DateInner) -> crate::types::DayOfYear {
264        types::DayOfYear(date.0.year.day_of_year(date.0.month, date.0.day))
265    }
266
267    fn calendar_algorithm(&self) -> Option<crate::preferences::CalendarAlgorithm> {
268        Some(crate::preferences::CalendarAlgorithm::Dangi)
269    }
270}
271
272impl<A: AsCalendar<Calendar = Dangi>> Date<A> {
273    /// Construct a new Dangi date from a `year`, `month`, and `day`.
274    /// `year` represents the [ISO](crate::Iso) year that roughly matches the Dangi year;
275    /// `month` represents the month of the year ordinally (ex. if it is a leap year, the last month will be 13, not 12);
276    /// `day` indicates day of month.
277    ///
278    /// This date will not use any precomputed calendrical calculations,
279    /// one that loads such data from a provider will be added in the future (#3933)
280    ///
281    /// ```rust
282    /// use icu::calendar::cal::Dangi;
283    /// use icu::calendar::Date;
284    ///
285    /// let dangi = Dangi::new();
286    ///
287    /// let date_dangi = Date::try_new_dangi_with_calendar(2023, 6, 18, dangi)
288    ///     .expect("Failed to initialize Dangi Date instance.");
289    ///
290    /// assert_eq!(date_dangi.cyclic_year().related_iso, 2023);
291    /// assert_eq!(date_dangi.cyclic_year().year, 40);
292    /// assert_eq!(date_dangi.month().ordinal, 6);
293    /// assert_eq!(date_dangi.day_of_month().0, 18);
294    /// ```
295    pub fn try_new_dangi_with_calendar(
296        related_iso_year: i32,
297        month: u8,
298        day: u8,
299        calendar: A,
300    ) -> Result<Date<A>, DateError> {
301        let year = calendar
302            .as_calendar()
303            .get_precomputed_data()
304            .load_or_compute_info(related_iso_year);
305        year.validate_md(month, day)?;
306        Ok(Date::from_raw(
307            DangiDateInner(ArithmeticDate::new_unchecked(year, month, day)),
308            calendar,
309        ))
310    }
311}
312
313impl ChineseBasedWithDataLoading for Dangi {
314    type CB = calendrical_calculations::chinese_based::Dangi;
315    fn get_precomputed_data(&self) -> ChineseBasedPrecomputedData<Self::CB> {
316        ChineseBasedPrecomputedData::new(self.data.as_ref().map(|d| d.get()))
317    }
318}
319
320#[cfg(test)]
321mod test {
322
323    use super::*;
324    use crate::cal::Chinese;
325    use calendrical_calculations::rata_die::RataDie;
326
327    /// Run a test twice, with two calendars
328    fn do_twice(
329        dangi_calculating: &Dangi,
330        dangi_cached: &Dangi,
331        test: impl Fn(crate::Ref<Dangi>, &'static str),
332    ) {
333        test(crate::Ref(dangi_calculating), "calculating");
334        test(crate::Ref(dangi_cached), "cached");
335    }
336
337    fn check_cyclic_and_rel_iso(year: i32) {
338        let iso = Date::try_new_iso(year, 6, 6).unwrap();
339        let chinese = iso.to_calendar(Chinese::new_always_calculating());
340        let dangi = iso.to_calendar(Dangi::new_always_calculating());
341        let chinese_year = chinese.cyclic_year();
342        let korean_year = dangi.cyclic_year();
343        assert_eq!(
344            chinese_year, korean_year,
345            "Cyclic year failed for year: {year}"
346        );
347        let chinese_rel_iso = chinese_year.related_iso;
348        let korean_rel_iso = korean_year.related_iso;
349        assert_eq!(
350            chinese_rel_iso, korean_rel_iso,
351            "Rel. ISO year equality failed for year: {year}"
352        );
353        assert_eq!(korean_rel_iso, year, "Dangi Rel. ISO failed!");
354    }
355
356    #[test]
357    fn test_cyclic_same_as_chinese_near_present_day() {
358        for year in 1923..=2123 {
359            check_cyclic_and_rel_iso(year);
360        }
361    }
362
363    #[test]
364    fn test_cyclic_same_as_chinese_near_rd_zero() {
365        for year in -100..=100 {
366            check_cyclic_and_rel_iso(year);
367        }
368    }
369
370    #[test]
371    fn test_iso_to_dangi_roundtrip() {
372        let mut rd = -1963020;
373        let max_rd = 1963020;
374        let mut iters = 0;
375        let max_iters = 560;
376        let dangi_calculating = Dangi::new_always_calculating();
377        let dangi_cached = Dangi::new();
378        while rd < max_rd && iters < max_iters {
379            let rata_die = RataDie::new(rd);
380            let iso = Date::from_rata_die(rata_die, Iso);
381            do_twice(&dangi_calculating, &dangi_cached, |dangi, calendar_type| {
382                let korean = iso.to_calendar(dangi);
383                let result = korean.to_calendar(Iso);
384                assert_eq!(
385                    iso, result,
386                    "[{calendar_type}] Failed roundtrip ISO -> Dangi -> ISO for RD: {rd}"
387                );
388            });
389
390            rd += 7043;
391            iters += 1;
392        }
393    }
394
395    #[test]
396    fn test_dangi_consistent_with_icu() {
397        // Test cases for this test are derived from existing ICU Intl.DateTimeFormat. If there is a bug in ICU,
398        // these test cases may be affected, and this calendar's output may not be entirely valid.
399
400        // There are a number of test cases which do not match ICU for dates very far in the past or future,
401        // see #3709.
402
403        #[derive(Debug)]
404        struct TestCase {
405            iso_year: i32,
406            iso_month: u8,
407            iso_day: u8,
408            expected_rel_iso: i32,
409            expected_cyclic: u8,
410            expected_month: u8,
411            expected_day: u8,
412        }
413
414        let cases = [
415            TestCase {
416                // #3709: This test case fails to match ICU
417                iso_year: 4321,
418                iso_month: 1,
419                iso_day: 23,
420                expected_rel_iso: 4320,
421                expected_cyclic: 57,
422                expected_month: 13,
423                expected_day: 12,
424            },
425            TestCase {
426                iso_year: 3649,
427                iso_month: 9,
428                iso_day: 20,
429                expected_rel_iso: 3649,
430                expected_cyclic: 46,
431                expected_month: 9,
432                expected_day: 1,
433            },
434            TestCase {
435                iso_year: 3333,
436                iso_month: 3,
437                iso_day: 3,
438                expected_rel_iso: 3333,
439                expected_cyclic: 30,
440                expected_month: 1,
441                expected_day: 25,
442            },
443            TestCase {
444                iso_year: 3000,
445                iso_month: 3,
446                iso_day: 30,
447                expected_rel_iso: 3000,
448                expected_cyclic: 57,
449                expected_month: 3,
450                expected_day: 3,
451            },
452            TestCase {
453                iso_year: 2772,
454                iso_month: 7,
455                iso_day: 27,
456                expected_rel_iso: 2772,
457                expected_cyclic: 9,
458                expected_month: 7,
459                expected_day: 5,
460            },
461            TestCase {
462                iso_year: 2525,
463                iso_month: 2,
464                iso_day: 25,
465                expected_rel_iso: 2525,
466                expected_cyclic: 2,
467                expected_month: 2,
468                expected_day: 3,
469            },
470            TestCase {
471                iso_year: 2345,
472                iso_month: 3,
473                iso_day: 21,
474                expected_rel_iso: 2345,
475                expected_cyclic: 2,
476                expected_month: 2,
477                expected_day: 17,
478            },
479            TestCase {
480                iso_year: 2222,
481                iso_month: 2,
482                iso_day: 22,
483                expected_rel_iso: 2222,
484                expected_cyclic: 59,
485                expected_month: 1,
486                expected_day: 11,
487            },
488            TestCase {
489                iso_year: 2167,
490                iso_month: 6,
491                iso_day: 22,
492                expected_rel_iso: 2167,
493                expected_cyclic: 4,
494                expected_month: 5,
495                expected_day: 6,
496            },
497            TestCase {
498                iso_year: 2121,
499                iso_month: 2,
500                iso_day: 12,
501                expected_rel_iso: 2120,
502                expected_cyclic: 17,
503                expected_month: 13,
504                expected_day: 25,
505            },
506            TestCase {
507                iso_year: 2080,
508                iso_month: 12,
509                iso_day: 31,
510                expected_rel_iso: 2080,
511                expected_cyclic: 37,
512                expected_month: 12,
513                expected_day: 21,
514            },
515            TestCase {
516                iso_year: 2030,
517                iso_month: 3,
518                iso_day: 20,
519                expected_rel_iso: 2030,
520                expected_cyclic: 47,
521                expected_month: 2,
522                expected_day: 17,
523            },
524            TestCase {
525                iso_year: 2027,
526                iso_month: 2,
527                iso_day: 7,
528                expected_rel_iso: 2027,
529                expected_cyclic: 44,
530                expected_month: 1,
531                expected_day: 1,
532            },
533            TestCase {
534                iso_year: 2023,
535                iso_month: 7,
536                iso_day: 1,
537                expected_rel_iso: 2023,
538                expected_cyclic: 40,
539                expected_month: 6,
540                expected_day: 14,
541            },
542            TestCase {
543                iso_year: 2022,
544                iso_month: 3,
545                iso_day: 1,
546                expected_rel_iso: 2022,
547                expected_cyclic: 39,
548                expected_month: 1,
549                expected_day: 29,
550            },
551            TestCase {
552                iso_year: 2021,
553                iso_month: 2,
554                iso_day: 1,
555                expected_rel_iso: 2020,
556                expected_cyclic: 37,
557                expected_month: 13,
558                expected_day: 20,
559            },
560            TestCase {
561                iso_year: 2016,
562                iso_month: 3,
563                iso_day: 30,
564                expected_rel_iso: 2016,
565                expected_cyclic: 33,
566                expected_month: 2,
567                expected_day: 22,
568            },
569            TestCase {
570                iso_year: 2016,
571                iso_month: 7,
572                iso_day: 30,
573                expected_rel_iso: 2016,
574                expected_cyclic: 33,
575                expected_month: 6,
576                expected_day: 27,
577            },
578            TestCase {
579                iso_year: 2015,
580                iso_month: 9,
581                iso_day: 22,
582                expected_rel_iso: 2015,
583                expected_cyclic: 32,
584                expected_month: 8,
585                expected_day: 10,
586            },
587            TestCase {
588                iso_year: 2013,
589                iso_month: 10,
590                iso_day: 1,
591                expected_rel_iso: 2013,
592                expected_cyclic: 30,
593                expected_month: 8,
594                expected_day: 27,
595            },
596            TestCase {
597                iso_year: 2010,
598                iso_month: 2,
599                iso_day: 1,
600                expected_rel_iso: 2009,
601                expected_cyclic: 26,
602                expected_month: 13,
603                expected_day: 18,
604            },
605            TestCase {
606                iso_year: 2000,
607                iso_month: 8,
608                iso_day: 30,
609                expected_rel_iso: 2000,
610                expected_cyclic: 17,
611                expected_month: 8,
612                expected_day: 2,
613            },
614            TestCase {
615                iso_year: 1990,
616                iso_month: 11,
617                iso_day: 11,
618                expected_rel_iso: 1990,
619                expected_cyclic: 7,
620                expected_month: 10,
621                expected_day: 24,
622            },
623            TestCase {
624                iso_year: 1970,
625                iso_month: 6,
626                iso_day: 10,
627                expected_rel_iso: 1970,
628                expected_cyclic: 47,
629                expected_month: 5,
630                expected_day: 7,
631            },
632            TestCase {
633                iso_year: 1970,
634                iso_month: 1,
635                iso_day: 1,
636                expected_rel_iso: 1969,
637                expected_cyclic: 46,
638                expected_month: 11,
639                expected_day: 24,
640            },
641            TestCase {
642                iso_year: 1941,
643                iso_month: 12,
644                iso_day: 7,
645                expected_rel_iso: 1941,
646                expected_cyclic: 18,
647                expected_month: 11,
648                expected_day: 19,
649            },
650            TestCase {
651                iso_year: 1812,
652                iso_month: 5,
653                iso_day: 4,
654                expected_rel_iso: 1812,
655                expected_cyclic: 9,
656                expected_month: 3,
657                expected_day: 24,
658            },
659            TestCase {
660                iso_year: 1655,
661                iso_month: 6,
662                iso_day: 15,
663                expected_rel_iso: 1655,
664                expected_cyclic: 32,
665                expected_month: 5,
666                expected_day: 12,
667            },
668            TestCase {
669                iso_year: 1333,
670                iso_month: 3,
671                iso_day: 10,
672                expected_rel_iso: 1333,
673                expected_cyclic: 10,
674                expected_month: 2,
675                expected_day: 16,
676            },
677            TestCase {
678                iso_year: 1000,
679                iso_month: 10,
680                iso_day: 10,
681                expected_rel_iso: 1000,
682                expected_cyclic: 37,
683                expected_month: 9,
684                expected_day: 5,
685            },
686            TestCase {
687                iso_year: 842,
688                iso_month: 2,
689                iso_day: 15,
690                expected_rel_iso: 841,
691                expected_cyclic: 58,
692                expected_month: 13,
693                expected_day: 28,
694            },
695            TestCase {
696                iso_year: 101,
697                iso_month: 1,
698                iso_day: 10,
699                expected_rel_iso: 100,
700                expected_cyclic: 37,
701                expected_month: 12,
702                expected_day: 24,
703            },
704            TestCase {
705                iso_year: -1,
706                iso_month: 3,
707                iso_day: 28,
708                expected_rel_iso: -1,
709                expected_cyclic: 56,
710                expected_month: 2,
711                expected_day: 25,
712            },
713            TestCase {
714                iso_year: -3,
715                iso_month: 2,
716                iso_day: 28,
717                expected_rel_iso: -3,
718                expected_cyclic: 54,
719                expected_month: 2,
720                expected_day: 5,
721            },
722            TestCase {
723                iso_year: -365,
724                iso_month: 7,
725                iso_day: 24,
726                expected_rel_iso: -365,
727                expected_cyclic: 52,
728                expected_month: 6,
729                expected_day: 24,
730            },
731            TestCase {
732                iso_year: -999,
733                iso_month: 9,
734                iso_day: 9,
735                expected_rel_iso: -999,
736                expected_cyclic: 18,
737                expected_month: 7,
738                expected_day: 27,
739            },
740            TestCase {
741                iso_year: -1500,
742                iso_month: 1,
743                iso_day: 5,
744                expected_rel_iso: -1501,
745                expected_cyclic: 56,
746                expected_month: 12,
747                expected_day: 2,
748            },
749            TestCase {
750                iso_year: -2332,
751                iso_month: 3,
752                iso_day: 1,
753                expected_rel_iso: -2332,
754                expected_cyclic: 5,
755                expected_month: 1,
756                expected_day: 16,
757            },
758            TestCase {
759                iso_year: -2332,
760                iso_month: 2,
761                iso_day: 15,
762                expected_rel_iso: -2332,
763                expected_cyclic: 5,
764                expected_month: 1,
765                expected_day: 1,
766            },
767            TestCase {
768                // #3709: This test case fails to match ICU
769                iso_year: -2332,
770                iso_month: 2,
771                iso_day: 14,
772                expected_rel_iso: -2333,
773                expected_cyclic: 4,
774                expected_month: 13,
775                expected_day: 30,
776            },
777            TestCase {
778                // #3709: This test case fails to match ICU
779                iso_year: -2332,
780                iso_month: 1,
781                iso_day: 17,
782                expected_rel_iso: -2333,
783                expected_cyclic: 4,
784                expected_month: 13,
785                expected_day: 2,
786            },
787            TestCase {
788                // #3709: This test case fails to match ICU
789                iso_year: -2332,
790                iso_month: 1,
791                iso_day: 16,
792                expected_rel_iso: -2333,
793                expected_cyclic: 4,
794                expected_month: 13,
795                expected_day: 1,
796            },
797            TestCase {
798                iso_year: -2332,
799                iso_month: 1,
800                iso_day: 15,
801                expected_rel_iso: -2333,
802                expected_cyclic: 4,
803                expected_month: 12,
804                expected_day: 29,
805            },
806            TestCase {
807                iso_year: -2332,
808                iso_month: 1,
809                iso_day: 1,
810                expected_rel_iso: -2333,
811                expected_cyclic: 4,
812                expected_month: 12,
813                expected_day: 15,
814            },
815            TestCase {
816                iso_year: -2333,
817                iso_month: 1,
818                iso_day: 16,
819                expected_rel_iso: -2334,
820                expected_cyclic: 3,
821                expected_month: 12,
822                expected_day: 19,
823            },
824            TestCase {
825                iso_year: -2333,
826                iso_month: 1,
827                iso_day: 27,
828                expected_rel_iso: -2333,
829                expected_cyclic: 4,
830                expected_month: 1,
831                expected_day: 1,
832            },
833            TestCase {
834                iso_year: -2333,
835                iso_month: 1,
836                iso_day: 26,
837                expected_rel_iso: -2334,
838                expected_cyclic: 3,
839                expected_month: 12,
840                expected_day: 29,
841            },
842            TestCase {
843                iso_year: -2600,
844                iso_month: 9,
845                iso_day: 16,
846                expected_rel_iso: -2600,
847                expected_cyclic: 37,
848                expected_month: 8,
849                expected_day: 16,
850            },
851            TestCase {
852                iso_year: -2855,
853                iso_month: 2,
854                iso_day: 3,
855                expected_rel_iso: -2856,
856                expected_cyclic: 21,
857                expected_month: 12,
858                expected_day: 30,
859            },
860            TestCase {
861                // #3709: This test case fails to match ICU
862                iso_year: -3000,
863                iso_month: 5,
864                iso_day: 15,
865                expected_rel_iso: -3000,
866                expected_cyclic: 57,
867                expected_month: 4,
868                expected_day: 1,
869            },
870            TestCase {
871                // #3709: This test case fails to match ICU
872                iso_year: -3649,
873                iso_month: 9,
874                iso_day: 20,
875                expected_rel_iso: -3649,
876                expected_cyclic: 8,
877                expected_month: 8,
878                expected_day: 10,
879            },
880            TestCase {
881                // #3709: This test case fails to match ICU
882                iso_year: -3649,
883                iso_month: 3,
884                iso_day: 30,
885                expected_rel_iso: -3649,
886                expected_cyclic: 8,
887                expected_month: 2,
888                expected_day: 14,
889            },
890            TestCase {
891                // #3709: This test case fails to match ICU
892                iso_year: -3650,
893                iso_month: 3,
894                iso_day: 30,
895                expected_rel_iso: -3650,
896                expected_cyclic: 7,
897                expected_month: 3,
898                expected_day: 3,
899            },
900        ];
901
902        let dangi_calculating = Dangi::new_always_calculating();
903        let dangi_cached = Dangi::new();
904
905        for case in cases {
906            let iso = Date::try_new_iso(case.iso_year, case.iso_month, case.iso_day).unwrap();
907            do_twice(&dangi_calculating, &dangi_cached, |dangi, calendar_type| {
908                let dangi = iso.to_calendar(dangi);
909                let dangi_cyclic = dangi.cyclic_year();
910                let dangi_month = dangi.month().ordinal;
911                let dangi_day = dangi.day_of_month().0;
912
913                assert_eq!(
914                    dangi_cyclic.related_iso, case.expected_rel_iso,
915                    "[{calendar_type}] Related ISO failed for test case: {case:?}"
916                );
917                assert_eq!(
918                    dangi_cyclic.year, case.expected_cyclic,
919                    "[{calendar_type}] Cyclic year failed for test case: {case:?}"
920                );
921                assert_eq!(
922                    dangi_month, case.expected_month,
923                    "[{calendar_type}] Month failed for test case: {case:?}"
924                );
925                assert_eq!(
926                    dangi_day, case.expected_day,
927                    "[{calendar_type}] Day failed for test case: {case:?}"
928                );
929            });
930        }
931    }
932}