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 - const { crate::iso::iso_year_from_fixed(C::EPOCH) as i32 - 1 }
37}
38/// Given an extended year, return the ISO year
39pub 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/// A type implementing [`ChineseBased`] for the Chinese calendar
44#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Default, Hash)]
45#[allow(clippy::exhaustive_structs)] // newtype
46pub struct Chinese;
47
48/// A type implementing [`ChineseBased`] for the Dangi (Korean) calendar
49#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Default, Hash)]
50#[allow(clippy::exhaustive_structs)] // newtype
51pub 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        // Before 1929, local time was used, represented as UTC+(1397/180 h).
57        // In 1929, China adopted a standard time zone based on 120 degrees of longitude, meaning
58        // from 1929 onward, all new moon calculations are based on UTC+8h.
59        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    /// The equivalent first day in the Chinese calendar (based on inception of the calendar), Feb. 15, -2636
67    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        // Before 1908, local time was used, represented as UTC+(3809/450 h).
75        // This changed multiple times as different standard timezones were adopted in Korea.
76        // Currently, UTC+9h is used.
77        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    /// The first day in the Korean Dangi calendar (based on the founding of Gojoseon), lunar new year -2332
91    const EPOCH: RataDie = crate::iso::const_fixed_from_iso(-2332, 2, 15);
92    const DEBUG_NAME: &'static str = "dangi";
93}
94
95/// Marks the bounds of a lunar year
96#[derive(Debug, Copy, Clone)]
97#[allow(clippy::exhaustive_structs)] // we're comfortable making frequent breaking changes to this crate
98pub struct YearBounds {
99    /// The date marking the start of the current lunar year
100    pub new_year: RataDie,
101    /// The date marking the start of the next lunar year
102    pub next_new_year: RataDie,
103}
104
105impl YearBounds {
106    /// Compute the YearBounds for the lunar year (年) containing `date`,
107    /// as well as the corresponding solar year (歲). Note that since the two
108    /// years overlap significantly but not entirely, the solstice bounds for the solar
109    /// year *may* not include `date`.
110    #[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        // Using 400 here since new years can be up to 390 days apart, and we add some padding
115        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    /// The number of days in this year
124    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    /// Whether or not this is a leap year
134    pub fn is_leap(self) -> bool {
135        let difference = self.next_new_year - self.new_year;
136        difference > 365
137    }
138}
139
140/// Get the current major solar term of a fixed date, output as an integer from 1..=12.
141///
142/// Based on functions from _Calendrical Calculations_ by Reingold & Dershowitz.
143/// Lisp reference code: https://github.com/EdReingold/calendar-code2/blob/main/calendar.l#L5273-L5281
144pub(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
159/// The fixed date in standard time at the observation location of the next new moon on or after a given Moment.
160///
161/// Based on functions from _Calendrical Calculations_ by Reingold & Dershowitz.
162/// Lisp reference code: https://github.com/EdReingold/calendar-code2/blob/main/calendar.l#L5329-L5338
163pub(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
169/// The fixed date in standard time at the observation location of the previous new moon before a given Moment.
170///
171/// Based on functions from _Calendrical Calculations_ by Reingold & Dershowitz.
172/// Lisp reference code: https://github.com/EdReingold/calendar-code2/blob/main/calendar.l#L5318-L5327
173pub(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
179/// Universal time of midnight at start of a Moment's day at the observation location
180///
181/// Based on functions from _Calendrical Calculations_ by Reingold & Dershowitz.
182/// Lisp reference code: https://github.com/EdReingold/calendar-code2/blob/main/calendar.l#L5353-L5357
183pub(crate) fn midnight<C: ChineseBased>(moment: Moment) -> Moment {
184    moment - C::utc_offset(moment.as_rata_die())
185}
186
187/// Determines the fixed date of the lunar new year given the start of its corresponding solar year (歲), which is
188/// also the winter solstice
189///
190/// Calls to `no_major_solar_term` have been inlined for increased efficiency.
191///
192/// Based on functions from _Calendrical Calculations_ by Reingold & Dershowitz.
193/// Lisp reference code: https://github.com/EdReingold/calendar-code2/blob/main/calendar.l#L5370-L5394
194pub(crate) fn new_year_in_sui<C: ChineseBased>(prior_solstice: RataDie) -> (RataDie, RataDie) {
195    // s1 is prior_solstice
196    // Using 370 here since solstices are ~365 days apart
197    // Both solstices should fall on December 20, 21, 22, or 23. The calendrical calculations
198    // drift away from this for large positive and negative years, so we artifically bind them
199    // to this range in order for other code invariants to be upheld.
200    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)); // s2
203    let month_after_eleventh = new_moon_on_or_after::<C>((prior_solstice + 1).as_moment()); // m12
204    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()); // m13
206    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()); // next-m11
209    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
221/// This function forces the RataDie to be on December 20, 21, 22, or 23. It was
222/// created for practical considerations and is not in the text.
223///
224/// See: <https://github.com/unicode-org/icu4x/pull/4904>
225fn 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
255/// Get the fixed date of the nearest winter solstice, in the Chinese time zone,
256/// on or before a given fixed date.
257///
258/// This is valid for several thousand years, but it drifts for large positive
259/// and negative years. See [`bind_winter_solstice`].
260///
261/// Based on functions from _Calendrical Calculations_ by Reingold & Dershowitz.
262/// Lisp reference code: https://github.com/EdReingold/calendar-code2/blob/main/calendar.l#L5359-L5368
263pub(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
286/// Get the fixed date of the nearest Lunar New Year on or before a given fixed date.
287/// This function also returns the solstice following a given date for optimization (see #3743).
288///
289/// To call this function you must precompute the value of the prior solstice, which
290/// is the result of winter_solstice_on_or_before
291///
292/// Based on functions from _Calendrical Calculations_ by Reingold & Dershowitz.
293/// Lisp reference code: https://github.com/EdReingold/calendar-code2/blob/main/calendar.l#L5396-L5405
294pub(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        // This happens when we're at the end of the current lunar year
303        // and the solstice has already happened. Thus the relevant solstice
304        // for the current lunar year is the previous one, which we calculate by offsetting
305        // back by a year.
306        let date_in_last_sui = date - 180; // This date is in the current lunar year, but the last solar year
307        let prior_solstice = winter_solstice_on_or_before::<C>(date_in_last_sui);
308        new_year_in_sui::<C>(prior_solstice)
309    }
310}
311
312/// Get a RataDie in the middle of a year.
313///
314/// This is not necessarily meant for direct use in
315/// calculations; rather, it is useful for getting a RataDie guaranteed to be in a given year
316/// as input for other calculations like calculating the leap month in a year.
317///
318/// Based on functions from _Calendrical Calculations_ by Reingold & Dershowitz
319/// Lisp reference code: <https://github.com/EdReingold/calendar-code2/blob/main/calendar.l#L5469-L5475>
320pub 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
326/// Whether this year is a leap year
327pub 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
332/// The last month and day in this year
333pub 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
342/// Calculated the numbers of days in the given year
343pub 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/// chinese_based_date_from_fixed returns extra things for use in caching
351#[derive(Debug)]
352#[non_exhaustive]
353pub struct ChineseFromFixedResult {
354    /// The chinese year
355    pub year: i32,
356    /// The chinese month
357    pub month: u8,
358    /// The chinese day
359    pub day: u8,
360    /// The bounds of the current lunar year
361    pub year_bounds: YearBounds,
362    /// The index of the leap month, if any
363    pub leap_month: Option<NonZeroU8>,
364}
365
366/// Get a chinese based date from a fixed date, with the related ISO year
367///
368/// Months are calculated by iterating through the dates of new moons until finding the last month which
369/// does not exceed the given fixed date. The day of month is calculated by subtracting the fixed date
370/// from the fixed date of the beginning of the month.
371///
372/// The calculation for `elapsed_years` and `month` in this function are based on code from _Calendrical Calculations_ by Reingold & Dershowitz.
373/// Lisp reference code: <https://github.com/EdReingold/calendar-code2/blob/main/calendar.l#L5414-L5459>
374pub 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        // This doesn't need to be checked for `None`, since `get_leap_month_from_new_year`
399        // will always return a number greater than or equal to 1, and less than 14.
400        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
414/// Given that `new_year` is the first day of a leap year, find which month in the year is a leap month.
415///
416/// Since the first month in which there are no major solar terms is a leap month, this function
417/// cycles through months until it finds the leap month, then returns the number of that month. This
418/// function assumes the date passed in is in a leap year and tests to ensure this is the case in debug
419/// mode by asserting that no more than thirteen months are analyzed.
420///
421/// Calls to `no_major_solar_term` have been inlined for increased efficiency.
422///
423/// Conceptually similar to code from _Calendrical Calculations_ by Reingold & Dershowitz
424/// Lisp reference code: <https://github.com/EdReingold/calendar-code2/blob/main/calendar.l#L5443-L5450>
425pub 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
443/// Returns the number of days in the given (year, month).
444///
445/// In the Chinese calendar, months start at each
446/// new moon, so this function finds the number of days between the new moon at the beginning of the given
447/// month and the new moon at the beginning of the next month.
448pub 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
455/// Returns the number of days in the given `month` after the given `new_year`.
456/// Also returns the RataDie of the new moon beginning the next month.
457pub 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
474/// Given a new year, calculate the number of days in the previous year
475pub 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
482/// Returns the length of each month in the year, as well as a leap month index (1-indexed) if any.
483///
484/// Month lengths are stored as true for 30-day, false for 29-day.
485/// In the case of no leap months, month 13 will have value false.
486pub 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)] // array is of length 13, we iterate till i=11
506        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        // not all months without solar terms are leap months; they are only leap months if
516        // the year can admit them
517        //
518        // From Reingold & Dershowitz (p 311):
519        //
520        // The leap month of a 13-month winter-solstice-to-winter-solstice period is the first month
521        // that does not contain a major solar term — that is, the first lunar month that is wholly within a solar month.
522        //
523        // As such, if a month without a solar term is found in a non-leap year, we just ingnore it.
524        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); // The last month is a leap month
534        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
543/// Given the new year and a month/day pair, calculate the number of days until the first day of the given month
544pub 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        // Mostly just tests that the assertions aren't hit
600        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                    // month_days has no defined behavior for month 13 on non-leap-years
611                    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}