1use crate::astronomy::{self, Astronomical, 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 utc_offset(fixed: RataDie) -> f64;
24
25 const EPOCH: RataDie;
29
30 const DEBUG_NAME: &'static str;
32}
33
34pub fn extended_from_iso<C: ChineseBased>(iso_year: i32) -> i32 {
36 iso_year
37 - const {
38 let Ok(y) = crate::iso::iso_year_from_fixed(C::EPOCH) else {
39 panic!()
40 };
41 y - 1
42 }
43}
44pub fn iso_from_extended<C: ChineseBased>(extended_year: i32) -> i32 {
46 extended_year
47 + const {
48 let Ok(y) = crate::iso::iso_year_from_fixed(C::EPOCH) else {
49 panic!()
50 };
51 y - 1
52 }
53}
54
55#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Default, Hash)]
57#[allow(clippy::exhaustive_structs)] pub struct Chinese;
59
60#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Default, Hash)]
62#[allow(clippy::exhaustive_structs)] pub struct Dangi;
64
65impl ChineseBased for Chinese {
66 fn utc_offset(fixed: RataDie) -> f64 {
67 use crate::iso::const_fixed_from_iso as iso;
68 if fixed < const { iso(1929, 1, 1) } {
72 1397.0 / 180.0 / 24.0
73 } else {
74 8.0 / 24.0
75 }
76 }
77
78 const EPOCH: RataDie = crate::iso::const_fixed_from_iso(-2636, 2, 15);
80 const DEBUG_NAME: &'static str = "chinese";
81}
82
83impl ChineseBased for Dangi {
84 fn utc_offset(fixed: RataDie) -> f64 {
85 use crate::iso::const_fixed_from_iso as iso;
86 if fixed < const { iso(1908, 4, 1) } {
90 3809.0 / 450.0 / 24.0
91 } else if fixed < const { iso(1912, 1, 1) } {
92 8.5 / 24.0
93 } else if fixed < const { iso(1954, 3, 21) } {
94 9.0 / 24.0
95 } else if fixed < const { iso(1961, 8, 10) } {
96 8.5 / 24.0
97 } else {
98 9.0 / 24.0
99 }
100 }
101
102 const EPOCH: RataDie = crate::iso::const_fixed_from_iso(-2332, 2, 15);
104 const DEBUG_NAME: &'static str = "dangi";
105}
106
107#[derive(Debug, Copy, Clone)]
109#[allow(clippy::exhaustive_structs)] pub struct YearBounds {
111 pub new_year: RataDie,
113 pub next_new_year: RataDie,
115}
116
117impl YearBounds {
118 #[inline]
123 pub fn compute<C: ChineseBased>(date: RataDie) -> Self {
124 let prev_solstice = winter_solstice_on_or_before::<C>(date);
125 let (new_year, next_solstice) = new_year_on_or_before_fixed_date::<C>(date, prev_solstice);
126 let next_new_year = new_year_on_or_before_fixed_date::<C>(new_year + 400, next_solstice).0;
128
129 Self {
130 new_year,
131 next_new_year,
132 }
133 }
134
135 pub fn count_days(self) -> u16 {
137 let result = self.next_new_year - self.new_year;
138 debug_assert!(
139 ((u16::MIN as i64)..=(u16::MAX as i64)).contains(&result),
140 "Days in year should be in range of u16."
141 );
142 result as u16
143 }
144
145 pub fn is_leap(self) -> bool {
147 let difference = self.next_new_year - self.new_year;
148 difference > 365
149 }
150}
151
152pub(crate) fn major_solar_term_from_fixed<C: ChineseBased>(date: RataDie) -> u32 {
157 let moment: Moment = date.as_moment();
158 let universal = moment - C::utc_offset(date);
159 let solar_longitude =
160 i64_to_i32(Astronomical::solar_longitude(Astronomical::julian_centuries(universal)) as i64);
161 debug_assert!(
162 solar_longitude.is_ok(),
163 "Solar longitude should be in range of i32"
164 );
165 let s = solar_longitude.unwrap_or_else(|e| e.saturate());
166 let result_signed = (2 + s.div_euclid(30) - 1).rem_euclid(12) + 1;
167 debug_assert!(result_signed >= 0);
168 result_signed as u32
169}
170
171pub(crate) fn new_moon_on_or_after<C: ChineseBased>(moment: Moment) -> RataDie {
176 let new_moon_moment = Astronomical::new_moon_at_or_after(midnight::<C>(moment));
177 let utc_offset = C::utc_offset(new_moon_moment.as_rata_die());
178 (new_moon_moment + utc_offset).as_rata_die()
179}
180
181pub(crate) fn new_moon_before<C: ChineseBased>(moment: Moment) -> RataDie {
186 let new_moon_moment = Astronomical::new_moon_before(midnight::<C>(moment));
187 let utc_offset = C::utc_offset(new_moon_moment.as_rata_die());
188 (new_moon_moment + utc_offset).as_rata_die()
189}
190
191pub(crate) fn midnight<C: ChineseBased>(moment: Moment) -> Moment {
196 moment - C::utc_offset(moment.as_rata_die())
197}
198
199pub(crate) fn new_year_in_sui<C: ChineseBased>(prior_solstice: RataDie) -> (RataDie, RataDie) {
207 let prior_solstice = bind_winter_solstice::<C>(prior_solstice);
213 let following_solstice =
214 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);
217 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());
219 debug_assert!(month_after_twelfth - month_after_eleventh >= 29);
220 let next_eleventh_month = new_moon_before::<C>((following_solstice + 1).as_moment()); let lhs_argument =
222 ((next_eleventh_month - month_after_eleventh) as f64 / MEAN_SYNODIC_MONTH).round() as i64;
223 let solar_term_a = major_solar_term_from_fixed::<C>(month_after_eleventh);
224 let solar_term_b = major_solar_term_from_fixed::<C>(month_after_twelfth);
225 let solar_term_c = major_solar_term_from_fixed::<C>(month_after_thirteenth);
226 if lhs_argument == 12 && (solar_term_a == solar_term_b || solar_term_b == solar_term_c) {
227 (month_after_thirteenth, following_solstice)
228 } else {
229 (month_after_twelfth, following_solstice)
230 }
231}
232
233fn bind_winter_solstice<C: ChineseBased>(solstice: RataDie) -> RataDie {
238 let (iso_year, iso_month, iso_day) = match iso_from_fixed(solstice) {
239 Ok(ymd) => ymd,
240 Err(_) => {
241 debug_assert!(false, "Solstice REALLY out of bounds: {solstice:?}");
242 return solstice;
243 }
244 };
245 let resolved_solstice = if iso_month < 12 || iso_day < 20 {
246 fixed_from_iso(iso_year, 12, 20)
247 } else if iso_day > 23 {
248 fixed_from_iso(iso_year, 12, 23)
249 } else {
250 solstice
251 };
252 if resolved_solstice != solstice {
253 if !(0..=4000).contains(&iso_year) {
254 #[cfg(feature = "logging")]
255 log::trace!("({}) Solstice out of bounds: {solstice:?}", C::DEBUG_NAME);
256 } else {
257 debug_assert!(
258 false,
259 "({}) Solstice out of bounds: {solstice:?}",
260 C::DEBUG_NAME
261 );
262 }
263 }
264 resolved_solstice
265}
266
267pub(crate) fn winter_solstice_on_or_before<C: ChineseBased>(date: RataDie) -> RataDie {
276 let approx = Astronomical::estimate_prior_solar_longitude(
277 astronomy::WINTER,
278 midnight::<C>((date + 1).as_moment()),
279 );
280 let mut iters = 0;
281 let mut day = Moment::new((approx.inner() - 1.0).floor());
282 while iters < MAX_ITERS_FOR_MONTHS_OF_YEAR
283 && astronomy::WINTER
284 >= Astronomical::solar_longitude(Astronomical::julian_centuries(midnight::<C>(
285 day + 1.0,
286 )))
287 {
288 iters += 1;
289 day += 1.0;
290 }
291 debug_assert!(
292 iters < MAX_ITERS_FOR_MONTHS_OF_YEAR,
293 "Number of iterations was higher than expected"
294 );
295 day.as_rata_die()
296}
297
298pub(crate) fn new_year_on_or_before_fixed_date<C: ChineseBased>(
307 date: RataDie,
308 prior_solstice: RataDie,
309) -> (RataDie, RataDie) {
310 let new_year = new_year_in_sui::<C>(prior_solstice);
311 if date >= new_year.0 {
312 new_year
313 } else {
314 let date_in_last_sui = date - 180; let prior_solstice = winter_solstice_on_or_before::<C>(date_in_last_sui);
320 new_year_in_sui::<C>(prior_solstice)
321 }
322}
323
324pub fn fixed_mid_year_from_year<C: ChineseBased>(elapsed_years: i32) -> RataDie {
333 let cycle = (elapsed_years - 1).div_euclid(60) + 1;
334 let year = (elapsed_years - 1).rem_euclid(60) + 1;
335 C::EPOCH + ((((cycle - 1) * 60 + year - 1) as f64 + 0.5) * MEAN_TROPICAL_YEAR) as i64
336}
337
338pub fn is_leap_year<C: ChineseBased>(year: i32) -> bool {
340 let mid_year = fixed_mid_year_from_year::<C>(year);
341 YearBounds::compute::<C>(mid_year).is_leap()
342}
343
344pub fn last_month_day_in_year<C: ChineseBased>(year: i32) -> (u8, u8) {
346 let mid_year = fixed_mid_year_from_year::<C>(year);
347 let year_bounds = YearBounds::compute::<C>(mid_year);
348 let last_day = year_bounds.next_new_year - 1;
349 let month = if year_bounds.is_leap() { 13 } else { 12 };
350 let day = last_day - new_moon_before::<C>(last_day.as_moment()) + 1;
351 (month, day as u8)
352}
353
354pub fn days_in_provided_year<C: ChineseBased>(year: i32) -> u16 {
356 let mid_year = fixed_mid_year_from_year::<C>(year);
357 let bounds = YearBounds::compute::<C>(mid_year);
358
359 bounds.count_days()
360}
361
362#[derive(Debug)]
364#[non_exhaustive]
365pub struct ChineseFromFixedResult {
366 pub year: i32,
368 pub month: u8,
370 pub day: u8,
372 pub year_bounds: YearBounds,
374 pub leap_month: Option<NonZeroU8>,
376}
377
378pub fn chinese_based_date_from_fixed<C: ChineseBased>(date: RataDie) -> ChineseFromFixedResult {
387 let year_bounds = YearBounds::compute::<C>(date);
388 let first_day_of_year = year_bounds.new_year;
389
390 let year_float =
391 (1.5 - 1.0 / 12.0 + ((first_day_of_year - C::EPOCH) as f64) / MEAN_TROPICAL_YEAR).floor();
392 let year_int = i64_to_i32(year_float as i64);
393 debug_assert!(year_int.is_ok(), "Year should be in range of i32");
394 let year = year_int.unwrap_or_else(|e| e.saturate());
395
396 let new_moon = new_moon_before::<C>((date + 1).as_moment());
397 let month_i64 = ((new_moon - first_day_of_year) as f64 / MEAN_SYNODIC_MONTH).round() as i64 + 1;
398 debug_assert!(
399 ((u8::MIN as i64)..=(u8::MAX as i64)).contains(&month_i64),
400 "Month should be in range of u8! Value {month_i64} failed for RD {date:?}"
401 );
402 let month = month_i64 as u8;
403 let day_i64 = date - new_moon + 1;
404 debug_assert!(
405 ((u8::MIN as i64)..=(u8::MAX as i64)).contains(&month_i64),
406 "Day should be in range of u8! Value {month_i64} failed for RD {date:?}"
407 );
408 let day = day_i64 as u8;
409 let leap_month = if year_bounds.is_leap() {
410 NonZeroU8::new(get_leap_month_from_new_year::<C>(first_day_of_year))
413 } else {
414 None
415 };
416
417 ChineseFromFixedResult {
418 year,
419 month,
420 day,
421 year_bounds,
422 leap_month,
423 }
424}
425
426pub fn get_leap_month_from_new_year<C: ChineseBased>(new_year: RataDie) -> u8 {
438 let mut cur = new_year;
439 let mut result = 1;
440 let mut solar_term = major_solar_term_from_fixed::<C>(cur);
441 loop {
442 let next = new_moon_on_or_after::<C>((cur + 1).as_moment());
443 let next_solar_term = major_solar_term_from_fixed::<C>(next);
444 if result >= MAX_ITERS_FOR_MONTHS_OF_YEAR || solar_term == next_solar_term {
445 break;
446 }
447 cur = next;
448 solar_term = next_solar_term;
449 result += 1;
450 }
451 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.");
452 result
453}
454
455pub fn month_days<C: ChineseBased>(year: i32, month: u8) -> u8 {
461 let mid_year = fixed_mid_year_from_year::<C>(year);
462 let prev_solstice = winter_solstice_on_or_before::<C>(mid_year);
463 let new_year = new_year_on_or_before_fixed_date::<C>(mid_year, prev_solstice).0;
464 days_in_month::<C>(month, new_year, None).0
465}
466
467pub fn days_in_month<C: ChineseBased>(
470 month: u8,
471 new_year: RataDie,
472 prev_new_moon: Option<RataDie>,
473) -> (u8, RataDie) {
474 let approx = new_year + ((month - 1) as i64 * 29);
475 let prev_new_moon = if let Some(prev_moon) = prev_new_moon {
476 prev_moon
477 } else {
478 new_moon_before::<C>((approx + 15).as_moment())
479 };
480 let next_new_moon = new_moon_on_or_after::<C>((approx + 15).as_moment());
481 let result = (next_new_moon - prev_new_moon) as u8;
482 debug_assert!(result == 29 || result == 30);
483 (result, next_new_moon)
484}
485
486pub fn days_in_prev_year<C: ChineseBased>(new_year: RataDie) -> u16 {
488 let date = new_year - 300;
489 let prev_solstice = winter_solstice_on_or_before::<C>(date);
490 let (prev_new_year, _) = new_year_on_or_before_fixed_date::<C>(date, prev_solstice);
491 u16::try_from(new_year - prev_new_year).unwrap_or(360)
492}
493
494pub fn month_structure_for_year<C: ChineseBased>(
499 new_year: RataDie,
500 next_new_year: RataDie,
501) -> ([bool; 13], Option<u8>) {
502 let mut ret = [false; 13];
503
504 let mut current_month_start = new_year;
505 let mut current_month_major_solar_term = major_solar_term_from_fixed::<C>(new_year);
506 let mut leap_month_index = None;
507 for i in 0u8..12 {
508 let next_month_start = new_moon_on_or_after::<C>((current_month_start + 28).as_moment());
509 let next_month_major_solar_term = major_solar_term_from_fixed::<C>(next_month_start);
510
511 if next_month_major_solar_term == current_month_major_solar_term {
512 leap_month_index = Some(i + 1);
513 }
514
515 let diff = next_month_start - current_month_start;
516 debug_assert!(diff == 29 || diff == 30);
517 #[expect(clippy::indexing_slicing)] if diff == 30 {
519 ret[usize::from(i)] = true;
520 }
521
522 current_month_start = next_month_start;
523 current_month_major_solar_term = next_month_major_solar_term;
524 }
525
526 if current_month_start == next_new_year {
527 leap_month_index = None;
537 } else {
538 let diff = next_new_year - current_month_start;
539 debug_assert!(diff == 29 || diff == 30);
540 if diff == 30 {
541 ret[12] = true;
542 }
543 }
544 if current_month_start != next_new_year && leap_month_index.is_none() {
545 leap_month_index = Some(13); debug_assert!(
547 major_solar_term_from_fixed::<C>(current_month_start) == current_month_major_solar_term,
548 "A leap month is required here, but it had a major solar term!"
549 );
550 }
551
552 (ret, leap_month_index)
553}
554
555pub fn days_until_month<C: ChineseBased>(new_year: RataDie, month: u8) -> u16 {
557 let month_approx = 28_u16.saturating_mul(u16::from(month) - 1);
558
559 let new_moon = new_moon_on_or_after::<C>(new_year.as_moment() + (month_approx as f64));
560 let result = new_moon - new_year;
561 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!");
562 result as u16
563}
564
565#[cfg(test)]
566mod test {
567
568 use super::*;
569 use crate::rata_die::Moment;
570
571 #[test]
572 fn check_epochs() {
573 assert_eq!(
574 YearBounds::compute::<Dangi>(Dangi::EPOCH).new_year,
575 Dangi::EPOCH
576 );
577 assert_eq!(
578 YearBounds::compute::<Chinese>(Chinese::EPOCH).new_year,
579 Chinese::EPOCH
580 );
581 }
582
583 #[test]
584 fn test_chinese_new_moon_directionality() {
585 for i in (-1000..1000).step_by(31) {
586 let moment = Moment::new(i as f64);
587 let before = new_moon_before::<Chinese>(moment);
588 let after = new_moon_on_or_after::<Chinese>(moment);
589 assert!(before < after, "Chinese new moon directionality failed for Moment: {moment:?}, with:\n\tBefore: {before:?}\n\tAfter: {after:?}");
590 }
591 }
592
593 #[test]
594 fn test_chinese_new_year_on_or_before() {
595 let fixed = crate::iso::fixed_from_iso(2023, 6, 22);
596 let prev_solstice = winter_solstice_on_or_before::<Chinese>(fixed);
597 let result_fixed = new_year_on_or_before_fixed_date::<Chinese>(fixed, prev_solstice).0;
598 let (y, m, d) = crate::iso::iso_from_fixed(result_fixed).unwrap();
599 assert_eq!(y, 2023);
600 assert_eq!(m, 1);
601 assert_eq!(d, 22);
602 }
603
604 fn seollal_on_or_before(fixed: RataDie) -> RataDie {
605 let prev_solstice = winter_solstice_on_or_before::<Dangi>(fixed);
606 new_year_on_or_before_fixed_date::<Dangi>(fixed, prev_solstice).0
607 }
608
609 #[test]
610 fn test_month_structure() {
611 for year in 1900..2050 {
613 let fixed = crate::iso::fixed_from_iso(year, 1, 1);
614 let chinese_year = chinese_based_date_from_fixed::<Chinese>(fixed);
615 let (month_lengths, leap) = month_structure_for_year::<Chinese>(
616 chinese_year.year_bounds.new_year,
617 chinese_year.year_bounds.next_new_year,
618 );
619
620 for (i, month_is_30) in month_lengths.into_iter().enumerate() {
621 if leap.is_none() && i == 12 {
622 continue;
624 }
625 let month_len = 29 + i32::from(month_is_30);
626 let month_days = month_days::<Chinese>(chinese_year.year, i as u8 + 1);
627 assert_eq!(
628 month_len,
629 i32::from(month_days),
630 "Month length for month {} must be the same",
631 i + 1
632 );
633 }
634 println!(
635 "{year} (chinese {}): {month_lengths:?} {leap:?}",
636 chinese_year.year
637 );
638 }
639 }
640
641 #[test]
642 fn test_seollal() {
643 #[derive(Debug)]
644 struct TestCase {
645 iso_year: i32,
646 iso_month: u8,
647 iso_day: u8,
648 expected_year: i32,
649 expected_month: u8,
650 expected_day: u8,
651 }
652
653 let cases = [
654 TestCase {
655 iso_year: 2024,
656 iso_month: 6,
657 iso_day: 6,
658 expected_year: 2024,
659 expected_month: 2,
660 expected_day: 10,
661 },
662 TestCase {
663 iso_year: 2024,
664 iso_month: 2,
665 iso_day: 9,
666 expected_year: 2023,
667 expected_month: 1,
668 expected_day: 22,
669 },
670 TestCase {
671 iso_year: 2023,
672 iso_month: 1,
673 iso_day: 22,
674 expected_year: 2023,
675 expected_month: 1,
676 expected_day: 22,
677 },
678 TestCase {
679 iso_year: 2023,
680 iso_month: 1,
681 iso_day: 21,
682 expected_year: 2022,
683 expected_month: 2,
684 expected_day: 1,
685 },
686 TestCase {
687 iso_year: 2022,
688 iso_month: 6,
689 iso_day: 6,
690 expected_year: 2022,
691 expected_month: 2,
692 expected_day: 1,
693 },
694 TestCase {
695 iso_year: 2021,
696 iso_month: 6,
697 iso_day: 6,
698 expected_year: 2021,
699 expected_month: 2,
700 expected_day: 12,
701 },
702 TestCase {
703 iso_year: 2020,
704 iso_month: 6,
705 iso_day: 6,
706 expected_year: 2020,
707 expected_month: 1,
708 expected_day: 25,
709 },
710 TestCase {
711 iso_year: 2019,
712 iso_month: 6,
713 iso_day: 6,
714 expected_year: 2019,
715 expected_month: 2,
716 expected_day: 5,
717 },
718 TestCase {
719 iso_year: 2018,
720 iso_month: 6,
721 iso_day: 6,
722 expected_year: 2018,
723 expected_month: 2,
724 expected_day: 16,
725 },
726 TestCase {
727 iso_year: 2025,
728 iso_month: 6,
729 iso_day: 6,
730 expected_year: 2025,
731 expected_month: 1,
732 expected_day: 29,
733 },
734 TestCase {
735 iso_year: 2026,
736 iso_month: 8,
737 iso_day: 8,
738 expected_year: 2026,
739 expected_month: 2,
740 expected_day: 17,
741 },
742 TestCase {
743 iso_year: 2027,
744 iso_month: 4,
745 iso_day: 4,
746 expected_year: 2027,
747 expected_month: 2,
748 expected_day: 7,
749 },
750 TestCase {
751 iso_year: 2028,
752 iso_month: 9,
753 iso_day: 21,
754 expected_year: 2028,
755 expected_month: 1,
756 expected_day: 27,
757 },
758 ];
759
760 for case in cases {
761 let fixed = crate::iso::fixed_from_iso(case.iso_year, case.iso_month, case.iso_day);
762 let seollal = seollal_on_or_before(fixed);
763 let (y, m, d) = crate::iso::iso_from_fixed(seollal).unwrap();
764 assert_eq!(
765 y, case.expected_year,
766 "Year check failed for case: {case:?}"
767 );
768 assert_eq!(
769 m, case.expected_month,
770 "Month check failed for case: {case:?}"
771 );
772 assert_eq!(d, case.expected_day, "Day check failed for case: {case:?}");
773 }
774 }
775}