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 use crate::gregorian::fixed_from_gregorian as gregorian;
91 if fixed < const { gregorian(1929, 1, 1) } {
95 1397.0 / 180.0 / 24.0
96 } else {
97 8.0 / 24.0
98 }
99 }
100
101 const EPOCH: RataDie = crate::gregorian::fixed_from_gregorian(-2636, 2, 15);
103 const DEBUG_NAME: &'static str = "chinese";
104}
105
106impl ChineseBased for Dangi {
107 fn utc_offset(fixed: RataDie) -> f64 {
108 use crate::gregorian::fixed_from_gregorian as gregorian;
109 if fixed < const { gregorian(1908, 4, 1) } {
113 3809.0 / 450.0 / 24.0
114 } else if fixed < const { gregorian(1912, 1, 1) } {
115 8.5 / 24.0
116 } else if fixed < const { gregorian(1954, 3, 21) } {
117 9.0 / 24.0
118 } else if fixed < const { gregorian(1961, 8, 10) } {
119 8.5 / 24.0
120 } else {
121 9.0 / 24.0
122 }
123 }
124
125 const EPOCH: RataDie = crate::gregorian::fixed_from_gregorian(-2332, 2, 15);
127 const DEBUG_NAME: &'static str = "dangi";
128}
129
130#[derive(Debug, Copy, Clone)]
132#[allow(clippy::exhaustive_structs)] pub struct YearBounds {
134 pub new_year: RataDie,
136 pub next_new_year: RataDie,
138}
139
140impl YearBounds {
141 #[inline]
146 pub fn compute<C: ChineseBased>(date: RataDie) -> Self {
147 let prev_solstice = winter_solstice_on_or_before::<C>(date);
148 let (new_year, next_solstice) = new_year_on_or_before_fixed_date::<C>(date, prev_solstice);
149 let next_new_year = new_year_on_or_before_fixed_date::<C>(new_year + 400, next_solstice).0;
151
152 Self {
153 new_year,
154 next_new_year,
155 }
156 }
157
158 pub fn count_days(self) -> u16 {
160 let result = self.next_new_year - self.new_year;
161 debug_assert!(
162 ((u16::MIN as i64)..=(u16::MAX as i64)).contains(&result),
163 "Days in year should be in range of u16."
164 );
165 result as u16
166 }
167
168 pub fn is_leap(self) -> bool {
170 let difference = self.next_new_year - self.new_year;
171 difference > 365
172 }
173}
174
175pub(crate) fn major_solar_term_from_fixed<C: ChineseBased>(date: RataDie) -> u32 {
180 let moment: Moment = date.as_moment();
181 let universal = moment - C::utc_offset(date);
182 let solar_longitude =
183 i64_to_i32(Astronomical::solar_longitude(Astronomical::julian_centuries(universal)) as i64);
184 debug_assert!(
185 solar_longitude.is_ok(),
186 "Solar longitude should be in range of i32"
187 );
188 let s = solar_longitude.unwrap_or_else(|e| e.saturate());
189 let result_signed = (2 + s.div_euclid(30) - 1).rem_euclid(12) + 1;
190 debug_assert!(result_signed >= 0);
191 result_signed as u32
192}
193
194pub(crate) fn new_moon_on_or_after<C: ChineseBased>(moment: Moment) -> RataDie {
199 let new_moon_moment = Astronomical::new_moon_at_or_after(midnight::<C>(moment));
200 let utc_offset = C::utc_offset(new_moon_moment.as_rata_die());
201 (new_moon_moment + utc_offset).as_rata_die()
202}
203
204pub(crate) fn new_moon_before<C: ChineseBased>(moment: Moment) -> RataDie {
209 let new_moon_moment = Astronomical::new_moon_before(midnight::<C>(moment));
210 let utc_offset = C::utc_offset(new_moon_moment.as_rata_die());
211 (new_moon_moment + utc_offset).as_rata_die()
212}
213
214pub(crate) fn midnight<C: ChineseBased>(moment: Moment) -> Moment {
219 moment - C::utc_offset(moment.as_rata_die())
220}
221
222pub(crate) fn new_year_in_sui<C: ChineseBased>(prior_solstice: RataDie) -> (RataDie, RataDie) {
230 let prior_solstice = bind_winter_solstice::<C>(prior_solstice);
236 let following_solstice =
237 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!(
240 month_after_eleventh - prior_solstice >= 0
241 || !WELL_BEHAVED_ASTRONOMICAL_RANGE.contains(&prior_solstice)
242 );
243 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());
245 debug_assert!(
246 month_after_twelfth - month_after_eleventh >= 29
247 || !WELL_BEHAVED_ASTRONOMICAL_RANGE.contains(&prior_solstice)
248 );
249 let next_eleventh_month = new_moon_before::<C>((following_solstice + 1).as_moment()); let lhs_argument =
251 ((next_eleventh_month - month_after_eleventh) as f64 / MEAN_SYNODIC_MONTH).round() as i64;
252 let solar_term_a = major_solar_term_from_fixed::<C>(month_after_eleventh);
253 let solar_term_b = major_solar_term_from_fixed::<C>(month_after_twelfth);
254 let solar_term_c = major_solar_term_from_fixed::<C>(month_after_thirteenth);
255 if lhs_argument == 12 && (solar_term_a == solar_term_b || solar_term_b == solar_term_c) {
256 (month_after_thirteenth, following_solstice)
257 } else {
258 (month_after_twelfth, following_solstice)
259 }
260}
261
262fn bind_winter_solstice<C: ChineseBased>(solstice: RataDie) -> RataDie {
267 let (gregorian_year, gregorian_month, gregorian_day) = match gregorian_from_fixed(solstice) {
268 Ok(ymd) => ymd,
269 Err(_) => {
270 debug_assert!(false, "Solstice REALLY out of bounds: {solstice:?}");
271 return solstice;
272 }
273 };
274 let resolved_solstice = if gregorian_month < 12 || gregorian_day < 20 {
275 fixed_from_gregorian(gregorian_year, 12, 20)
276 } else if gregorian_day > 23 {
277 fixed_from_gregorian(gregorian_year, 12, 23)
278 } else {
279 solstice
280 };
281 if resolved_solstice != solstice {
282 if !(0..=4000).contains(&gregorian_year) {
283 #[cfg(feature = "logging")]
284 log::trace!("({}) Solstice out of bounds: {solstice:?}", C::DEBUG_NAME);
285 } else {
286 debug_assert!(
287 false,
288 "({}) Solstice out of bounds: {solstice:?}",
289 C::DEBUG_NAME
290 );
291 }
292 }
293 resolved_solstice
294}
295
296pub(crate) fn winter_solstice_on_or_before<C: ChineseBased>(date: RataDie) -> RataDie {
305 let approx = Astronomical::estimate_prior_solar_longitude(
306 astronomy::WINTER,
307 midnight::<C>((date + 1).as_moment()),
308 );
309 let mut iters = 0;
310 let mut day = Moment::new((approx.inner() - 1.0).floor());
311 while iters < MAX_ITERS_FOR_MONTHS_OF_YEAR
312 && astronomy::WINTER
313 >= Astronomical::solar_longitude(Astronomical::julian_centuries(midnight::<C>(
314 day + 1.0,
315 )))
316 {
317 iters += 1;
318 day += 1.0;
319 }
320 debug_assert!(
321 iters < MAX_ITERS_FOR_MONTHS_OF_YEAR || !WELL_BEHAVED_ASTRONOMICAL_RANGE.contains(&date),
322 "Number of iterations was higher than expected"
323 );
324 day.as_rata_die()
325}
326
327pub(crate) fn new_year_on_or_before_fixed_date<C: ChineseBased>(
336 date: RataDie,
337 prior_solstice: RataDie,
338) -> (RataDie, RataDie) {
339 let new_year = new_year_in_sui::<C>(prior_solstice);
340 if date >= new_year.0 {
341 new_year
342 } else {
343 let date_in_last_sui = date - 180; let prior_solstice = winter_solstice_on_or_before::<C>(date_in_last_sui);
349 new_year_in_sui::<C>(prior_solstice)
350 }
351}
352
353pub fn fixed_mid_year_from_year<C: ChineseBased>(elapsed_years: i32) -> RataDie {
362 let cycle = (elapsed_years - 1).div_euclid(60) + 1;
363 let year = (elapsed_years - 1).rem_euclid(60) + 1;
364 C::EPOCH + ((((cycle - 1) * 60 + year - 1) as f64 + 0.5) * MEAN_TROPICAL_YEAR) as i64
365}
366
367pub fn is_leap_year<C: ChineseBased>(year: i32) -> bool {
369 let mid_year = fixed_mid_year_from_year::<C>(year);
370 YearBounds::compute::<C>(mid_year).is_leap()
371}
372
373pub fn last_month_day_in_year<C: ChineseBased>(year: i32) -> (u8, u8) {
375 let mid_year = fixed_mid_year_from_year::<C>(year);
376 let year_bounds = YearBounds::compute::<C>(mid_year);
377 let last_day = year_bounds.next_new_year - 1;
378 let month = if year_bounds.is_leap() { 13 } else { 12 };
379 let day = last_day - new_moon_before::<C>(last_day.as_moment()) + 1;
380 (month, day as u8)
381}
382
383pub fn days_in_provided_year<C: ChineseBased>(year: i32) -> u16 {
385 let mid_year = fixed_mid_year_from_year::<C>(year);
386 let bounds = YearBounds::compute::<C>(mid_year);
387
388 bounds.count_days()
389}
390
391#[derive(Debug)]
393#[non_exhaustive]
394pub struct ChineseFromFixedResult {
395 pub year: i32,
397 pub month: u8,
399 pub day: u8,
401 pub year_bounds: YearBounds,
403 pub leap_month: Option<NonZeroU8>,
405}
406
407pub fn chinese_based_date_from_fixed<C: ChineseBased>(date: RataDie) -> ChineseFromFixedResult {
416 let year_bounds = YearBounds::compute::<C>(date);
417 let first_day_of_year = year_bounds.new_year;
418
419 let year_float =
420 (1.5 - 1.0 / 12.0 + ((first_day_of_year - C::EPOCH) as f64) / MEAN_TROPICAL_YEAR).floor();
421 let year_int = i64_to_i32(year_float as i64);
422 debug_assert!(year_int.is_ok(), "Year should be in range of i32");
423 let year = year_int.unwrap_or_else(|e| e.saturate());
424
425 let new_moon = new_moon_before::<C>((date + 1).as_moment());
426 let month_i64 = ((new_moon - first_day_of_year) as f64 / MEAN_SYNODIC_MONTH).round() as i64 + 1;
427 debug_assert!(
428 ((u8::MIN as i64)..=(u8::MAX as i64)).contains(&month_i64),
429 "Month should be in range of u8! Value {month_i64} failed for RD {date:?}"
430 );
431 let month = month_i64 as u8;
432 let day_i64 = date - new_moon + 1;
433 debug_assert!(
434 ((u8::MIN as i64)..=(u8::MAX as i64)).contains(&month_i64),
435 "Day should be in range of u8! Value {month_i64} failed for RD {date:?}"
436 );
437 let day = day_i64 as u8;
438 let leap_month = if year_bounds.is_leap() {
439 NonZeroU8::new(get_leap_month_from_new_year::<C>(first_day_of_year))
442 } else {
443 None
444 };
445
446 ChineseFromFixedResult {
447 year,
448 month,
449 day,
450 year_bounds,
451 leap_month,
452 }
453}
454
455pub fn get_leap_month_from_new_year<C: ChineseBased>(new_year: RataDie) -> u8 {
467 let mut cur = new_year;
468 let mut result = 1;
469 let mut solar_term = major_solar_term_from_fixed::<C>(cur);
470 loop {
471 let next = new_moon_on_or_after::<C>((cur + 1).as_moment());
472 let next_solar_term = major_solar_term_from_fixed::<C>(next);
473 if result >= MAX_ITERS_FOR_MONTHS_OF_YEAR || solar_term == next_solar_term {
474 break;
475 }
476 cur = next;
477 solar_term = next_solar_term;
478 result += 1;
479 }
480 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.");
481 result
482}
483
484pub fn month_days<C: ChineseBased>(year: i32, month: u8) -> u8 {
490 let mid_year = fixed_mid_year_from_year::<C>(year);
491 let prev_solstice = winter_solstice_on_or_before::<C>(mid_year);
492 let new_year = new_year_on_or_before_fixed_date::<C>(mid_year, prev_solstice).0;
493 days_in_month::<C>(month, new_year, None).0
494}
495
496pub fn days_in_month<C: ChineseBased>(
499 month: u8,
500 new_year: RataDie,
501 prev_new_moon: Option<RataDie>,
502) -> (u8, RataDie) {
503 let approx = new_year + ((month - 1) as i64 * 29);
504 let prev_new_moon = if let Some(prev_moon) = prev_new_moon {
505 prev_moon
506 } else {
507 new_moon_before::<C>((approx + 15).as_moment())
508 };
509 let next_new_moon = new_moon_on_or_after::<C>((approx + 15).as_moment());
510 let result = (next_new_moon - prev_new_moon) as u8;
511 debug_assert!(
512 result == 29 || result == 30 || !WELL_BEHAVED_ASTRONOMICAL_RANGE.contains(&new_year)
513 );
514 (result, next_new_moon)
515}
516
517pub fn days_in_prev_year<C: ChineseBased>(new_year: RataDie) -> u16 {
519 let date = new_year - 300;
520 let prev_solstice = winter_solstice_on_or_before::<C>(date);
521 let (prev_new_year, _) = new_year_on_or_before_fixed_date::<C>(date, prev_solstice);
522 u16::try_from(new_year - prev_new_year).unwrap_or(360)
523}
524
525pub fn month_structure_for_year<C: ChineseBased>(
530 new_year: RataDie,
531 next_new_year: RataDie,
532) -> ([bool; 13], Option<u8>) {
533 let mut ret = [false; 13];
534
535 let mut current_month_start = new_year;
536 let mut current_month_major_solar_term = major_solar_term_from_fixed::<C>(new_year);
537 let mut leap_month_index = None;
538 for i in 0u8..12 {
539 let next_month_start = new_moon_on_or_after::<C>((current_month_start + 28).as_moment());
540 let next_month_major_solar_term = major_solar_term_from_fixed::<C>(next_month_start);
541
542 if next_month_major_solar_term == current_month_major_solar_term {
543 leap_month_index = Some(i + 1);
544 }
545
546 let diff = next_month_start - current_month_start;
547 debug_assert!(
548 diff == 29 || diff == 30 || !WELL_BEHAVED_ASTRONOMICAL_RANGE.contains(&new_year)
549 );
550 #[expect(clippy::indexing_slicing)] if diff == 30 {
552 ret[usize::from(i)] = true;
553 }
554
555 current_month_start = next_month_start;
556 current_month_major_solar_term = next_month_major_solar_term;
557 }
558
559 if current_month_start == next_new_year {
560 leap_month_index = None;
570 } else {
571 let diff = next_new_year - current_month_start;
572 debug_assert!(
573 diff == 29 || diff == 30 || !WELL_BEHAVED_ASTRONOMICAL_RANGE.contains(&new_year)
574 );
575 if diff == 30 {
576 ret[12] = true;
577 }
578 }
579 if current_month_start != next_new_year && leap_month_index.is_none() {
580 leap_month_index = Some(13); debug_assert!(
582 major_solar_term_from_fixed::<C>(current_month_start) == current_month_major_solar_term
583 || !WELL_BEHAVED_ASTRONOMICAL_RANGE.contains(&new_year),
584 "A leap month is required here, but it had a major solar term!"
585 );
586 }
587
588 (ret, leap_month_index)
589}
590
591pub fn days_until_month<C: ChineseBased>(new_year: RataDie, month: u8) -> u16 {
593 let month_approx = 28_u16.saturating_mul(u16::from(month) - 1);
594
595 let new_moon = new_moon_on_or_after::<C>(new_year.as_moment() + (month_approx as f64));
596 let result = new_moon - new_year;
597 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!");
598 result as u16
599}
600
601#[cfg(test)]
602mod test {
603
604 use super::*;
605 use crate::rata_die::Moment;
606
607 #[test]
608 fn check_epochs() {
609 assert_eq!(
610 YearBounds::compute::<Dangi>(Dangi::EPOCH).new_year,
611 Dangi::EPOCH
612 );
613 assert_eq!(
614 YearBounds::compute::<Chinese>(Chinese::EPOCH).new_year,
615 Chinese::EPOCH
616 );
617 }
618
619 #[test]
620 fn test_chinese_new_moon_directionality() {
621 for i in (-1000..1000).step_by(31) {
622 let moment = Moment::new(i as f64);
623 let before = new_moon_before::<Chinese>(moment);
624 let after = new_moon_on_or_after::<Chinese>(moment);
625 assert!(before < after, "Chinese new moon directionality failed for Moment: {moment:?}, with:\n\tBefore: {before:?}\n\tAfter: {after:?}");
626 }
627 }
628
629 #[test]
630 fn test_chinese_new_year_on_or_before() {
631 let fixed = crate::gregorian::fixed_from_gregorian(2023, 6, 22);
632 let prev_solstice = winter_solstice_on_or_before::<Chinese>(fixed);
633 let result_fixed = new_year_on_or_before_fixed_date::<Chinese>(fixed, prev_solstice).0;
634 let (y, m, d) = crate::gregorian::gregorian_from_fixed(result_fixed).unwrap();
635 assert_eq!(y, 2023);
636 assert_eq!(m, 1);
637 assert_eq!(d, 22);
638 }
639
640 fn seollal_on_or_before(fixed: RataDie) -> RataDie {
641 let prev_solstice = winter_solstice_on_or_before::<Dangi>(fixed);
642 new_year_on_or_before_fixed_date::<Dangi>(fixed, prev_solstice).0
643 }
644
645 #[test]
646 fn test_month_structure() {
647 for year in 1900..2050 {
649 let fixed = crate::gregorian::fixed_from_gregorian(year, 1, 1);
650 let chinese_year = chinese_based_date_from_fixed::<Chinese>(fixed);
651 let (month_lengths, leap) = month_structure_for_year::<Chinese>(
652 chinese_year.year_bounds.new_year,
653 chinese_year.year_bounds.next_new_year,
654 );
655
656 for (i, month_is_30) in month_lengths.into_iter().enumerate() {
657 if leap.is_none() && i == 12 {
658 continue;
660 }
661 let month_len = 29 + i32::from(month_is_30);
662 let month_days = month_days::<Chinese>(chinese_year.year, i as u8 + 1);
663 assert_eq!(
664 month_len,
665 i32::from(month_days),
666 "Month length for month {} must be the same",
667 i + 1
668 );
669 }
670 println!(
671 "{year} (chinese {}): {month_lengths:?} {leap:?}",
672 chinese_year.year
673 );
674 }
675 }
676
677 #[test]
678 fn test_seollal() {
679 #[derive(Debug)]
680 struct TestCase {
681 gregorian_year: i32,
682 gregorian_month: u8,
683 gregorian_day: u8,
684 expected_year: i32,
685 expected_month: u8,
686 expected_day: u8,
687 }
688
689 let cases = [
690 TestCase {
691 gregorian_year: 2024,
692 gregorian_month: 6,
693 gregorian_day: 6,
694 expected_year: 2024,
695 expected_month: 2,
696 expected_day: 10,
697 },
698 TestCase {
699 gregorian_year: 2024,
700 gregorian_month: 2,
701 gregorian_day: 9,
702 expected_year: 2023,
703 expected_month: 1,
704 expected_day: 22,
705 },
706 TestCase {
707 gregorian_year: 2023,
708 gregorian_month: 1,
709 gregorian_day: 22,
710 expected_year: 2023,
711 expected_month: 1,
712 expected_day: 22,
713 },
714 TestCase {
715 gregorian_year: 2023,
716 gregorian_month: 1,
717 gregorian_day: 21,
718 expected_year: 2022,
719 expected_month: 2,
720 expected_day: 1,
721 },
722 TestCase {
723 gregorian_year: 2022,
724 gregorian_month: 6,
725 gregorian_day: 6,
726 expected_year: 2022,
727 expected_month: 2,
728 expected_day: 1,
729 },
730 TestCase {
731 gregorian_year: 2021,
732 gregorian_month: 6,
733 gregorian_day: 6,
734 expected_year: 2021,
735 expected_month: 2,
736 expected_day: 12,
737 },
738 TestCase {
739 gregorian_year: 2020,
740 gregorian_month: 6,
741 gregorian_day: 6,
742 expected_year: 2020,
743 expected_month: 1,
744 expected_day: 25,
745 },
746 TestCase {
747 gregorian_year: 2019,
748 gregorian_month: 6,
749 gregorian_day: 6,
750 expected_year: 2019,
751 expected_month: 2,
752 expected_day: 5,
753 },
754 TestCase {
755 gregorian_year: 2018,
756 gregorian_month: 6,
757 gregorian_day: 6,
758 expected_year: 2018,
759 expected_month: 2,
760 expected_day: 16,
761 },
762 TestCase {
763 gregorian_year: 2025,
764 gregorian_month: 6,
765 gregorian_day: 6,
766 expected_year: 2025,
767 expected_month: 1,
768 expected_day: 29,
769 },
770 TestCase {
771 gregorian_year: 2026,
772 gregorian_month: 8,
773 gregorian_day: 8,
774 expected_year: 2026,
775 expected_month: 2,
776 expected_day: 17,
777 },
778 TestCase {
779 gregorian_year: 2027,
780 gregorian_month: 4,
781 gregorian_day: 4,
782 expected_year: 2027,
783 expected_month: 2,
784 expected_day: 7,
785 },
786 TestCase {
787 gregorian_year: 2028,
788 gregorian_month: 9,
789 gregorian_day: 21,
790 expected_year: 2028,
791 expected_month: 1,
792 expected_day: 27,
793 },
794 ];
795
796 for case in cases {
797 let fixed = crate::gregorian::fixed_from_gregorian(
798 case.gregorian_year,
799 case.gregorian_month,
800 case.gregorian_day,
801 );
802 let seollal = seollal_on_or_before(fixed);
803 let (y, m, d) = crate::gregorian::gregorian_from_fixed(seollal).unwrap();
804 assert_eq!(
805 y, case.expected_year,
806 "Year check failed for case: {case:?}"
807 );
808 assert_eq!(
809 m, case.expected_month,
810 "Month check failed for case: {case:?}"
811 );
812 assert_eq!(d, case.expected_day, "Day check failed for case: {case:?}");
813 }
814 }
815}