1use crate::astronomy::{self, Astronomical, MEAN_SYNODIC_MONTH, MEAN_TROPICAL_YEAR};
2use crate::gregorian::{fixed_from_gregorian, gregorian_from_fixed};
3use crate::helpers::i64_to_i32;
4use crate::rata_die::{Moment, RataDie};
5use core::num::NonZeroU8;
6use core::ops::Range;
7#[allow(unused_imports)]
8use core_maths::*;
9
10const MAX_ITERS_FOR_MONTHS_OF_YEAR: u8 = 14;
12
13pub const WELL_BEHAVED_ASTRONOMICAL_RANGE: Range<RataDie> =
31 RataDie::new(365 * -10_000)..RataDie::new(365 * 10_000);
32
33pub trait ChineseBased {
41 fn utc_offset(fixed: RataDie) -> f64;
45
46 const EPOCH: RataDie;
50
51 const DEBUG_NAME: &'static str;
53}
54
55#[deprecated(since = "0.2.3", note = "extended year calculation subject to removal")]
57pub fn extended_from_iso<C: ChineseBased>(iso_year: i32) -> i32 {
58 iso_year
59 - const {
60 let Ok(y) = crate::gregorian::year_from_fixed(C::EPOCH) else {
61 panic!()
62 };
63 y - 1
64 }
65}
66#[deprecated(since = "0.2.3", note = "extended year calculation subject to removal")]
68pub fn iso_from_extended<C: ChineseBased>(extended_year: i32) -> i32 {
69 extended_year
70 + const {
71 let Ok(y) = crate::gregorian::year_from_fixed(C::EPOCH) else {
72 panic!()
73 };
74 y - 1
75 }
76}
77
78#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Default, Hash)]
80#[allow(clippy::exhaustive_structs)] pub struct Chinese;
82
83#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Default, Hash)]
85#[allow(clippy::exhaustive_structs)] pub struct Dangi;
87
88impl ChineseBased for Chinese {
89 fn utc_offset(fixed: RataDie) -> f64 {
90 if fixed < const { fixed_from_gregorian(1929, 1, 1) } {
94 1397.0 / 180.0 / 24.0
95 } else {
96 8.0 / 24.0
97 }
98 }
99
100 const EPOCH: RataDie = fixed_from_gregorian(-2636, 2, 15);
102 const DEBUG_NAME: &'static str = "chinese";
103}
104
105impl ChineseBased for Dangi {
106 fn utc_offset(fixed: RataDie) -> f64 {
107 if fixed < const { fixed_from_gregorian(1908, 4, 1) } {
111 3809.0 / 450.0 / 24.0
112 } else if fixed < const { fixed_from_gregorian(1912, 1, 1) } {
113 8.5 / 24.0
114 } else if fixed < const { fixed_from_gregorian(1954, 3, 21) } {
115 9.0 / 24.0
116 } else if fixed < const { fixed_from_gregorian(1961, 8, 10) } {
117 8.5 / 24.0
118 } else {
119 9.0 / 24.0
120 }
121 }
122
123 const EPOCH: RataDie = fixed_from_gregorian(-2332, 2, 15);
125 const DEBUG_NAME: &'static str = "dangi";
126}
127
128#[derive(Debug, Copy, Clone)]
130#[allow(clippy::exhaustive_structs)] pub struct YearBounds {
132 pub new_year: RataDie,
134 pub next_new_year: RataDie,
136}
137
138impl YearBounds {
139 #[inline]
144 pub fn compute<C: ChineseBased>(date: RataDie) -> Self {
145 let prev_solstice = winter_solstice_on_or_before::<C>(date);
146 let (new_year, next_solstice) = new_year_on_or_before_fixed_date::<C>(date, prev_solstice);
147 let next_new_year = new_year_on_or_before_fixed_date::<C>(new_year + 400, next_solstice).0;
149
150 Self {
151 new_year,
152 next_new_year,
153 }
154 }
155
156 pub fn count_days(self) -> u16 {
158 let result = self.next_new_year - self.new_year;
159 debug_assert!(
160 ((u16::MIN as i64)..=(u16::MAX as i64)).contains(&result),
161 "Days in year should be in range of u16."
162 );
163 result as u16
164 }
165
166 pub fn is_leap(self) -> bool {
168 let difference = self.next_new_year - self.new_year;
169 difference > 365
170 }
171}
172
173pub(crate) fn major_solar_term_from_fixed<C: ChineseBased>(date: RataDie) -> u32 {
178 let moment: Moment = date.as_moment();
179 let universal = moment - C::utc_offset(date);
180 let solar_longitude =
181 i64_to_i32(Astronomical::solar_longitude(Astronomical::julian_centuries(universal)) as i64);
182 debug_assert!(
183 solar_longitude.is_ok(),
184 "Solar longitude should be in range of i32"
185 );
186 let s = solar_longitude.unwrap_or_else(|e| e.saturate());
187 let result_signed = (2 + s.div_euclid(30) - 1).rem_euclid(12) + 1;
188 debug_assert!(result_signed >= 0);
189 result_signed as u32
190}
191
192pub(crate) fn new_moon_on_or_after<C: ChineseBased>(moment: Moment) -> RataDie {
197 let new_moon_moment = Astronomical::new_moon_at_or_after(midnight::<C>(moment));
198 let utc_offset = C::utc_offset(new_moon_moment.as_rata_die());
199 (new_moon_moment + utc_offset).as_rata_die()
200}
201
202pub(crate) fn new_moon_before<C: ChineseBased>(moment: Moment) -> RataDie {
207 let new_moon_moment = Astronomical::new_moon_before(midnight::<C>(moment));
208 let utc_offset = C::utc_offset(new_moon_moment.as_rata_die());
209 (new_moon_moment + utc_offset).as_rata_die()
210}
211
212pub(crate) fn midnight<C: ChineseBased>(moment: Moment) -> Moment {
217 moment - C::utc_offset(moment.as_rata_die())
218}
219
220pub(crate) fn new_year_in_sui<C: ChineseBased>(prior_solstice: RataDie) -> (RataDie, RataDie) {
228 let prior_solstice = bind_winter_solstice::<C>(prior_solstice);
234 let following_solstice =
235 bind_winter_solstice::<C>(winter_solstice_on_or_before::<C>(prior_solstice + 370)); let month_after_eleventh = new_moon_on_or_after::<C>((prior_solstice + 1).as_moment()); debug_assert!(
238 month_after_eleventh - prior_solstice >= 0
239 || !WELL_BEHAVED_ASTRONOMICAL_RANGE.contains(&prior_solstice)
240 );
241 let month_after_twelfth = new_moon_on_or_after::<C>((month_after_eleventh + 1).as_moment()); let month_after_thirteenth = new_moon_on_or_after::<C>((month_after_twelfth + 1).as_moment());
243 debug_assert!(
244 month_after_twelfth - month_after_eleventh >= 29
245 || !WELL_BEHAVED_ASTRONOMICAL_RANGE.contains(&prior_solstice)
246 );
247 let next_eleventh_month = new_moon_before::<C>((following_solstice + 1).as_moment()); let lhs_argument =
249 ((next_eleventh_month - month_after_eleventh) as f64 / MEAN_SYNODIC_MONTH).round() as i64;
250 let solar_term_a = major_solar_term_from_fixed::<C>(month_after_eleventh);
251 let solar_term_b = major_solar_term_from_fixed::<C>(month_after_twelfth);
252 let solar_term_c = major_solar_term_from_fixed::<C>(month_after_thirteenth);
253 if lhs_argument == 12 && (solar_term_a == solar_term_b || solar_term_b == solar_term_c) {
254 (month_after_thirteenth, following_solstice)
255 } else {
256 (month_after_twelfth, following_solstice)
257 }
258}
259
260fn bind_winter_solstice<C: ChineseBased>(solstice: RataDie) -> RataDie {
265 let (gregorian_year, gregorian_month, gregorian_day) = match gregorian_from_fixed(solstice) {
266 Ok(ymd) => ymd,
267 Err(_) => {
268 debug_assert!(false, "Solstice REALLY out of bounds: {solstice:?}");
269 return solstice;
270 }
271 };
272 let resolved_solstice = if gregorian_month < 12 || gregorian_day < 20 {
273 fixed_from_gregorian(gregorian_year, 12, 20)
274 } else if gregorian_day > 23 {
275 fixed_from_gregorian(gregorian_year, 12, 23)
276 } else {
277 solstice
278 };
279 if resolved_solstice != solstice {
280 if !(0..=4000).contains(&gregorian_year) {
281 #[cfg(feature = "logging")]
282 log::trace!("({}) Solstice out of bounds: {solstice:?}", C::DEBUG_NAME);
283 } else {
284 debug_assert!(
285 false,
286 "({}) Solstice out of bounds: {solstice:?}",
287 C::DEBUG_NAME
288 );
289 }
290 }
291 resolved_solstice
292}
293
294pub(crate) fn winter_solstice_on_or_before<C: ChineseBased>(date: RataDie) -> RataDie {
303 let approx = Astronomical::estimate_prior_solar_longitude(
304 astronomy::WINTER,
305 midnight::<C>((date + 1).as_moment()),
306 );
307 let mut iters = 0;
308 let mut day = Moment::new((approx.inner() - 1.0).floor());
309 while iters < MAX_ITERS_FOR_MONTHS_OF_YEAR
310 && astronomy::WINTER
311 >= Astronomical::solar_longitude(Astronomical::julian_centuries(midnight::<C>(
312 day + 1.0,
313 )))
314 {
315 iters += 1;
316 day += 1.0;
317 }
318 debug_assert!(
319 iters < MAX_ITERS_FOR_MONTHS_OF_YEAR || !WELL_BEHAVED_ASTRONOMICAL_RANGE.contains(&date),
320 "Number of iterations was higher than expected"
321 );
322 day.as_rata_die()
323}
324
325pub(crate) fn new_year_on_or_before_fixed_date<C: ChineseBased>(
334 date: RataDie,
335 prior_solstice: RataDie,
336) -> (RataDie, RataDie) {
337 let new_year = new_year_in_sui::<C>(prior_solstice);
338 if date >= new_year.0 {
339 new_year
340 } else {
341 let date_in_last_sui = date - 180; let prior_solstice = winter_solstice_on_or_before::<C>(date_in_last_sui);
347 new_year_in_sui::<C>(prior_solstice)
348 }
349}
350
351pub fn fixed_mid_year_from_year<C: ChineseBased>(elapsed_years: i32) -> RataDie {
360 let cycle = (elapsed_years - 1).div_euclid(60) + 1;
361 let year = (elapsed_years - 1).rem_euclid(60) + 1;
362 C::EPOCH + ((((cycle - 1) * 60 + year - 1) as f64 + 0.5) * MEAN_TROPICAL_YEAR) as i64
363}
364
365pub fn is_leap_year<C: ChineseBased>(year: i32) -> bool {
367 let mid_year = fixed_mid_year_from_year::<C>(year);
368 YearBounds::compute::<C>(mid_year).is_leap()
369}
370
371pub fn last_month_day_in_year<C: ChineseBased>(year: i32) -> (u8, u8) {
373 let mid_year = fixed_mid_year_from_year::<C>(year);
374 let year_bounds = YearBounds::compute::<C>(mid_year);
375 let last_day = year_bounds.next_new_year - 1;
376 let month = if year_bounds.is_leap() { 13 } else { 12 };
377 let day = last_day - new_moon_before::<C>(last_day.as_moment()) + 1;
378 (month, day as u8)
379}
380
381pub fn days_in_provided_year<C: ChineseBased>(year: i32) -> u16 {
383 let mid_year = fixed_mid_year_from_year::<C>(year);
384 let bounds = YearBounds::compute::<C>(mid_year);
385
386 bounds.count_days()
387}
388
389#[derive(Debug)]
391#[non_exhaustive]
392pub struct ChineseFromFixedResult {
393 pub year: i32,
395 pub month: u8,
397 pub day: u8,
399 pub year_bounds: YearBounds,
401 pub leap_month: Option<NonZeroU8>,
403}
404
405pub fn chinese_based_date_from_fixed<C: ChineseBased>(date: RataDie) -> ChineseFromFixedResult {
414 let year_bounds = YearBounds::compute::<C>(date);
415 let first_day_of_year = year_bounds.new_year;
416
417 let year_float =
418 (1.5 - 1.0 / 12.0 + ((first_day_of_year - C::EPOCH) as f64) / MEAN_TROPICAL_YEAR).floor();
419 let year_int = i64_to_i32(year_float as i64);
420 debug_assert!(year_int.is_ok(), "Year should be in range of i32");
421 let year = year_int.unwrap_or_else(|e| e.saturate());
422
423 let new_moon = new_moon_before::<C>((date + 1).as_moment());
424 let month_i64 = ((new_moon - first_day_of_year) as f64 / MEAN_SYNODIC_MONTH).round() as i64 + 1;
425 debug_assert!(
426 ((u8::MIN as i64)..=(u8::MAX as i64)).contains(&month_i64),
427 "Month should be in range of u8! Value {month_i64} failed for RD {date:?}"
428 );
429 let month = month_i64 as u8;
430 let day_i64 = date - new_moon + 1;
431 debug_assert!(
432 ((u8::MIN as i64)..=(u8::MAX as i64)).contains(&month_i64),
433 "Day should be in range of u8! Value {month_i64} failed for RD {date:?}"
434 );
435 let day = day_i64 as u8;
436 let leap_month = if year_bounds.is_leap() {
437 NonZeroU8::new(get_leap_month_from_new_year::<C>(first_day_of_year))
440 } else {
441 None
442 };
443
444 ChineseFromFixedResult {
445 year,
446 month,
447 day,
448 year_bounds,
449 leap_month,
450 }
451}
452
453pub fn get_leap_month_from_new_year<C: ChineseBased>(new_year: RataDie) -> u8 {
465 let mut cur = new_year;
466 let mut result = 1;
467 let mut solar_term = major_solar_term_from_fixed::<C>(cur);
468 loop {
469 let next = new_moon_on_or_after::<C>((cur + 1).as_moment());
470 let next_solar_term = major_solar_term_from_fixed::<C>(next);
471 if result >= MAX_ITERS_FOR_MONTHS_OF_YEAR || solar_term == next_solar_term {
472 break;
473 }
474 cur = next;
475 solar_term = next_solar_term;
476 result += 1;
477 }
478 debug_assert!(result < MAX_ITERS_FOR_MONTHS_OF_YEAR, "The given year was not a leap year and an unexpected number of iterations occurred searching for a leap month.");
479 result
480}
481
482pub fn month_days<C: ChineseBased>(year: i32, month: u8) -> u8 {
488 let mid_year = fixed_mid_year_from_year::<C>(year);
489 let prev_solstice = winter_solstice_on_or_before::<C>(mid_year);
490 let new_year = new_year_on_or_before_fixed_date::<C>(mid_year, prev_solstice).0;
491 days_in_month::<C>(month, new_year, None).0
492}
493
494pub fn days_in_month<C: ChineseBased>(
497 month: u8,
498 new_year: RataDie,
499 prev_new_moon: Option<RataDie>,
500) -> (u8, RataDie) {
501 let approx = new_year + ((month - 1) as i64 * 29);
502 let prev_new_moon = if let Some(prev_moon) = prev_new_moon {
503 prev_moon
504 } else {
505 new_moon_before::<C>((approx + 15).as_moment())
506 };
507 let next_new_moon = new_moon_on_or_after::<C>((approx + 15).as_moment());
508 let result = (next_new_moon - prev_new_moon) as u8;
509 debug_assert!(
510 result == 29 || result == 30 || !WELL_BEHAVED_ASTRONOMICAL_RANGE.contains(&new_year)
511 );
512 (result, next_new_moon)
513}
514
515pub fn days_in_prev_year<C: ChineseBased>(new_year: RataDie) -> u16 {
517 let date = new_year - 300;
518 let prev_solstice = winter_solstice_on_or_before::<C>(date);
519 let (prev_new_year, _) = new_year_on_or_before_fixed_date::<C>(date, prev_solstice);
520 u16::try_from(new_year - prev_new_year).unwrap_or(360)
521}
522
523pub fn month_structure_for_year<C: ChineseBased>(
528 new_year: RataDie,
529 next_new_year: RataDie,
530) -> ([bool; 13], Option<u8>) {
531 let mut ret = [false; 13];
532
533 let mut current_month_start = new_year;
534 let mut current_month_major_solar_term = major_solar_term_from_fixed::<C>(new_year);
535 let mut leap_month_index = None;
536 for i in 0u8..12 {
537 let next_month_start = new_moon_on_or_after::<C>((current_month_start + 28).as_moment());
538 let next_month_major_solar_term = major_solar_term_from_fixed::<C>(next_month_start);
539
540 if next_month_major_solar_term == current_month_major_solar_term {
541 leap_month_index = Some(i + 1);
542 }
543
544 let diff = next_month_start - current_month_start;
545 debug_assert!(
546 diff == 29 || diff == 30 || !WELL_BEHAVED_ASTRONOMICAL_RANGE.contains(&new_year)
547 );
548 #[expect(clippy::indexing_slicing)] if diff == 30 {
550 ret[usize::from(i)] = true;
551 }
552
553 current_month_start = next_month_start;
554 current_month_major_solar_term = next_month_major_solar_term;
555 }
556
557 if current_month_start == next_new_year {
558 leap_month_index = None;
568 } else {
569 let diff = next_new_year - current_month_start;
570 debug_assert!(
571 diff == 29 || diff == 30 || !WELL_BEHAVED_ASTRONOMICAL_RANGE.contains(&new_year)
572 );
573 if diff == 30 {
574 ret[12] = true;
575 }
576 }
577 if current_month_start != next_new_year && leap_month_index.is_none() {
578 leap_month_index = Some(13); debug_assert!(
580 major_solar_term_from_fixed::<C>(current_month_start) == current_month_major_solar_term
581 || !WELL_BEHAVED_ASTRONOMICAL_RANGE.contains(&new_year),
582 "A leap month is required here, but it had a major solar term!"
583 );
584 }
585
586 (ret, leap_month_index)
587}
588
589pub fn days_until_month<C: ChineseBased>(new_year: RataDie, month: u8) -> u16 {
591 let month_approx = 28_u16.saturating_mul(u16::from(month) - 1);
592
593 let new_moon = new_moon_on_or_after::<C>(new_year.as_moment() + (month_approx as f64));
594 let result = new_moon - new_year;
595 debug_assert!(((u16::MIN as i64)..=(u16::MAX as i64)).contains(&result), "Result {result} from new moon: {new_moon:?} and new year: {new_year:?} should be in range of u16!");
596 result as u16
597}
598
599#[cfg(test)]
600mod test {
601
602 use super::*;
603 use crate::rata_die::Moment;
604
605 #[test]
606 fn check_epochs() {
607 assert_eq!(
608 YearBounds::compute::<Dangi>(Dangi::EPOCH).new_year,
609 Dangi::EPOCH
610 );
611 assert_eq!(
612 YearBounds::compute::<Chinese>(Chinese::EPOCH).new_year,
613 Chinese::EPOCH
614 );
615 }
616
617 #[test]
618 fn test_chinese_new_moon_directionality() {
619 for i in (-1000..1000).step_by(31) {
620 let moment = Moment::new(i as f64);
621 let before = new_moon_before::<Chinese>(moment);
622 let after = new_moon_on_or_after::<Chinese>(moment);
623 assert!(before < after, "Chinese new moon directionality failed for Moment: {moment:?}, with:\n\tBefore: {before:?}\n\tAfter: {after:?}");
624 }
625 }
626
627 #[test]
628 fn test_chinese_new_year_on_or_before() {
629 let fixed = fixed_from_gregorian(2023, 6, 22);
630 let prev_solstice = winter_solstice_on_or_before::<Chinese>(fixed);
631 let result_fixed = new_year_on_or_before_fixed_date::<Chinese>(fixed, prev_solstice).0;
632 let (y, m, d) = gregorian_from_fixed(result_fixed).unwrap();
633 assert_eq!(y, 2023);
634 assert_eq!(m, 1);
635 assert_eq!(d, 22);
636 }
637
638 fn seollal_on_or_before(fixed: RataDie) -> RataDie {
639 let prev_solstice = winter_solstice_on_or_before::<Dangi>(fixed);
640 new_year_on_or_before_fixed_date::<Dangi>(fixed, prev_solstice).0
641 }
642
643 #[test]
644 fn test_month_structure() {
645 for year in 1900..2050 {
647 let fixed = fixed_from_gregorian(year, 1, 1);
648 let chinese_year = chinese_based_date_from_fixed::<Chinese>(fixed);
649 let (month_lengths, leap) = month_structure_for_year::<Chinese>(
650 chinese_year.year_bounds.new_year,
651 chinese_year.year_bounds.next_new_year,
652 );
653
654 for (i, month_is_30) in month_lengths.into_iter().enumerate() {
655 if leap.is_none() && i == 12 {
656 continue;
658 }
659 let month_len = 29 + i32::from(month_is_30);
660 let month_days = month_days::<Chinese>(chinese_year.year, i as u8 + 1);
661 assert_eq!(
662 month_len,
663 i32::from(month_days),
664 "Month length for month {} must be the same",
665 i + 1
666 );
667 }
668 println!(
669 "{year} (chinese {}): {month_lengths:?} {leap:?}",
670 chinese_year.year
671 );
672 }
673 }
674
675 #[test]
676 fn test_seollal() {
677 #[derive(Debug)]
678 struct TestCase {
679 gregorian_year: i32,
680 gregorian_month: u8,
681 gregorian_day: u8,
682 expected_year: i32,
683 expected_month: u8,
684 expected_day: u8,
685 }
686
687 let cases = [
688 TestCase {
689 gregorian_year: 2024,
690 gregorian_month: 6,
691 gregorian_day: 6,
692 expected_year: 2024,
693 expected_month: 2,
694 expected_day: 10,
695 },
696 TestCase {
697 gregorian_year: 2024,
698 gregorian_month: 2,
699 gregorian_day: 9,
700 expected_year: 2023,
701 expected_month: 1,
702 expected_day: 22,
703 },
704 TestCase {
705 gregorian_year: 2023,
706 gregorian_month: 1,
707 gregorian_day: 22,
708 expected_year: 2023,
709 expected_month: 1,
710 expected_day: 22,
711 },
712 TestCase {
713 gregorian_year: 2023,
714 gregorian_month: 1,
715 gregorian_day: 21,
716 expected_year: 2022,
717 expected_month: 2,
718 expected_day: 1,
719 },
720 TestCase {
721 gregorian_year: 2022,
722 gregorian_month: 6,
723 gregorian_day: 6,
724 expected_year: 2022,
725 expected_month: 2,
726 expected_day: 1,
727 },
728 TestCase {
729 gregorian_year: 2021,
730 gregorian_month: 6,
731 gregorian_day: 6,
732 expected_year: 2021,
733 expected_month: 2,
734 expected_day: 12,
735 },
736 TestCase {
737 gregorian_year: 2020,
738 gregorian_month: 6,
739 gregorian_day: 6,
740 expected_year: 2020,
741 expected_month: 1,
742 expected_day: 25,
743 },
744 TestCase {
745 gregorian_year: 2019,
746 gregorian_month: 6,
747 gregorian_day: 6,
748 expected_year: 2019,
749 expected_month: 2,
750 expected_day: 5,
751 },
752 TestCase {
753 gregorian_year: 2018,
754 gregorian_month: 6,
755 gregorian_day: 6,
756 expected_year: 2018,
757 expected_month: 2,
758 expected_day: 16,
759 },
760 TestCase {
761 gregorian_year: 2025,
762 gregorian_month: 6,
763 gregorian_day: 6,
764 expected_year: 2025,
765 expected_month: 1,
766 expected_day: 29,
767 },
768 TestCase {
769 gregorian_year: 2026,
770 gregorian_month: 8,
771 gregorian_day: 8,
772 expected_year: 2026,
773 expected_month: 2,
774 expected_day: 17,
775 },
776 TestCase {
777 gregorian_year: 2027,
778 gregorian_month: 4,
779 gregorian_day: 4,
780 expected_year: 2027,
781 expected_month: 2,
782 expected_day: 7,
783 },
784 TestCase {
785 gregorian_year: 2028,
786 gregorian_month: 9,
787 gregorian_day: 21,
788 expected_year: 2028,
789 expected_month: 1,
790 expected_day: 27,
791 },
792 ];
793
794 for case in cases {
795 let fixed = fixed_from_gregorian(
796 case.gregorian_year,
797 case.gregorian_month,
798 case.gregorian_day,
799 );
800 let seollal = seollal_on_or_before(fixed);
801 let (y, m, d) = gregorian_from_fixed(seollal).unwrap();
802 assert_eq!(
803 y, case.expected_year,
804 "Year check failed for case: {case:?}"
805 );
806 assert_eq!(
807 m, case.expected_month,
808 "Month check failed for case: {case:?}"
809 );
810 assert_eq!(d, case.expected_day, "Day check failed for case: {case:?}");
811 }
812 }
813}
814
815#[test]
816fn test_chinese_leap_months() {
817 let expected = [
818 (1933, 6),
819 (1938, 8),
820 (1984, 11),
821 (2009, 6),
822 (2017, 7),
823 (2028, 6),
824 ];
825
826 for (year, expected_month) in expected {
827 let bounds = YearBounds::compute::<Chinese>(fixed_from_gregorian(year, 6, 1));
828
829 assert!(bounds.is_leap(), "{year} should be a leap year");
830 assert_eq!(
831 expected_month,
832 get_leap_month_from_new_year::<Chinese>(bounds.new_year),
833 "{year} have leap month {expected_month}"
834 );
835 }
836}