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 - const { crate::iso::iso_year_from_fixed(C::EPOCH) as i32 - 1 }
37}
38pub fn iso_from_extended<C: ChineseBased>(extended_year: i32) -> i32 {
40 extended_year + const { crate::iso::iso_year_from_fixed(C::EPOCH) as i32 - 1 }
41}
42
43#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Default, Hash)]
45#[allow(clippy::exhaustive_structs)] pub struct Chinese;
47
48#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Default, Hash)]
50#[allow(clippy::exhaustive_structs)] pub struct Dangi;
52
53impl ChineseBased for Chinese {
54 fn utc_offset(fixed: RataDie) -> f64 {
55 use crate::iso::const_fixed_from_iso as iso;
56 if fixed < const { iso(1929, 1, 1) } {
60 1397.0 / 180.0 / 24.0
61 } else {
62 8.0 / 24.0
63 }
64 }
65
66 const EPOCH: RataDie = crate::iso::const_fixed_from_iso(-2636, 2, 15);
68 const DEBUG_NAME: &'static str = "chinese";
69}
70
71impl ChineseBased for Dangi {
72 fn utc_offset(fixed: RataDie) -> f64 {
73 use crate::iso::const_fixed_from_iso as iso;
74 if fixed < const { iso(1908, 4, 1) } {
78 3809.0 / 450.0 / 24.0
79 } else if fixed < const { iso(1912, 1, 1) } {
80 8.5 / 24.0
81 } else if fixed < const { iso(1954, 3, 21) } {
82 9.0 / 24.0
83 } else if fixed < const { iso(1961, 8, 10) } {
84 8.5 / 24.0
85 } else {
86 9.0 / 24.0
87 }
88 }
89
90 const EPOCH: RataDie = crate::iso::const_fixed_from_iso(-2332, 2, 15);
92 const DEBUG_NAME: &'static str = "dangi";
93}
94
95#[derive(Debug, Copy, Clone)]
97#[allow(clippy::exhaustive_structs)] pub struct YearBounds {
99 pub new_year: RataDie,
101 pub next_new_year: RataDie,
103}
104
105impl YearBounds {
106 #[inline]
111 pub fn compute<C: ChineseBased>(date: RataDie) -> Self {
112 let prev_solstice = winter_solstice_on_or_before::<C>(date);
113 let (new_year, next_solstice) = new_year_on_or_before_fixed_date::<C>(date, prev_solstice);
114 let next_new_year = new_year_on_or_before_fixed_date::<C>(new_year + 400, next_solstice).0;
116
117 Self {
118 new_year,
119 next_new_year,
120 }
121 }
122
123 pub fn count_days(self) -> u16 {
125 let result = self.next_new_year - self.new_year;
126 debug_assert!(
127 ((u16::MIN as i64)..=(u16::MAX as i64)).contains(&result),
128 "Days in year should be in range of u16."
129 );
130 result as u16
131 }
132
133 pub fn is_leap(self) -> bool {
135 let difference = self.next_new_year - self.new_year;
136 difference > 365
137 }
138}
139
140pub(crate) fn major_solar_term_from_fixed<C: ChineseBased>(date: RataDie) -> u32 {
145 let moment: Moment = date.as_moment();
146 let universal = moment - C::utc_offset(date);
147 let solar_longitude =
148 i64_to_i32(Astronomical::solar_longitude(Astronomical::julian_centuries(universal)) as i64);
149 debug_assert!(
150 solar_longitude.is_ok(),
151 "Solar longitude should be in range of i32"
152 );
153 let s = solar_longitude.unwrap_or_else(|e| e.saturate());
154 let result_signed = (2 + s.div_euclid(30) - 1).rem_euclid(12) + 1;
155 debug_assert!(result_signed >= 0);
156 result_signed as u32
157}
158
159pub(crate) fn new_moon_on_or_after<C: ChineseBased>(moment: Moment) -> RataDie {
164 let new_moon_moment = Astronomical::new_moon_at_or_after(midnight::<C>(moment));
165 let utc_offset = C::utc_offset(new_moon_moment.as_rata_die());
166 (new_moon_moment + utc_offset).as_rata_die()
167}
168
169pub(crate) fn new_moon_before<C: ChineseBased>(moment: Moment) -> RataDie {
174 let new_moon_moment = Astronomical::new_moon_before(midnight::<C>(moment));
175 let utc_offset = C::utc_offset(new_moon_moment.as_rata_die());
176 (new_moon_moment + utc_offset).as_rata_die()
177}
178
179pub(crate) fn midnight<C: ChineseBased>(moment: Moment) -> Moment {
184 moment - C::utc_offset(moment.as_rata_die())
185}
186
187pub(crate) fn new_year_in_sui<C: ChineseBased>(prior_solstice: RataDie) -> (RataDie, RataDie) {
195 let prior_solstice = bind_winter_solstice::<C>(prior_solstice);
201 let following_solstice =
202 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);
205 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());
207 debug_assert!(month_after_twelfth - month_after_eleventh >= 29);
208 let next_eleventh_month = new_moon_before::<C>((following_solstice + 1).as_moment()); let lhs_argument =
210 ((next_eleventh_month - month_after_eleventh) as f64 / MEAN_SYNODIC_MONTH).round() as i64;
211 let solar_term_a = major_solar_term_from_fixed::<C>(month_after_eleventh);
212 let solar_term_b = major_solar_term_from_fixed::<C>(month_after_twelfth);
213 let solar_term_c = major_solar_term_from_fixed::<C>(month_after_thirteenth);
214 if lhs_argument == 12 && (solar_term_a == solar_term_b || solar_term_b == solar_term_c) {
215 (month_after_thirteenth, following_solstice)
216 } else {
217 (month_after_twelfth, following_solstice)
218 }
219}
220
221fn bind_winter_solstice<C: ChineseBased>(solstice: RataDie) -> RataDie {
226 let (iso_year, iso_month, iso_day) = match iso_from_fixed(solstice) {
227 Ok(ymd) => ymd,
228 Err(_) => {
229 debug_assert!(false, "Solstice REALLY out of bounds: {solstice:?}");
230 return solstice;
231 }
232 };
233 let resolved_solstice = if iso_month < 12 || iso_day < 20 {
234 fixed_from_iso(iso_year, 12, 20)
235 } else if iso_day > 23 {
236 fixed_from_iso(iso_year, 12, 23)
237 } else {
238 solstice
239 };
240 if resolved_solstice != solstice {
241 if !(0..=4000).contains(&iso_year) {
242 #[cfg(feature = "logging")]
243 log::trace!("({}) Solstice out of bounds: {solstice:?}", C::DEBUG_NAME);
244 } else {
245 debug_assert!(
246 false,
247 "({}) Solstice out of bounds: {solstice:?}",
248 C::DEBUG_NAME
249 );
250 }
251 }
252 resolved_solstice
253}
254
255pub(crate) fn winter_solstice_on_or_before<C: ChineseBased>(date: RataDie) -> RataDie {
264 let approx = Astronomical::estimate_prior_solar_longitude(
265 astronomy::WINTER,
266 midnight::<C>((date + 1).as_moment()),
267 );
268 let mut iters = 0;
269 let mut day = Moment::new((approx.inner() - 1.0).floor());
270 while iters < MAX_ITERS_FOR_MONTHS_OF_YEAR
271 && astronomy::WINTER
272 >= Astronomical::solar_longitude(Astronomical::julian_centuries(midnight::<C>(
273 day + 1.0,
274 )))
275 {
276 iters += 1;
277 day += 1.0;
278 }
279 debug_assert!(
280 iters < MAX_ITERS_FOR_MONTHS_OF_YEAR,
281 "Number of iterations was higher than expected"
282 );
283 day.as_rata_die()
284}
285
286pub(crate) fn new_year_on_or_before_fixed_date<C: ChineseBased>(
295 date: RataDie,
296 prior_solstice: RataDie,
297) -> (RataDie, RataDie) {
298 let new_year = new_year_in_sui::<C>(prior_solstice);
299 if date >= new_year.0 {
300 new_year
301 } else {
302 let date_in_last_sui = date - 180; let prior_solstice = winter_solstice_on_or_before::<C>(date_in_last_sui);
308 new_year_in_sui::<C>(prior_solstice)
309 }
310}
311
312pub fn fixed_mid_year_from_year<C: ChineseBased>(elapsed_years: i32) -> RataDie {
321 let cycle = (elapsed_years - 1).div_euclid(60) + 1;
322 let year = (elapsed_years - 1).rem_euclid(60) + 1;
323 C::EPOCH + ((((cycle - 1) * 60 + year - 1) as f64 + 0.5) * MEAN_TROPICAL_YEAR) as i64
324}
325
326pub fn is_leap_year<C: ChineseBased>(year: i32) -> bool {
328 let mid_year = fixed_mid_year_from_year::<C>(year);
329 YearBounds::compute::<C>(mid_year).is_leap()
330}
331
332pub fn last_month_day_in_year<C: ChineseBased>(year: i32) -> (u8, u8) {
334 let mid_year = fixed_mid_year_from_year::<C>(year);
335 let year_bounds = YearBounds::compute::<C>(mid_year);
336 let last_day = year_bounds.next_new_year - 1;
337 let month = if year_bounds.is_leap() { 13 } else { 12 };
338 let day = last_day - new_moon_before::<C>(last_day.as_moment()) + 1;
339 (month, day as u8)
340}
341
342pub fn days_in_provided_year<C: ChineseBased>(year: i32) -> u16 {
344 let mid_year = fixed_mid_year_from_year::<C>(year);
345 let bounds = YearBounds::compute::<C>(mid_year);
346
347 bounds.count_days()
348}
349
350#[derive(Debug)]
352#[non_exhaustive]
353pub struct ChineseFromFixedResult {
354 pub year: i32,
356 pub month: u8,
358 pub day: u8,
360 pub year_bounds: YearBounds,
362 pub leap_month: Option<NonZeroU8>,
364}
365
366pub fn chinese_based_date_from_fixed<C: ChineseBased>(date: RataDie) -> ChineseFromFixedResult {
375 let year_bounds = YearBounds::compute::<C>(date);
376 let first_day_of_year = year_bounds.new_year;
377
378 let year_float =
379 (1.5 - 1.0 / 12.0 + ((first_day_of_year - C::EPOCH) as f64) / MEAN_TROPICAL_YEAR).floor();
380 let year_int = i64_to_i32(year_float as i64);
381 debug_assert!(year_int.is_ok(), "Year should be in range of i32");
382 let year = year_int.unwrap_or_else(|e| e.saturate());
383
384 let new_moon = new_moon_before::<C>((date + 1).as_moment());
385 let month_i64 = ((new_moon - first_day_of_year) as f64 / MEAN_SYNODIC_MONTH).round() as i64 + 1;
386 debug_assert!(
387 ((u8::MIN as i64)..=(u8::MAX as i64)).contains(&month_i64),
388 "Month should be in range of u8! Value {month_i64} failed for RD {date:?}"
389 );
390 let month = month_i64 as u8;
391 let day_i64 = date - new_moon + 1;
392 debug_assert!(
393 ((u8::MIN as i64)..=(u8::MAX as i64)).contains(&month_i64),
394 "Day should be in range of u8! Value {month_i64} failed for RD {date:?}"
395 );
396 let day = day_i64 as u8;
397 let leap_month = if year_bounds.is_leap() {
398 NonZeroU8::new(get_leap_month_from_new_year::<C>(first_day_of_year))
401 } else {
402 None
403 };
404
405 ChineseFromFixedResult {
406 year,
407 month,
408 day,
409 year_bounds,
410 leap_month,
411 }
412}
413
414pub fn get_leap_month_from_new_year<C: ChineseBased>(new_year: RataDie) -> u8 {
426 let mut cur = new_year;
427 let mut result = 1;
428 let mut solar_term = major_solar_term_from_fixed::<C>(cur);
429 loop {
430 let next = new_moon_on_or_after::<C>((cur + 1).as_moment());
431 let next_solar_term = major_solar_term_from_fixed::<C>(next);
432 if result >= MAX_ITERS_FOR_MONTHS_OF_YEAR || solar_term == next_solar_term {
433 break;
434 }
435 cur = next;
436 solar_term = next_solar_term;
437 result += 1;
438 }
439 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.");
440 result
441}
442
443pub fn month_days<C: ChineseBased>(year: i32, month: u8) -> u8 {
449 let mid_year = fixed_mid_year_from_year::<C>(year);
450 let prev_solstice = winter_solstice_on_or_before::<C>(mid_year);
451 let new_year = new_year_on_or_before_fixed_date::<C>(mid_year, prev_solstice).0;
452 days_in_month::<C>(month, new_year, None).0
453}
454
455pub fn days_in_month<C: ChineseBased>(
458 month: u8,
459 new_year: RataDie,
460 prev_new_moon: Option<RataDie>,
461) -> (u8, RataDie) {
462 let approx = new_year + ((month - 1) as i64 * 29);
463 let prev_new_moon = if let Some(prev_moon) = prev_new_moon {
464 prev_moon
465 } else {
466 new_moon_before::<C>((approx + 15).as_moment())
467 };
468 let next_new_moon = new_moon_on_or_after::<C>((approx + 15).as_moment());
469 let result = (next_new_moon - prev_new_moon) as u8;
470 debug_assert!(result == 29 || result == 30);
471 (result, next_new_moon)
472}
473
474pub fn days_in_prev_year<C: ChineseBased>(new_year: RataDie) -> u16 {
476 let date = new_year - 300;
477 let prev_solstice = winter_solstice_on_or_before::<C>(date);
478 let (prev_new_year, _) = new_year_on_or_before_fixed_date::<C>(date, prev_solstice);
479 u16::try_from(new_year - prev_new_year).unwrap_or(360)
480}
481
482pub fn month_structure_for_year<C: ChineseBased>(
487 new_year: RataDie,
488 next_new_year: RataDie,
489) -> ([bool; 13], Option<u8>) {
490 let mut ret = [false; 13];
491
492 let mut current_month_start = new_year;
493 let mut current_month_major_solar_term = major_solar_term_from_fixed::<C>(new_year);
494 let mut leap_month_index = None;
495 for i in 0u8..12 {
496 let next_month_start = new_moon_on_or_after::<C>((current_month_start + 28).as_moment());
497 let next_month_major_solar_term = major_solar_term_from_fixed::<C>(next_month_start);
498
499 if next_month_major_solar_term == current_month_major_solar_term {
500 leap_month_index = Some(i + 1);
501 }
502
503 let diff = next_month_start - current_month_start;
504 debug_assert!(diff == 29 || diff == 30);
505 #[allow(clippy::indexing_slicing)] if diff == 30 {
507 ret[usize::from(i)] = true;
508 }
509
510 current_month_start = next_month_start;
511 current_month_major_solar_term = next_month_major_solar_term;
512 }
513
514 if current_month_start == next_new_year {
515 leap_month_index = None;
525 } else {
526 let diff = next_new_year - current_month_start;
527 debug_assert!(diff == 29 || diff == 30);
528 if diff == 30 {
529 ret[12] = true;
530 }
531 }
532 if current_month_start != next_new_year && leap_month_index.is_none() {
533 leap_month_index = Some(13); debug_assert!(
535 major_solar_term_from_fixed::<C>(current_month_start) == current_month_major_solar_term,
536 "A leap month is required here, but it had a major solar term!"
537 );
538 }
539
540 (ret, leap_month_index)
541}
542
543pub fn days_until_month<C: ChineseBased>(new_year: RataDie, month: u8) -> u16 {
545 let month_approx = 28_u16.saturating_mul(u16::from(month) - 1);
546
547 let new_moon = new_moon_on_or_after::<C>(new_year.as_moment() + (month_approx as f64));
548 let result = new_moon - new_year;
549 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!");
550 result as u16
551}
552
553#[cfg(test)]
554mod test {
555
556 use super::*;
557 use crate::rata_die::Moment;
558
559 #[test]
560 fn check_epochs() {
561 assert_eq!(
562 YearBounds::compute::<Dangi>(Dangi::EPOCH).new_year,
563 Dangi::EPOCH
564 );
565 assert_eq!(
566 YearBounds::compute::<Chinese>(Chinese::EPOCH).new_year,
567 Chinese::EPOCH
568 );
569 }
570
571 #[test]
572 fn test_chinese_new_moon_directionality() {
573 for i in (-1000..1000).step_by(31) {
574 let moment = Moment::new(i as f64);
575 let before = new_moon_before::<Chinese>(moment);
576 let after = new_moon_on_or_after::<Chinese>(moment);
577 assert!(before < after, "Chinese new moon directionality failed for Moment: {moment:?}, with:\n\tBefore: {before:?}\n\tAfter: {after:?}");
578 }
579 }
580
581 #[test]
582 fn test_chinese_new_year_on_or_before() {
583 let fixed = crate::iso::fixed_from_iso(2023, 6, 22);
584 let prev_solstice = winter_solstice_on_or_before::<Chinese>(fixed);
585 let result_fixed = new_year_on_or_before_fixed_date::<Chinese>(fixed, prev_solstice).0;
586 let (y, m, d) = crate::iso::iso_from_fixed(result_fixed).unwrap();
587 assert_eq!(y, 2023);
588 assert_eq!(m, 1);
589 assert_eq!(d, 22);
590 }
591
592 fn seollal_on_or_before(fixed: RataDie) -> RataDie {
593 let prev_solstice = winter_solstice_on_or_before::<Dangi>(fixed);
594 new_year_on_or_before_fixed_date::<Dangi>(fixed, prev_solstice).0
595 }
596
597 #[test]
598 fn test_month_structure() {
599 for year in 1900..2050 {
601 let fixed = crate::iso::fixed_from_iso(year, 1, 1);
602 let chinese_year = chinese_based_date_from_fixed::<Chinese>(fixed);
603 let (month_lengths, leap) = month_structure_for_year::<Chinese>(
604 chinese_year.year_bounds.new_year,
605 chinese_year.year_bounds.next_new_year,
606 );
607
608 for (i, month_is_30) in month_lengths.into_iter().enumerate() {
609 if leap.is_none() && i == 12 {
610 continue;
612 }
613 let month_len = 29 + i32::from(month_is_30);
614 let month_days = month_days::<Chinese>(chinese_year.year, i as u8 + 1);
615 assert_eq!(
616 month_len,
617 i32::from(month_days),
618 "Month length for month {} must be the same",
619 i + 1
620 );
621 }
622 println!(
623 "{year} (chinese {}): {month_lengths:?} {leap:?}",
624 chinese_year.year
625 );
626 }
627 }
628
629 #[test]
630 fn test_seollal() {
631 #[derive(Debug)]
632 struct TestCase {
633 iso_year: i32,
634 iso_month: u8,
635 iso_day: u8,
636 expected_year: i32,
637 expected_month: u8,
638 expected_day: u8,
639 }
640
641 let cases = [
642 TestCase {
643 iso_year: 2024,
644 iso_month: 6,
645 iso_day: 6,
646 expected_year: 2024,
647 expected_month: 2,
648 expected_day: 10,
649 },
650 TestCase {
651 iso_year: 2024,
652 iso_month: 2,
653 iso_day: 9,
654 expected_year: 2023,
655 expected_month: 1,
656 expected_day: 22,
657 },
658 TestCase {
659 iso_year: 2023,
660 iso_month: 1,
661 iso_day: 22,
662 expected_year: 2023,
663 expected_month: 1,
664 expected_day: 22,
665 },
666 TestCase {
667 iso_year: 2023,
668 iso_month: 1,
669 iso_day: 21,
670 expected_year: 2022,
671 expected_month: 2,
672 expected_day: 1,
673 },
674 TestCase {
675 iso_year: 2022,
676 iso_month: 6,
677 iso_day: 6,
678 expected_year: 2022,
679 expected_month: 2,
680 expected_day: 1,
681 },
682 TestCase {
683 iso_year: 2021,
684 iso_month: 6,
685 iso_day: 6,
686 expected_year: 2021,
687 expected_month: 2,
688 expected_day: 12,
689 },
690 TestCase {
691 iso_year: 2020,
692 iso_month: 6,
693 iso_day: 6,
694 expected_year: 2020,
695 expected_month: 1,
696 expected_day: 25,
697 },
698 TestCase {
699 iso_year: 2019,
700 iso_month: 6,
701 iso_day: 6,
702 expected_year: 2019,
703 expected_month: 2,
704 expected_day: 5,
705 },
706 TestCase {
707 iso_year: 2018,
708 iso_month: 6,
709 iso_day: 6,
710 expected_year: 2018,
711 expected_month: 2,
712 expected_day: 16,
713 },
714 TestCase {
715 iso_year: 2025,
716 iso_month: 6,
717 iso_day: 6,
718 expected_year: 2025,
719 expected_month: 1,
720 expected_day: 29,
721 },
722 TestCase {
723 iso_year: 2026,
724 iso_month: 8,
725 iso_day: 8,
726 expected_year: 2026,
727 expected_month: 2,
728 expected_day: 17,
729 },
730 TestCase {
731 iso_year: 2027,
732 iso_month: 4,
733 iso_day: 4,
734 expected_year: 2027,
735 expected_month: 2,
736 expected_day: 7,
737 },
738 TestCase {
739 iso_year: 2028,
740 iso_month: 9,
741 iso_day: 21,
742 expected_year: 2028,
743 expected_month: 1,
744 expected_day: 27,
745 },
746 ];
747
748 for case in cases {
749 let fixed = crate::iso::fixed_from_iso(case.iso_year, case.iso_month, case.iso_day);
750 let seollal = seollal_on_or_before(fixed);
751 let (y, m, d) = crate::iso::iso_from_fixed(seollal).unwrap();
752 assert_eq!(
753 y, case.expected_year,
754 "Year check failed for case: {case:?}"
755 );
756 assert_eq!(
757 m, case.expected_month,
758 "Month check failed for case: {case:?}"
759 );
760 assert_eq!(d, case.expected_day, "Day check failed for case: {case:?}");
761 }
762 }
763}