1use crate::astronomy::{self, Astronomical, Location, MEAN_SYNODIC_MONTH, MEAN_TROPICAL_YEAR};
2use crate::helpers::i64_to_i32;
3use crate::iso::{fixed_from_iso, iso_from_fixed};
4use crate::rata_die::{Moment, RataDie};
5use core::num::NonZeroU8;
6#[allow(unused_imports)]
7use core_maths::*;
8
9const MAX_ITERS_FOR_MONTHS_OF_YEAR: u8 = 14;
11
12pub trait ChineseBased {
20 fn location(fixed: RataDie) -> Location;
24
25 const EPOCH: RataDie;
29
30 const EPOCH_ISO: i32;
32
33 const DEBUG_NAME: &'static str;
35
36 fn extended_from_iso(iso_year: i32) -> i32 {
38 iso_year - Self::EPOCH_ISO + 1
39 }
40 fn iso_from_extended(extended_year: i32) -> i32 {
42 extended_year - 1 + Self::EPOCH_ISO
43 }
44}
45
46const CHINESE_EPOCH: RataDie = RataDie::new(-963099); const CHINESE_EPOCH_ISO: i32 = -2636;
49
50const UTC_OFFSET_PRE_1929: f64 = (1397.0 / 180.0) / 24.0;
58const UTC_OFFSET_POST_1929: f64 = 8.0 / 24.0;
59
60const CHINESE_LOCATION_PRE_1929: Location =
61 Location::new_unchecked(39.0, 116.0, 43.5, UTC_OFFSET_PRE_1929);
62const CHINESE_LOCATION_POST_1929: Location =
63 Location::new_unchecked(39.0, 116.0, 43.5, UTC_OFFSET_POST_1929);
64
65const KOREAN_EPOCH: RataDie = RataDie::new(-852065); const KOREAN_EPOCH_ISO: i32 = -2332; const UTC_OFFSET_ORIGINAL: f64 = (3809.0 / 450.0) / 24.0;
77const UTC_OFFSET_1908: f64 = 8.5 / 24.0;
78const UTC_OFFSET_1912: f64 = 9.0 / 24.0;
79const UTC_OFFSET_1954: f64 = 8.5 / 24.0;
80const UTC_OFFSET_1961: f64 = 9.0 / 24.0;
81
82const FIXED_1908: RataDie = RataDie::new(696608); const FIXED_1912: RataDie = RataDie::new(697978); const FIXED_1954: RataDie = RataDie::new(713398); const FIXED_1961: RataDie = RataDie::new(716097); const KOREAN_LATITUDE: f64 = 37.0 + (34.0 / 60.0);
88const KOREAN_LONGITUDE: f64 = 126.0 + (58.0 / 60.0);
89const KOREAN_ELEVATION: f64 = 0.0;
90
91const KOREAN_LOCATION_ORIGINAL: Location = Location::new_unchecked(
92 KOREAN_LATITUDE,
93 KOREAN_LONGITUDE,
94 KOREAN_ELEVATION,
95 UTC_OFFSET_ORIGINAL,
96);
97const KOREAN_LOCATION_1908: Location = Location::new_unchecked(
98 KOREAN_LATITUDE,
99 KOREAN_LONGITUDE,
100 KOREAN_ELEVATION,
101 UTC_OFFSET_1908,
102);
103const KOREAN_LOCATION_1912: Location = Location::new_unchecked(
104 KOREAN_LATITUDE,
105 KOREAN_LONGITUDE,
106 KOREAN_ELEVATION,
107 UTC_OFFSET_1912,
108);
109const KOREAN_LOCATION_1954: Location = Location::new_unchecked(
110 KOREAN_LATITUDE,
111 KOREAN_LONGITUDE,
112 KOREAN_ELEVATION,
113 UTC_OFFSET_1954,
114);
115const KOREAN_LOCATION_1961: Location = Location::new_unchecked(
116 KOREAN_LATITUDE,
117 KOREAN_LONGITUDE,
118 KOREAN_ELEVATION,
119 UTC_OFFSET_1961,
120);
121
122#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Default, Hash)]
124#[allow(clippy::exhaustive_structs)] pub struct Chinese;
126
127#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Default, Hash)]
129#[allow(clippy::exhaustive_structs)] pub struct Dangi;
131
132impl ChineseBased for Chinese {
133 fn location(fixed: RataDie) -> Location {
134 let year = crate::iso::iso_year_from_fixed(fixed);
135 if year < 1929 {
136 CHINESE_LOCATION_PRE_1929
137 } else {
138 CHINESE_LOCATION_POST_1929
139 }
140 }
141
142 const EPOCH: RataDie = CHINESE_EPOCH;
143 const EPOCH_ISO: i32 = CHINESE_EPOCH_ISO;
144 const DEBUG_NAME: &'static str = "chinese";
145}
146
147impl ChineseBased for Dangi {
148 fn location(fixed: RataDie) -> Location {
149 if fixed < FIXED_1908 {
150 KOREAN_LOCATION_ORIGINAL
151 } else if fixed < FIXED_1912 {
152 KOREAN_LOCATION_1908
153 } else if fixed < FIXED_1954 {
154 KOREAN_LOCATION_1912
155 } else if fixed < FIXED_1961 {
156 KOREAN_LOCATION_1954
157 } else {
158 KOREAN_LOCATION_1961
159 }
160 }
161
162 const EPOCH: RataDie = KOREAN_EPOCH;
163 const EPOCH_ISO: i32 = KOREAN_EPOCH_ISO;
164 const DEBUG_NAME: &'static str = "dangi";
165}
166
167#[derive(Debug, Copy, Clone)]
169#[allow(clippy::exhaustive_structs)] pub struct YearBounds {
171 pub new_year: RataDie,
173 pub next_new_year: RataDie,
175}
176
177impl YearBounds {
178 #[inline]
183 pub fn compute<C: ChineseBased>(date: RataDie) -> Self {
184 let prev_solstice = winter_solstice_on_or_before::<C>(date);
185 let (new_year, next_solstice) = new_year_on_or_before_fixed_date::<C>(date, prev_solstice);
186 let next_new_year = new_year_on_or_before_fixed_date::<C>(new_year + 400, next_solstice).0;
188
189 Self {
190 new_year,
191 next_new_year,
192 }
193 }
194
195 pub fn count_days(self) -> u16 {
197 let result = self.next_new_year - self.new_year;
198 debug_assert!(
199 ((u16::MIN as i64)..=(u16::MAX as i64)).contains(&result),
200 "Days in year should be in range of u16."
201 );
202 result as u16
203 }
204
205 pub fn is_leap(self) -> bool {
207 let difference = self.next_new_year - self.new_year;
208 difference > 365
209 }
210}
211
212pub(crate) fn major_solar_term_from_fixed<C: ChineseBased>(date: RataDie) -> u32 {
217 let moment: Moment = date.as_moment();
218 let location = C::location(date);
219 let universal: Moment = Location::universal_from_standard(moment, location);
220 let solar_longitude =
221 i64_to_i32(Astronomical::solar_longitude(Astronomical::julian_centuries(universal)) as i64);
222 debug_assert!(
223 solar_longitude.is_ok(),
224 "Solar longitude should be in range of i32"
225 );
226 let s = solar_longitude.unwrap_or_else(|e| e.saturate());
227 let result_signed = (2 + s.div_euclid(30) - 1).rem_euclid(12) + 1;
228 debug_assert!(result_signed >= 0);
229 result_signed as u32
230}
231
232pub(crate) fn new_moon_on_or_after<C: ChineseBased>(moment: Moment) -> RataDie {
237 let new_moon_moment = Astronomical::new_moon_at_or_after(midnight::<C>(moment));
238 let location = C::location(new_moon_moment.as_rata_die());
239 Location::standard_from_universal(new_moon_moment, location).as_rata_die()
240}
241
242pub(crate) fn new_moon_before<C: ChineseBased>(moment: Moment) -> RataDie {
247 let new_moon_moment = Astronomical::new_moon_before(midnight::<C>(moment));
248 let location = C::location(new_moon_moment.as_rata_die());
249 Location::standard_from_universal(new_moon_moment, location).as_rata_die()
250}
251
252pub(crate) fn midnight<C: ChineseBased>(moment: Moment) -> Moment {
257 Location::universal_from_standard(moment, C::location(moment.as_rata_die()))
258}
259
260pub(crate) fn new_year_in_sui<C: ChineseBased>(prior_solstice: RataDie) -> (RataDie, RataDie) {
268 let prior_solstice = bind_winter_solstice::<C>(prior_solstice);
274 let following_solstice =
275 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!(month_after_eleventh - prior_solstice >= 0);
278 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());
280 debug_assert!(month_after_twelfth - month_after_eleventh >= 29);
281 let next_eleventh_month = new_moon_before::<C>((following_solstice + 1).as_moment()); let lhs_argument =
283 ((next_eleventh_month - month_after_eleventh) as f64 / MEAN_SYNODIC_MONTH).round() as i64;
284 let solar_term_a = major_solar_term_from_fixed::<C>(month_after_eleventh);
285 let solar_term_b = major_solar_term_from_fixed::<C>(month_after_twelfth);
286 let solar_term_c = major_solar_term_from_fixed::<C>(month_after_thirteenth);
287 if lhs_argument == 12 && (solar_term_a == solar_term_b || solar_term_b == solar_term_c) {
288 (month_after_thirteenth, following_solstice)
289 } else {
290 (month_after_twelfth, following_solstice)
291 }
292}
293
294fn bind_winter_solstice<C: ChineseBased>(solstice: RataDie) -> RataDie {
299 let (iso_year, iso_month, iso_day) = match iso_from_fixed(solstice) {
300 Ok(ymd) => ymd,
301 Err(_) => {
302 debug_assert!(false, "Solstice REALLY out of bounds: {solstice:?}");
303 return solstice;
304 }
305 };
306 let resolved_solstice = if iso_month < 12 || iso_day < 20 {
307 fixed_from_iso(iso_year, 12, 20)
308 } else if iso_day > 23 {
309 fixed_from_iso(iso_year, 12, 23)
310 } else {
311 solstice
312 };
313 if resolved_solstice != solstice {
314 if !(0..=4000).contains(&iso_year) {
315 #[cfg(feature = "logging")]
316 log::trace!("({}) Solstice out of bounds: {solstice:?}", C::DEBUG_NAME);
317 } else {
318 debug_assert!(
319 false,
320 "({}) Solstice out of bounds: {solstice:?}",
321 C::DEBUG_NAME
322 );
323 }
324 }
325 resolved_solstice
326}
327
328pub(crate) fn winter_solstice_on_or_before<C: ChineseBased>(date: RataDie) -> RataDie {
337 let approx = Astronomical::estimate_prior_solar_longitude(
338 astronomy::WINTER,
339 midnight::<C>((date + 1).as_moment()),
340 );
341 let mut iters = 0;
342 let mut day = Moment::new((approx.inner() - 1.0).floor());
343 while iters < MAX_ITERS_FOR_MONTHS_OF_YEAR
344 && astronomy::WINTER
345 >= Astronomical::solar_longitude(Astronomical::julian_centuries(midnight::<C>(
346 day + 1.0,
347 )))
348 {
349 iters += 1;
350 day += 1.0;
351 }
352 debug_assert!(
353 iters < MAX_ITERS_FOR_MONTHS_OF_YEAR,
354 "Number of iterations was higher than expected"
355 );
356 day.as_rata_die()
357}
358
359pub(crate) fn new_year_on_or_before_fixed_date<C: ChineseBased>(
368 date: RataDie,
369 prior_solstice: RataDie,
370) -> (RataDie, RataDie) {
371 let new_year = new_year_in_sui::<C>(prior_solstice);
372 if date >= new_year.0 {
373 new_year
374 } else {
375 let date_in_last_sui = date - 180; let prior_solstice = winter_solstice_on_or_before::<C>(date_in_last_sui);
381 new_year_in_sui::<C>(prior_solstice)
382 }
383}
384
385pub fn fixed_mid_year_from_year<C: ChineseBased>(elapsed_years: i32) -> RataDie {
394 let cycle = (elapsed_years - 1).div_euclid(60) + 1;
395 let year = (elapsed_years - 1).rem_euclid(60) + 1;
396 C::EPOCH + ((((cycle - 1) * 60 + year - 1) as f64 + 0.5) * MEAN_TROPICAL_YEAR) as i64
397}
398
399pub fn is_leap_year<C: ChineseBased>(year: i32) -> bool {
401 let mid_year = fixed_mid_year_from_year::<C>(year);
402 YearBounds::compute::<C>(mid_year).is_leap()
403}
404
405pub fn last_month_day_in_year<C: ChineseBased>(year: i32) -> (u8, u8) {
407 let mid_year = fixed_mid_year_from_year::<C>(year);
408 let year_bounds = YearBounds::compute::<C>(mid_year);
409 let last_day = year_bounds.next_new_year - 1;
410 let month = if year_bounds.is_leap() { 13 } else { 12 };
411 let day = last_day - new_moon_before::<C>(last_day.as_moment()) + 1;
412 (month, day as u8)
413}
414
415pub fn days_in_provided_year<C: ChineseBased>(year: i32) -> u16 {
417 let mid_year = fixed_mid_year_from_year::<C>(year);
418 let bounds = YearBounds::compute::<C>(mid_year);
419
420 bounds.count_days()
421}
422
423#[derive(Debug)]
425#[non_exhaustive]
426pub struct ChineseFromFixedResult {
427 pub year: i32,
429 pub month: u8,
431 pub day: u8,
433 pub year_bounds: YearBounds,
435 pub leap_month: Option<NonZeroU8>,
437}
438
439pub fn chinese_based_date_from_fixed<C: ChineseBased>(date: RataDie) -> ChineseFromFixedResult {
448 let year_bounds = YearBounds::compute::<C>(date);
449 let first_day_of_year = year_bounds.new_year;
450
451 let year_float =
452 (1.5 - 1.0 / 12.0 + ((first_day_of_year - C::EPOCH) as f64) / MEAN_TROPICAL_YEAR).floor();
453 let year_int = i64_to_i32(year_float as i64);
454 debug_assert!(year_int.is_ok(), "Year should be in range of i32");
455 let year = year_int.unwrap_or_else(|e| e.saturate());
456
457 let new_moon = new_moon_before::<C>((date + 1).as_moment());
458 let month_i64 = ((new_moon - first_day_of_year) as f64 / MEAN_SYNODIC_MONTH).round() as i64 + 1;
459 debug_assert!(
460 ((u8::MIN as i64)..=(u8::MAX as i64)).contains(&month_i64),
461 "Month should be in range of u8! Value {month_i64} failed for RD {date:?}"
462 );
463 let month = month_i64 as u8;
464 let day_i64 = date - new_moon + 1;
465 debug_assert!(
466 ((u8::MIN as i64)..=(u8::MAX as i64)).contains(&month_i64),
467 "Day should be in range of u8! Value {month_i64} failed for RD {date:?}"
468 );
469 let day = day_i64 as u8;
470 let leap_month = if year_bounds.is_leap() {
471 NonZeroU8::new(get_leap_month_from_new_year::<C>(first_day_of_year))
474 } else {
475 None
476 };
477
478 ChineseFromFixedResult {
479 year,
480 month,
481 day,
482 year_bounds,
483 leap_month,
484 }
485}
486
487pub fn get_leap_month_from_new_year<C: ChineseBased>(new_year: RataDie) -> u8 {
499 let mut cur = new_year;
500 let mut result = 1;
501 let mut solar_term = major_solar_term_from_fixed::<C>(cur);
502 loop {
503 let next = new_moon_on_or_after::<C>((cur + 1).as_moment());
504 let next_solar_term = major_solar_term_from_fixed::<C>(next);
505 if result >= MAX_ITERS_FOR_MONTHS_OF_YEAR || solar_term == next_solar_term {
506 break;
507 }
508 cur = next;
509 solar_term = next_solar_term;
510 result += 1;
511 }
512 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.");
513 result
514}
515
516pub fn month_days<C: ChineseBased>(year: i32, month: u8) -> u8 {
522 let mid_year = fixed_mid_year_from_year::<C>(year);
523 let prev_solstice = winter_solstice_on_or_before::<C>(mid_year);
524 let new_year = new_year_on_or_before_fixed_date::<C>(mid_year, prev_solstice).0;
525 days_in_month::<C>(month, new_year, None).0
526}
527
528pub fn days_in_month<C: ChineseBased>(
531 month: u8,
532 new_year: RataDie,
533 prev_new_moon: Option<RataDie>,
534) -> (u8, RataDie) {
535 let approx = new_year + ((month - 1) as i64 * 29);
536 let prev_new_moon = if let Some(prev_moon) = prev_new_moon {
537 prev_moon
538 } else {
539 new_moon_before::<C>((approx + 15).as_moment())
540 };
541 let next_new_moon = new_moon_on_or_after::<C>((approx + 15).as_moment());
542 let result = (next_new_moon - prev_new_moon) as u8;
543 debug_assert!(result == 29 || result == 30);
544 (result, next_new_moon)
545}
546
547pub fn days_in_prev_year<C: ChineseBased>(new_year: RataDie) -> u16 {
549 let date = new_year - 300;
550 let prev_solstice = winter_solstice_on_or_before::<C>(date);
551 let (prev_new_year, _) = new_year_on_or_before_fixed_date::<C>(date, prev_solstice);
552 u16::try_from(new_year - prev_new_year).unwrap_or(360)
553}
554
555pub fn month_structure_for_year<C: ChineseBased>(
560 new_year: RataDie,
561 next_new_year: RataDie,
562) -> ([bool; 13], Option<NonZeroU8>) {
563 let mut ret = [false; 13];
564
565 let mut current_month_start = new_year;
566 let mut current_month_major_solar_term = major_solar_term_from_fixed::<C>(new_year);
567 let mut leap_month_index = None;
568 for i in 0u8..12 {
569 let next_month_start = new_moon_on_or_after::<C>((current_month_start + 28).as_moment());
570 let next_month_major_solar_term = major_solar_term_from_fixed::<C>(next_month_start);
571
572 if next_month_major_solar_term == current_month_major_solar_term {
573 leap_month_index = NonZeroU8::new(i + 1);
574 }
575
576 let diff = next_month_start - current_month_start;
577 debug_assert!(diff == 29 || diff == 30);
578 #[allow(clippy::indexing_slicing)] if diff == 30 {
580 ret[usize::from(i)] = true;
581 }
582
583 current_month_start = next_month_start;
584 current_month_major_solar_term = next_month_major_solar_term;
585 }
586
587 if current_month_start == next_new_year {
588 leap_month_index = None;
598 } else {
599 let diff = next_new_year - current_month_start;
600 debug_assert!(diff == 29 || diff == 30);
601 if diff == 30 {
602 ret[12] = true;
603 }
604 }
605 if current_month_start != next_new_year && leap_month_index.is_none() {
606 leap_month_index = NonZeroU8::new(13); debug_assert!(
608 major_solar_term_from_fixed::<C>(current_month_start) == current_month_major_solar_term,
609 "A leap month is required here, but it had a major solar term!"
610 );
611 }
612
613 (ret, leap_month_index)
614}
615
616pub fn days_until_month<C: ChineseBased>(new_year: RataDie, month: u8) -> u16 {
618 let month_approx = 28_u16.saturating_mul(u16::from(month) - 1);
619
620 let new_moon = new_moon_on_or_after::<C>(new_year.as_moment() + (month_approx as f64));
621 let result = new_moon - new_year;
622 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!");
623 result as u16
624}
625
626#[cfg(test)]
627mod test {
628
629 use super::*;
630 use crate::rata_die::Moment;
631
632 #[test]
633 fn test_chinese_new_moon_directionality() {
634 for i in (-1000..1000).step_by(31) {
635 let moment = Moment::new(i as f64);
636 let before = new_moon_before::<Chinese>(moment);
637 let after = new_moon_on_or_after::<Chinese>(moment);
638 assert!(before < after, "Chinese new moon directionality failed for Moment: {moment:?}, with:\n\tBefore: {before:?}\n\tAfter: {after:?}");
639 }
640 }
641
642 #[test]
643 fn test_chinese_new_year_on_or_before() {
644 let fixed = crate::iso::fixed_from_iso(2023, 6, 22);
645 let prev_solstice = winter_solstice_on_or_before::<Chinese>(fixed);
646 let result_fixed = new_year_on_or_before_fixed_date::<Chinese>(fixed, prev_solstice).0;
647 let (y, m, d) = crate::iso::iso_from_fixed(result_fixed).unwrap();
648 assert_eq!(y, 2023);
649 assert_eq!(m, 1);
650 assert_eq!(d, 22);
651 }
652
653 fn seollal_on_or_before(fixed: RataDie) -> RataDie {
654 let prev_solstice = winter_solstice_on_or_before::<Dangi>(fixed);
655 new_year_on_or_before_fixed_date::<Dangi>(fixed, prev_solstice).0
656 }
657
658 #[test]
659 fn test_month_structure() {
660 for year in 1900..2050 {
662 let fixed = crate::iso::fixed_from_iso(year, 1, 1);
663 let chinese_year = chinese_based_date_from_fixed::<Chinese>(fixed);
664 let (month_lengths, leap) = month_structure_for_year::<Chinese>(
665 chinese_year.year_bounds.new_year,
666 chinese_year.year_bounds.next_new_year,
667 );
668
669 for (i, month_is_30) in month_lengths.into_iter().enumerate() {
670 if leap.is_none() && i == 12 {
671 continue;
673 }
674 let month_len = 29 + i32::from(month_is_30);
675 let month_days = month_days::<Chinese>(chinese_year.year, i as u8 + 1);
676 assert_eq!(
677 month_len,
678 i32::from(month_days),
679 "Month length for month {} must be the same",
680 i + 1
681 );
682 }
683 println!(
684 "{year} (chinese {}): {month_lengths:?} {leap:?}",
685 chinese_year.year
686 );
687 }
688 }
689
690 #[test]
691 fn test_seollal() {
692 #[derive(Debug)]
693 struct TestCase {
694 iso_year: i32,
695 iso_month: u8,
696 iso_day: u8,
697 expected_year: i32,
698 expected_month: u8,
699 expected_day: u8,
700 }
701
702 let cases = [
703 TestCase {
704 iso_year: 2024,
705 iso_month: 6,
706 iso_day: 6,
707 expected_year: 2024,
708 expected_month: 2,
709 expected_day: 10,
710 },
711 TestCase {
712 iso_year: 2024,
713 iso_month: 2,
714 iso_day: 9,
715 expected_year: 2023,
716 expected_month: 1,
717 expected_day: 22,
718 },
719 TestCase {
720 iso_year: 2023,
721 iso_month: 1,
722 iso_day: 22,
723 expected_year: 2023,
724 expected_month: 1,
725 expected_day: 22,
726 },
727 TestCase {
728 iso_year: 2023,
729 iso_month: 1,
730 iso_day: 21,
731 expected_year: 2022,
732 expected_month: 2,
733 expected_day: 1,
734 },
735 TestCase {
736 iso_year: 2022,
737 iso_month: 6,
738 iso_day: 6,
739 expected_year: 2022,
740 expected_month: 2,
741 expected_day: 1,
742 },
743 TestCase {
744 iso_year: 2021,
745 iso_month: 6,
746 iso_day: 6,
747 expected_year: 2021,
748 expected_month: 2,
749 expected_day: 12,
750 },
751 TestCase {
752 iso_year: 2020,
753 iso_month: 6,
754 iso_day: 6,
755 expected_year: 2020,
756 expected_month: 1,
757 expected_day: 25,
758 },
759 TestCase {
760 iso_year: 2019,
761 iso_month: 6,
762 iso_day: 6,
763 expected_year: 2019,
764 expected_month: 2,
765 expected_day: 5,
766 },
767 TestCase {
768 iso_year: 2018,
769 iso_month: 6,
770 iso_day: 6,
771 expected_year: 2018,
772 expected_month: 2,
773 expected_day: 16,
774 },
775 TestCase {
776 iso_year: 2025,
777 iso_month: 6,
778 iso_day: 6,
779 expected_year: 2025,
780 expected_month: 1,
781 expected_day: 29,
782 },
783 TestCase {
784 iso_year: 2026,
785 iso_month: 8,
786 iso_day: 8,
787 expected_year: 2026,
788 expected_month: 2,
789 expected_day: 17,
790 },
791 TestCase {
792 iso_year: 2027,
793 iso_month: 4,
794 iso_day: 4,
795 expected_year: 2027,
796 expected_month: 2,
797 expected_day: 7,
798 },
799 TestCase {
800 iso_year: 2028,
801 iso_month: 9,
802 iso_day: 21,
803 expected_year: 2028,
804 expected_month: 1,
805 expected_day: 27,
806 },
807 ];
808
809 for case in cases {
810 let fixed = crate::iso::fixed_from_iso(case.iso_year, case.iso_month, case.iso_day);
811 let seollal = seollal_on_or_before(fixed);
812 let (y, m, d) = crate::iso::iso_from_fixed(seollal).unwrap();
813 assert_eq!(
814 y, case.expected_year,
815 "Year check failed for case: {case:?}"
816 );
817 assert_eq!(
818 m, case.expected_month,
819 "Month check failed for case: {case:?}"
820 );
821 assert_eq!(d, case.expected_day, "Day check failed for case: {case:?}");
822 }
823 }
824}