calendrical_calculations/
chinese_based.rs

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
9// Don't iterate more than 14 times (which accounts for checking for 13 months)
10const MAX_ITERS_FOR_MONTHS_OF_YEAR: u8 = 14;
11
12/// The trait ChineseBased is used by Chinese-based calendars to perform computations shared by such calendar.
13/// To do so, calendars should:
14///
15/// - Implement `fn location` by providing a location at which observations of the moon are recorded, which
16///   may change over time (the zone is important, long, lat, and elevation are not relevant for these calculations)
17/// - Define `const EPOCH` as a `RataDie` marking the start date of the era of the Calendar for internal use,
18///   which may not accurately reflect how years or eras are marked traditionally or seen by end-users
19pub trait ChineseBased {
20    /// Given a fixed date, return the UTC offset used for observations of the new moon in order to
21    /// calculate the beginning of months. For multiple Chinese-based lunar calendars, this has
22    /// changed over the years, and can cause differences in calendar date.
23    fn utc_offset(fixed: RataDie) -> f64;
24
25    /// The RataDie of the beginning of the epoch used for internal computation; this may not
26    /// reflect traditional methods of year-tracking or eras, since Chinese-based calendars
27    /// may not track years ordinally in the same way many western calendars do.
28    const EPOCH: RataDie;
29
30    /// The name of the calendar for debugging.
31    const DEBUG_NAME: &'static str;
32}
33
34/// Given an ISO year, return the extended year
35pub 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}
44/// Given an extended year, return the ISO year
45pub 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/// A type implementing [`ChineseBased`] for the Chinese calendar
56#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Default, Hash)]
57#[allow(clippy::exhaustive_structs)] // newtype
58pub struct Chinese;
59
60/// A type implementing [`ChineseBased`] for the Dangi (Korean) calendar
61#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Default, Hash)]
62#[allow(clippy::exhaustive_structs)] // newtype
63pub 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        // Before 1929, local time was used, represented as UTC+(1397/180 h).
69        // In 1929, China adopted a standard time zone based on 120 degrees of longitude, meaning
70        // from 1929 onward, all new moon calculations are based on UTC+8h.
71        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    /// The equivalent first day in the Chinese calendar (based on inception of the calendar), Feb. 15, -2636
79    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        // Before 1908, local time was used, represented as UTC+(3809/450 h).
87        // This changed multiple times as different standard timezones were adopted in Korea.
88        // Currently, UTC+9h is used.
89        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    /// The first day in the Korean Dangi calendar (based on the founding of Gojoseon), lunar new year -2332
103    const EPOCH: RataDie = crate::iso::const_fixed_from_iso(-2332, 2, 15);
104    const DEBUG_NAME: &'static str = "dangi";
105}
106
107/// Marks the bounds of a lunar year
108#[derive(Debug, Copy, Clone)]
109#[allow(clippy::exhaustive_structs)] // we're comfortable making frequent breaking changes to this crate
110pub struct YearBounds {
111    /// The date marking the start of the current lunar year
112    pub new_year: RataDie,
113    /// The date marking the start of the next lunar year
114    pub next_new_year: RataDie,
115}
116
117impl YearBounds {
118    /// Compute the YearBounds for the lunar year (年) containing `date`,
119    /// as well as the corresponding solar year (歲). Note that since the two
120    /// years overlap significantly but not entirely, the solstice bounds for the solar
121    /// year *may* not include `date`.
122    #[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        // Using 400 here since new years can be up to 390 days apart, and we add some padding
127        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    /// The number of days in this year
136    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    /// Whether or not this is a leap year
146    pub fn is_leap(self) -> bool {
147        let difference = self.next_new_year - self.new_year;
148        difference > 365
149    }
150}
151
152/// Get the current major solar term of a fixed date, output as an integer from 1..=12.
153///
154/// Based on functions from _Calendrical Calculations_ by Reingold & Dershowitz.
155/// Lisp reference code: https://github.com/EdReingold/calendar-code2/blob/main/calendar.l#L5273-L5281
156pub(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
171/// The fixed date in standard time at the observation location of the next new moon on or after a given Moment.
172///
173/// Based on functions from _Calendrical Calculations_ by Reingold & Dershowitz.
174/// Lisp reference code: https://github.com/EdReingold/calendar-code2/blob/main/calendar.l#L5329-L5338
175pub(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
181/// The fixed date in standard time at the observation location of the previous new moon before a given Moment.
182///
183/// Based on functions from _Calendrical Calculations_ by Reingold & Dershowitz.
184/// Lisp reference code: https://github.com/EdReingold/calendar-code2/blob/main/calendar.l#L5318-L5327
185pub(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
191/// Universal time of midnight at start of a Moment's day at the observation location
192///
193/// Based on functions from _Calendrical Calculations_ by Reingold & Dershowitz.
194/// Lisp reference code: https://github.com/EdReingold/calendar-code2/blob/main/calendar.l#L5353-L5357
195pub(crate) fn midnight<C: ChineseBased>(moment: Moment) -> Moment {
196    moment - C::utc_offset(moment.as_rata_die())
197}
198
199/// Determines the fixed date of the lunar new year given the start of its corresponding solar year (歲), which is
200/// also the winter solstice
201///
202/// Calls to `no_major_solar_term` have been inlined for increased efficiency.
203///
204/// Based on functions from _Calendrical Calculations_ by Reingold & Dershowitz.
205/// Lisp reference code: https://github.com/EdReingold/calendar-code2/blob/main/calendar.l#L5370-L5394
206pub(crate) fn new_year_in_sui<C: ChineseBased>(prior_solstice: RataDie) -> (RataDie, RataDie) {
207    // s1 is prior_solstice
208    // Using 370 here since solstices are ~365 days apart
209    // Both solstices should fall on December 20, 21, 22, or 23. The calendrical calculations
210    // drift away from this for large positive and negative years, so we artifically bind them
211    // to this range in order for other code invariants to be upheld.
212    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)); // s2
215    let month_after_eleventh = new_moon_on_or_after::<C>((prior_solstice + 1).as_moment()); // m12
216    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()); // m13
218    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()); // next-m11
221    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
233/// This function forces the RataDie to be on December 20, 21, 22, or 23. It was
234/// created for practical considerations and is not in the text.
235///
236/// See: <https://github.com/unicode-org/icu4x/pull/4904>
237fn 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
267/// Get the fixed date of the nearest winter solstice, in the Chinese time zone,
268/// on or before a given fixed date.
269///
270/// This is valid for several thousand years, but it drifts for large positive
271/// and negative years. See [`bind_winter_solstice`].
272///
273/// Based on functions from _Calendrical Calculations_ by Reingold & Dershowitz.
274/// Lisp reference code: https://github.com/EdReingold/calendar-code2/blob/main/calendar.l#L5359-L5368
275pub(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
298/// Get the fixed date of the nearest Lunar New Year on or before a given fixed date.
299/// This function also returns the solstice following a given date for optimization (see #3743).
300///
301/// To call this function you must precompute the value of the prior solstice, which
302/// is the result of winter_solstice_on_or_before
303///
304/// Based on functions from _Calendrical Calculations_ by Reingold & Dershowitz.
305/// Lisp reference code: https://github.com/EdReingold/calendar-code2/blob/main/calendar.l#L5396-L5405
306pub(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        // This happens when we're at the end of the current lunar year
315        // and the solstice has already happened. Thus the relevant solstice
316        // for the current lunar year is the previous one, which we calculate by offsetting
317        // back by a year.
318        let date_in_last_sui = date - 180; // This date is in the current lunar year, but the last solar year
319        let prior_solstice = winter_solstice_on_or_before::<C>(date_in_last_sui);
320        new_year_in_sui::<C>(prior_solstice)
321    }
322}
323
324/// Get a RataDie in the middle of a year.
325///
326/// This is not necessarily meant for direct use in
327/// calculations; rather, it is useful for getting a RataDie guaranteed to be in a given year
328/// as input for other calculations like calculating the leap month in a year.
329///
330/// Based on functions from _Calendrical Calculations_ by Reingold & Dershowitz
331/// Lisp reference code: <https://github.com/EdReingold/calendar-code2/blob/main/calendar.l#L5469-L5475>
332pub 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
338/// Whether this year is a leap year
339pub 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
344/// The last month and day in this year
345pub 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
354/// Calculated the numbers of days in the given year
355pub 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/// chinese_based_date_from_fixed returns extra things for use in caching
363#[derive(Debug)]
364#[non_exhaustive]
365pub struct ChineseFromFixedResult {
366    /// The chinese year
367    pub year: i32,
368    /// The chinese month
369    pub month: u8,
370    /// The chinese day
371    pub day: u8,
372    /// The bounds of the current lunar year
373    pub year_bounds: YearBounds,
374    /// The index of the leap month, if any
375    pub leap_month: Option<NonZeroU8>,
376}
377
378/// Get a chinese based date from a fixed date, with the related ISO year
379///
380/// Months are calculated by iterating through the dates of new moons until finding the last month which
381/// does not exceed the given fixed date. The day of month is calculated by subtracting the fixed date
382/// from the fixed date of the beginning of the month.
383///
384/// The calculation for `elapsed_years` and `month` in this function are based on code from _Calendrical Calculations_ by Reingold & Dershowitz.
385/// Lisp reference code: <https://github.com/EdReingold/calendar-code2/blob/main/calendar.l#L5414-L5459>
386pub 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        // This doesn't need to be checked for `None`, since `get_leap_month_from_new_year`
411        // will always return a number greater than or equal to 1, and less than 14.
412        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
426/// Given that `new_year` is the first day of a leap year, find which month in the year is a leap month.
427///
428/// Since the first month in which there are no major solar terms is a leap month, this function
429/// cycles through months until it finds the leap month, then returns the number of that month. This
430/// function assumes the date passed in is in a leap year and tests to ensure this is the case in debug
431/// mode by asserting that no more than thirteen months are analyzed.
432///
433/// Calls to `no_major_solar_term` have been inlined for increased efficiency.
434///
435/// Conceptually similar to code from _Calendrical Calculations_ by Reingold & Dershowitz
436/// Lisp reference code: <https://github.com/EdReingold/calendar-code2/blob/main/calendar.l#L5443-L5450>
437pub 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
455/// Returns the number of days in the given (year, month).
456///
457/// In the Chinese calendar, months start at each
458/// new moon, so this function finds the number of days between the new moon at the beginning of the given
459/// month and the new moon at the beginning of the next month.
460pub 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
467/// Returns the number of days in the given `month` after the given `new_year`.
468/// Also returns the RataDie of the new moon beginning the next month.
469pub 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
486/// Given a new year, calculate the number of days in the previous year
487pub 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
494/// Returns the length of each month in the year, as well as a leap month index (1-indexed) if any.
495///
496/// Month lengths are stored as true for 30-day, false for 29-day.
497/// In the case of no leap months, month 13 will have value false.
498pub 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)] // array is of length 13, we iterate till i=11
518        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        // not all months without solar terms are leap months; they are only leap months if
528        // the year can admit them
529        //
530        // From Reingold & Dershowitz (p 311):
531        //
532        // The leap month of a 13-month winter-solstice-to-winter-solstice period is the first month
533        // that does not contain a major solar term — that is, the first lunar month that is wholly within a solar month.
534        //
535        // As such, if a month without a solar term is found in a non-leap year, we just ingnore it.
536        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); // The last month is a leap month
546        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
555/// Given the new year and a month/day pair, calculate the number of days until the first day of the given month
556pub 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        // Mostly just tests that the assertions aren't hit
612        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                    // month_days has no defined behavior for month 13 on non-leap-years
623                    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}