Skip to main content

calendrical_calculations/
chinese_based.rs

1use crate::astronomy::{self, Astronomical, MEAN_SYNODIC_MONTH, MEAN_TROPICAL_YEAR};
2use crate::gregorian::{fixed_from_gregorian, gregorian_from_fixed};
3use crate::helpers::i64_to_i32;
4use crate::rata_die::{Moment, RataDie};
5use core::num::NonZeroU8;
6use core::ops::Range;
7#[allow(unused_imports)]
8use core_maths::*;
9
10// Don't iterate more than 14 times (which accounts for checking for 13 months)
11const MAX_ITERS_FOR_MONTHS_OF_YEAR: u8 = 14;
12
13/// For astronomical calendars in this module, the range in which they are expected to be well-behaved.
14///
15/// With astronomical calendars, for dates in the far past or far future, floating point error, algorithm inaccuracies,
16/// and other issues may cause the calendar algorithm to behave unexpectedly.
17///
18/// Our code has a number of debug assertions for various calendrical invariants (for example, lunar calendar months
19/// must be 29 or 30 days), but it will turn these off outside of these ranges.
20///
21/// Consumers of this code are encouraged to disallow such out-of-range values; or, if allowing them, not expect too
22/// much in terms of calendrical invariants. Once we have proleptic approximations of these calendars (#5778),
23/// developers will be encouraged to use them when dates are out of range.
24///
25/// This value is not stable and may change. It's currently somewhat arbitrarily chosen to be
26/// approximately ±10,000 years from 0 CE.
27//
28// NOTE: this value is doc(inline)d in islamic.rs; if you wish to change this consider if you wish to also
29// change the value there, or if it should be split.
30pub const WELL_BEHAVED_ASTRONOMICAL_RANGE: Range<RataDie> =
31    RataDie::new(365 * -10_000)..RataDie::new(365 * 10_000);
32
33/// The trait [`ChineseBased`] is used by Chinese-based calendars to perform computations shared by such calendar.
34/// To do so, calendars should:
35///
36/// - Implement `fn location` by providing a location at which observations of the moon are recorded, which
37///   may change over time (the zone is important, long, lat, and elevation are not relevant for these calculations)
38/// - Define `const EPOCH` as a [`RataDie`] marking the start date of the era of the Calendar for internal use,
39///   which may not accurately reflect how years or eras are marked traditionally or seen by end-users
40pub trait ChineseBased {
41    /// Given a fixed date, return the UTC offset used for observations of the new moon in order to
42    /// calculate the beginning of months. For multiple Chinese-based lunar calendars, this has
43    /// changed over the years, and can cause differences in calendar date.
44    fn utc_offset(fixed: RataDie) -> f64;
45
46    /// The [`RataDie`] of the beginning of the epoch used for internal computation; this may not
47    /// reflect traditional methods of year-tracking or eras, since Chinese-based calendars
48    /// may not track years ordinally in the same way many western calendars do.
49    const EPOCH: RataDie;
50
51    /// The name of the calendar for debugging.
52    const DEBUG_NAME: &'static str;
53}
54
55/// Given an ISO year, return the extended year
56#[deprecated(since = "0.2.3", note = "extended year calculation subject to removal")]
57pub fn extended_from_iso<C: ChineseBased>(iso_year: i32) -> i32 {
58    iso_year
59        - const {
60            let Ok(y) = crate::gregorian::year_from_fixed(C::EPOCH) else {
61                panic!()
62            };
63            y - 1
64        }
65}
66/// Given an extended year, return the ISO year
67#[deprecated(since = "0.2.3", note = "extended year calculation subject to removal")]
68pub fn iso_from_extended<C: ChineseBased>(extended_year: i32) -> i32 {
69    extended_year
70        + const {
71            let Ok(y) = crate::gregorian::year_from_fixed(C::EPOCH) else {
72                panic!()
73            };
74            y - 1
75        }
76}
77
78/// A type implementing [`ChineseBased`] for the Chinese calendar
79#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Default, Hash)]
80#[allow(clippy::exhaustive_structs)] // newtype
81pub struct Chinese;
82
83/// A type implementing [`ChineseBased`] for the Dangi (Korean) calendar
84#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Default, Hash)]
85#[allow(clippy::exhaustive_structs)] // newtype
86pub struct Dangi;
87
88impl ChineseBased for Chinese {
89    fn utc_offset(fixed: RataDie) -> f64 {
90        // Before 1929, local time was used, represented as UTC+(1397/180 h).
91        // In 1929, China adopted a standard time zone based on 120 degrees of longitude, meaning
92        // from 1929 onward, all new moon calculations are based on UTC+8h.
93        if fixed < const { fixed_from_gregorian(1929, 1, 1) } {
94            1397.0 / 180.0 / 24.0
95        } else {
96            8.0 / 24.0
97        }
98    }
99
100    /// The equivalent first day in the Chinese calendar (based on inception of the calendar), Feb. 15, -2636
101    const EPOCH: RataDie = fixed_from_gregorian(-2636, 2, 15);
102    const DEBUG_NAME: &'static str = "chinese";
103}
104
105impl ChineseBased for Dangi {
106    fn utc_offset(fixed: RataDie) -> f64 {
107        // Before 1908, local time was used, represented as UTC+(3809/450 h).
108        // This changed multiple times as different standard timezones were adopted in Korea.
109        // Currently, UTC+9h is used.
110        if fixed < const { fixed_from_gregorian(1908, 4, 1) } {
111            3809.0 / 450.0 / 24.0
112        } else if fixed < const { fixed_from_gregorian(1912, 1, 1) } {
113            8.5 / 24.0
114        } else if fixed < const { fixed_from_gregorian(1954, 3, 21) } {
115            9.0 / 24.0
116        } else if fixed < const { fixed_from_gregorian(1961, 8, 10) } {
117            8.5 / 24.0
118        } else {
119            9.0 / 24.0
120        }
121    }
122
123    /// The first day in the Korean Dangi calendar (based on the founding of Gojoseon), lunar new year -2332
124    const EPOCH: RataDie = fixed_from_gregorian(-2332, 2, 15);
125    const DEBUG_NAME: &'static str = "dangi";
126}
127
128/// Marks the bounds of a lunar year
129#[derive(Debug, Copy, Clone)]
130#[allow(clippy::exhaustive_structs)] // we're comfortable making frequent breaking changes to this crate
131pub struct YearBounds {
132    /// The date marking the start of the current lunar year
133    pub new_year: RataDie,
134    /// The date marking the start of the next lunar year
135    pub next_new_year: RataDie,
136}
137
138impl YearBounds {
139    /// Compute the [`YearBounds`] for the lunar year (年) containing `date`,
140    /// as well as the corresponding solar year (歲). Note that since the two
141    /// years overlap significantly but not entirely, the solstice bounds for the solar
142    /// year *may* not include `date`.
143    #[inline]
144    pub fn compute<C: ChineseBased>(date: RataDie) -> Self {
145        let prev_solstice = winter_solstice_on_or_before::<C>(date);
146        let (new_year, next_solstice) = new_year_on_or_before_fixed_date::<C>(date, prev_solstice);
147        // Using 400 here since new years can be up to 390 days apart, and we add some padding
148        let next_new_year = new_year_on_or_before_fixed_date::<C>(new_year + 400, next_solstice).0;
149
150        Self {
151            new_year,
152            next_new_year,
153        }
154    }
155
156    /// The number of days in this year
157    pub fn count_days(self) -> u16 {
158        let result = self.next_new_year - self.new_year;
159        debug_assert!(
160            ((u16::MIN as i64)..=(u16::MAX as i64)).contains(&result),
161            "Days in year should be in range of u16."
162        );
163        result as u16
164    }
165
166    /// Whether or not this is a leap year
167    pub fn is_leap(self) -> bool {
168        let difference = self.next_new_year - self.new_year;
169        difference > 365
170    }
171}
172
173/// Get the current major solar term of a fixed date, output as an integer from 1..=12.
174///
175/// Based on functions from _Calendrical Calculations_ by Reingold & Dershowitz.
176/// Lisp reference code: <https://github.com/EdReingold/calendar-code2/blob/main/calendar.l#L5273-L5281>
177pub(crate) fn major_solar_term_from_fixed<C: ChineseBased>(date: RataDie) -> u32 {
178    let moment: Moment = date.as_moment();
179    let universal = moment - C::utc_offset(date);
180    let solar_longitude =
181        i64_to_i32(Astronomical::solar_longitude(Astronomical::julian_centuries(universal)) as i64);
182    debug_assert!(
183        solar_longitude.is_ok(),
184        "Solar longitude should be in range of i32"
185    );
186    let s = solar_longitude.unwrap_or_else(|e| e.saturate());
187    let result_signed = (2 + s.div_euclid(30) - 1).rem_euclid(12) + 1;
188    debug_assert!(result_signed >= 0);
189    result_signed as u32
190}
191
192/// The fixed date in standard time at the observation location of the next new moon on or after a given Moment.
193///
194/// Based on functions from _Calendrical Calculations_ by Reingold & Dershowitz.
195/// Lisp reference code: <https://github.com/EdReingold/calendar-code2/blob/main/calendar.l#L5329-L5338>
196pub(crate) fn new_moon_on_or_after<C: ChineseBased>(moment: Moment) -> RataDie {
197    let new_moon_moment = Astronomical::new_moon_at_or_after(midnight::<C>(moment));
198    let utc_offset = C::utc_offset(new_moon_moment.as_rata_die());
199    (new_moon_moment + utc_offset).as_rata_die()
200}
201
202/// The fixed date in standard time at the observation location of the previous new moon before a given Moment.
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#L5318-L5327>
206pub(crate) fn new_moon_before<C: ChineseBased>(moment: Moment) -> RataDie {
207    let new_moon_moment = Astronomical::new_moon_before(midnight::<C>(moment));
208    let utc_offset = C::utc_offset(new_moon_moment.as_rata_die());
209    (new_moon_moment + utc_offset).as_rata_die()
210}
211
212/// Universal time of midnight at start of a Moment's day at the observation location
213///
214/// Based on functions from _Calendrical Calculations_ by Reingold & Dershowitz.
215/// Lisp reference code: <https://github.com/EdReingold/calendar-code2/blob/main/calendar.l#L5353-L5357>
216pub(crate) fn midnight<C: ChineseBased>(moment: Moment) -> Moment {
217    moment - C::utc_offset(moment.as_rata_die())
218}
219
220/// Determines the fixed date of the lunar new year given the start of its corresponding solar year (歲), which is
221/// also the winter solstice
222///
223/// Calls to `no_major_solar_term` have been inlined for increased efficiency.
224///
225/// Based on functions from _Calendrical Calculations_ by Reingold & Dershowitz.
226/// Lisp reference code: <https://github.com/EdReingold/calendar-code2/blob/main/calendar.l#L5370-L5394>
227pub(crate) fn new_year_in_sui<C: ChineseBased>(prior_solstice: RataDie) -> (RataDie, RataDie) {
228    // s1 is prior_solstice
229    // Using 370 here since solstices are ~365 days apart
230    // Both solstices should fall on December 20, 21, 22, or 23. The calendrical calculations
231    // drift away from this for large positive and negative years, so we artifically bind them
232    // to this range in order for other code invariants to be upheld.
233    let prior_solstice = bind_winter_solstice::<C>(prior_solstice);
234    let following_solstice =
235        bind_winter_solstice::<C>(winter_solstice_on_or_before::<C>(prior_solstice + 370)); // s2
236    let month_after_eleventh = new_moon_on_or_after::<C>((prior_solstice + 1).as_moment()); // m12
237    debug_assert!(
238        month_after_eleventh - prior_solstice >= 0
239            || !WELL_BEHAVED_ASTRONOMICAL_RANGE.contains(&prior_solstice)
240    );
241    let month_after_twelfth = new_moon_on_or_after::<C>((month_after_eleventh + 1).as_moment()); // m13
242    let month_after_thirteenth = new_moon_on_or_after::<C>((month_after_twelfth + 1).as_moment());
243    debug_assert!(
244        month_after_twelfth - month_after_eleventh >= 29
245            || !WELL_BEHAVED_ASTRONOMICAL_RANGE.contains(&prior_solstice)
246    );
247    let next_eleventh_month = new_moon_before::<C>((following_solstice + 1).as_moment()); // next-m11
248    let lhs_argument =
249        ((next_eleventh_month - month_after_eleventh) as f64 / MEAN_SYNODIC_MONTH).round() as i64;
250    let solar_term_a = major_solar_term_from_fixed::<C>(month_after_eleventh);
251    let solar_term_b = major_solar_term_from_fixed::<C>(month_after_twelfth);
252    let solar_term_c = major_solar_term_from_fixed::<C>(month_after_thirteenth);
253    if lhs_argument == 12 && (solar_term_a == solar_term_b || solar_term_b == solar_term_c) {
254        (month_after_thirteenth, following_solstice)
255    } else {
256        (month_after_twelfth, following_solstice)
257    }
258}
259
260/// This function forces the [`RataDie`] to be on December 20, 21, 22, or 23. It was
261/// created for practical considerations and is not in the text.
262///
263/// See: <https://github.com/unicode-org/icu4x/pull/4904>
264fn bind_winter_solstice<C: ChineseBased>(solstice: RataDie) -> RataDie {
265    let (gregorian_year, gregorian_month, gregorian_day) = match gregorian_from_fixed(solstice) {
266        Ok(ymd) => ymd,
267        Err(_) => {
268            debug_assert!(false, "Solstice REALLY out of bounds: {solstice:?}");
269            return solstice;
270        }
271    };
272    let resolved_solstice = if gregorian_month < 12 || gregorian_day < 20 {
273        fixed_from_gregorian(gregorian_year, 12, 20)
274    } else if gregorian_day > 23 {
275        fixed_from_gregorian(gregorian_year, 12, 23)
276    } else {
277        solstice
278    };
279    if resolved_solstice != solstice {
280        if !(0..=4000).contains(&gregorian_year) {
281            #[cfg(feature = "logging")]
282            log::trace!("({}) Solstice out of bounds: {solstice:?}", C::DEBUG_NAME);
283        } else {
284            debug_assert!(
285                false,
286                "({}) Solstice out of bounds: {solstice:?}",
287                C::DEBUG_NAME
288            );
289        }
290    }
291    resolved_solstice
292}
293
294/// Get the fixed date of the nearest winter solstice, in the Chinese time zone,
295/// on or before a given fixed date.
296///
297/// This is valid for several thousand years, but it drifts for large positive
298/// and negative years. See [`bind_winter_solstice`].
299///
300/// Based on functions from _Calendrical Calculations_ by Reingold & Dershowitz.
301/// Lisp reference code: <https://github.com/EdReingold/calendar-code2/blob/main/calendar.l#L5359-L5368>
302pub(crate) fn winter_solstice_on_or_before<C: ChineseBased>(date: RataDie) -> RataDie {
303    let approx = Astronomical::estimate_prior_solar_longitude(
304        astronomy::WINTER,
305        midnight::<C>((date + 1).as_moment()),
306    );
307    let mut iters = 0;
308    let mut day = Moment::new((approx.inner() - 1.0).floor());
309    while iters < MAX_ITERS_FOR_MONTHS_OF_YEAR
310        && astronomy::WINTER
311            >= Astronomical::solar_longitude(Astronomical::julian_centuries(midnight::<C>(
312                day + 1.0,
313            )))
314    {
315        iters += 1;
316        day += 1.0;
317    }
318    debug_assert!(
319        iters < MAX_ITERS_FOR_MONTHS_OF_YEAR || !WELL_BEHAVED_ASTRONOMICAL_RANGE.contains(&date),
320        "Number of iterations was higher than expected"
321    );
322    day.as_rata_die()
323}
324
325/// Get the fixed date of the nearest Lunar New Year on or before a given fixed date.
326/// This function also returns the solstice following a given date for optimization (see #3743).
327///
328/// To call this function you must precompute the value of the prior solstice, which
329/// is the result of [`winter_solstice_on_or_before`]
330///
331/// Based on functions from _Calendrical Calculations_ by Reingold & Dershowitz.
332/// Lisp reference code: <https://github.com/EdReingold/calendar-code2/blob/main/calendar.l#L5396-L5405>
333pub(crate) fn new_year_on_or_before_fixed_date<C: ChineseBased>(
334    date: RataDie,
335    prior_solstice: RataDie,
336) -> (RataDie, RataDie) {
337    let new_year = new_year_in_sui::<C>(prior_solstice);
338    if date >= new_year.0 {
339        new_year
340    } else {
341        // This happens when we're at the end of the current lunar year
342        // and the solstice has already happened. Thus the relevant solstice
343        // for the current lunar year is the previous one, which we calculate by offsetting
344        // back by a year.
345        let date_in_last_sui = date - 180; // This date is in the current lunar year, but the last solar year
346        let prior_solstice = winter_solstice_on_or_before::<C>(date_in_last_sui);
347        new_year_in_sui::<C>(prior_solstice)
348    }
349}
350
351/// Get a [`RataDie`] in the middle of a year.
352///
353/// This is not necessarily meant for direct use in
354/// calculations; rather, it is useful for getting a [`RataDie`] guaranteed to be in a given year
355/// as input for other calculations like calculating the leap month in a year.
356///
357/// Based on functions from _Calendrical Calculations_ by Reingold & Dershowitz
358/// Lisp reference code: <https://github.com/EdReingold/calendar-code2/blob/main/calendar.l#L5469-L5475>
359pub fn fixed_mid_year_from_year<C: ChineseBased>(elapsed_years: i32) -> RataDie {
360    let cycle = (elapsed_years - 1).div_euclid(60) + 1;
361    let year = (elapsed_years - 1).rem_euclid(60) + 1;
362    C::EPOCH + ((((cycle - 1) * 60 + year - 1) as f64 + 0.5) * MEAN_TROPICAL_YEAR) as i64
363}
364
365/// Whether this year is a leap year
366pub fn is_leap_year<C: ChineseBased>(year: i32) -> bool {
367    let mid_year = fixed_mid_year_from_year::<C>(year);
368    YearBounds::compute::<C>(mid_year).is_leap()
369}
370
371/// The last month and day in this year
372pub fn last_month_day_in_year<C: ChineseBased>(year: i32) -> (u8, u8) {
373    let mid_year = fixed_mid_year_from_year::<C>(year);
374    let year_bounds = YearBounds::compute::<C>(mid_year);
375    let last_day = year_bounds.next_new_year - 1;
376    let month = if year_bounds.is_leap() { 13 } else { 12 };
377    let day = last_day - new_moon_before::<C>(last_day.as_moment()) + 1;
378    (month, day as u8)
379}
380
381/// Calculated the numbers of days in the given year
382pub fn days_in_provided_year<C: ChineseBased>(year: i32) -> u16 {
383    let mid_year = fixed_mid_year_from_year::<C>(year);
384    let bounds = YearBounds::compute::<C>(mid_year);
385
386    bounds.count_days()
387}
388
389/// [`chinese_based_date_from_fixed`] returns extra things for use in caching
390#[derive(Debug)]
391#[non_exhaustive]
392pub struct ChineseFromFixedResult {
393    /// The chinese year
394    pub year: i32,
395    /// The chinese month
396    pub month: u8,
397    /// The chinese day
398    pub day: u8,
399    /// The bounds of the current lunar year
400    pub year_bounds: YearBounds,
401    /// The index of the leap month, if any
402    pub leap_month: Option<NonZeroU8>,
403}
404
405/// Get a chinese based date from a fixed date, with the related Gregorian year
406///
407/// Months are calculated by iterating through the dates of new moons until finding the last month which
408/// does not exceed the given fixed date. The day of month is calculated by subtracting the fixed date
409/// from the fixed date of the beginning of the month.
410///
411/// The calculation for `elapsed_years` and `month` in this function are based on code from _Calendrical Calculations_ by Reingold & Dershowitz.
412/// Lisp reference code: <https://github.com/EdReingold/calendar-code2/blob/main/calendar.l#L5414-L5459>
413pub fn chinese_based_date_from_fixed<C: ChineseBased>(date: RataDie) -> ChineseFromFixedResult {
414    let year_bounds = YearBounds::compute::<C>(date);
415    let first_day_of_year = year_bounds.new_year;
416
417    let year_float =
418        (1.5 - 1.0 / 12.0 + ((first_day_of_year - C::EPOCH) as f64) / MEAN_TROPICAL_YEAR).floor();
419    let year_int = i64_to_i32(year_float as i64);
420    debug_assert!(year_int.is_ok(), "Year should be in range of i32");
421    let year = year_int.unwrap_or_else(|e| e.saturate());
422
423    let new_moon = new_moon_before::<C>((date + 1).as_moment());
424    let month_i64 = ((new_moon - first_day_of_year) as f64 / MEAN_SYNODIC_MONTH).round() as i64 + 1;
425    debug_assert!(
426        ((u8::MIN as i64)..=(u8::MAX as i64)).contains(&month_i64),
427        "Month should be in range of u8! Value {month_i64} failed for RD {date:?}"
428    );
429    let month = month_i64 as u8;
430    let day_i64 = date - new_moon + 1;
431    debug_assert!(
432        ((u8::MIN as i64)..=(u8::MAX as i64)).contains(&month_i64),
433        "Day should be in range of u8! Value {month_i64} failed for RD {date:?}"
434    );
435    let day = day_i64 as u8;
436    let leap_month = if year_bounds.is_leap() {
437        // This doesn't need to be checked for `None`, since `get_leap_month_from_new_year`
438        // will always return a number greater than or equal to 1, and less than 14.
439        NonZeroU8::new(get_leap_month_from_new_year::<C>(first_day_of_year))
440    } else {
441        None
442    };
443
444    ChineseFromFixedResult {
445        year,
446        month,
447        day,
448        year_bounds,
449        leap_month,
450    }
451}
452
453/// Given that `new_year` is the first day of a leap year, find which month in the year is a leap month.
454///
455/// Since the first month in which there are no major solar terms is a leap month, this function
456/// cycles through months until it finds the leap month, then returns the number of that month. This
457/// function assumes the date passed in is in a leap year and tests to ensure this is the case in debug
458/// mode by asserting that no more than thirteen months are analyzed.
459///
460/// Calls to `no_major_solar_term` have been inlined for increased efficiency.
461///
462/// Conceptually similar to code from _Calendrical Calculations_ by Reingold & Dershowitz
463/// Lisp reference code: <https://github.com/EdReingold/calendar-code2/blob/main/calendar.l#L5443-L5450>
464pub fn get_leap_month_from_new_year<C: ChineseBased>(new_year: RataDie) -> u8 {
465    let mut cur = new_year;
466    let mut result = 1;
467    let mut solar_term = major_solar_term_from_fixed::<C>(cur);
468    loop {
469        let next = new_moon_on_or_after::<C>((cur + 1).as_moment());
470        let next_solar_term = major_solar_term_from_fixed::<C>(next);
471        if result >= MAX_ITERS_FOR_MONTHS_OF_YEAR || solar_term == next_solar_term {
472            break;
473        }
474        cur = next;
475        solar_term = next_solar_term;
476        result += 1;
477    }
478    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.");
479    result
480}
481
482/// Returns the number of days in the given (year, month).
483///
484/// In the Chinese calendar, months start at each
485/// new moon, so this function finds the number of days between the new moon at the beginning of the given
486/// month and the new moon at the beginning of the next month.
487pub fn month_days<C: ChineseBased>(year: i32, month: u8) -> u8 {
488    let mid_year = fixed_mid_year_from_year::<C>(year);
489    let prev_solstice = winter_solstice_on_or_before::<C>(mid_year);
490    let new_year = new_year_on_or_before_fixed_date::<C>(mid_year, prev_solstice).0;
491    days_in_month::<C>(month, new_year, None).0
492}
493
494/// Returns the number of days in the given `month` after the given `new_year`.
495/// Also returns the [`RataDie`] of the new moon beginning the next month.
496pub fn days_in_month<C: ChineseBased>(
497    month: u8,
498    new_year: RataDie,
499    prev_new_moon: Option<RataDie>,
500) -> (u8, RataDie) {
501    let approx = new_year + ((month - 1) as i64 * 29);
502    let prev_new_moon = if let Some(prev_moon) = prev_new_moon {
503        prev_moon
504    } else {
505        new_moon_before::<C>((approx + 15).as_moment())
506    };
507    let next_new_moon = new_moon_on_or_after::<C>((approx + 15).as_moment());
508    let result = (next_new_moon - prev_new_moon) as u8;
509    debug_assert!(
510        result == 29 || result == 30 || !WELL_BEHAVED_ASTRONOMICAL_RANGE.contains(&new_year)
511    );
512    (result, next_new_moon)
513}
514
515/// Given a new year, calculate the number of days in the previous year
516pub fn days_in_prev_year<C: ChineseBased>(new_year: RataDie) -> u16 {
517    let date = new_year - 300;
518    let prev_solstice = winter_solstice_on_or_before::<C>(date);
519    let (prev_new_year, _) = new_year_on_or_before_fixed_date::<C>(date, prev_solstice);
520    u16::try_from(new_year - prev_new_year).unwrap_or(360)
521}
522
523/// Returns the length of each month in the year, as well as a leap month index (1-indexed) if any.
524///
525/// Month lengths are stored as true for 30-day, false for 29-day.
526/// In the case of no leap months, month 13 will have value false.
527pub fn month_structure_for_year<C: ChineseBased>(
528    new_year: RataDie,
529    next_new_year: RataDie,
530) -> ([bool; 13], Option<u8>) {
531    let mut ret = [false; 13];
532
533    let mut current_month_start = new_year;
534    let mut current_month_major_solar_term = major_solar_term_from_fixed::<C>(new_year);
535    let mut leap_month_index = None;
536    for i in 0u8..12 {
537        let next_month_start = new_moon_on_or_after::<C>((current_month_start + 28).as_moment());
538        let next_month_major_solar_term = major_solar_term_from_fixed::<C>(next_month_start);
539
540        if next_month_major_solar_term == current_month_major_solar_term {
541            leap_month_index = Some(i + 1);
542        }
543
544        let diff = next_month_start - current_month_start;
545        debug_assert!(
546            diff == 29 || diff == 30 || !WELL_BEHAVED_ASTRONOMICAL_RANGE.contains(&new_year)
547        );
548        #[expect(clippy::indexing_slicing)] // array is of length 13, we iterate till i=11
549        if diff == 30 {
550            ret[usize::from(i)] = true;
551        }
552
553        current_month_start = next_month_start;
554        current_month_major_solar_term = next_month_major_solar_term;
555    }
556
557    if current_month_start == next_new_year {
558        // not all months without solar terms are leap months; they are only leap months if
559        // the year can admit them
560        //
561        // From Reingold & Dershowitz (p 311):
562        //
563        // The leap month of a 13-month winter-solstice-to-winter-solstice period is the first month
564        // that does not contain a major solar term — that is, the first lunar month that is wholly within a solar month.
565        //
566        // As such, if a month without a solar term is found in a non-leap year, we just ingnore it.
567        leap_month_index = None;
568    } else {
569        let diff = next_new_year - current_month_start;
570        debug_assert!(
571            diff == 29 || diff == 30 || !WELL_BEHAVED_ASTRONOMICAL_RANGE.contains(&new_year)
572        );
573        if diff == 30 {
574            ret[12] = true;
575        }
576    }
577    if current_month_start != next_new_year && leap_month_index.is_none() {
578        leap_month_index = Some(13); // The last month is a leap month
579        debug_assert!(
580            major_solar_term_from_fixed::<C>(current_month_start) == current_month_major_solar_term
581                || !WELL_BEHAVED_ASTRONOMICAL_RANGE.contains(&new_year),
582            "A leap month is required here, but it had a major solar term!"
583        );
584    }
585
586    (ret, leap_month_index)
587}
588
589/// Given the new year and a month/day pair, calculate the number of days until the first day of the given month
590pub fn days_until_month<C: ChineseBased>(new_year: RataDie, month: u8) -> u16 {
591    let month_approx = 28_u16.saturating_mul(u16::from(month) - 1);
592
593    let new_moon = new_moon_on_or_after::<C>(new_year.as_moment() + (month_approx as f64));
594    let result = new_moon - new_year;
595    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!");
596    result as u16
597}
598
599#[cfg(test)]
600mod test {
601
602    use super::*;
603    use crate::rata_die::Moment;
604
605    #[test]
606    fn check_epochs() {
607        assert_eq!(
608            YearBounds::compute::<Dangi>(Dangi::EPOCH).new_year,
609            Dangi::EPOCH
610        );
611        assert_eq!(
612            YearBounds::compute::<Chinese>(Chinese::EPOCH).new_year,
613            Chinese::EPOCH
614        );
615    }
616
617    #[test]
618    fn test_chinese_new_moon_directionality() {
619        for i in (-1000..1000).step_by(31) {
620            let moment = Moment::new(i as f64);
621            let before = new_moon_before::<Chinese>(moment);
622            let after = new_moon_on_or_after::<Chinese>(moment);
623            assert!(before < after, "Chinese new moon directionality failed for Moment: {moment:?}, with:\n\tBefore: {before:?}\n\tAfter: {after:?}");
624        }
625    }
626
627    #[test]
628    fn test_chinese_new_year_on_or_before() {
629        let fixed = fixed_from_gregorian(2023, 6, 22);
630        let prev_solstice = winter_solstice_on_or_before::<Chinese>(fixed);
631        let result_fixed = new_year_on_or_before_fixed_date::<Chinese>(fixed, prev_solstice).0;
632        let (y, m, d) = gregorian_from_fixed(result_fixed).unwrap();
633        assert_eq!(y, 2023);
634        assert_eq!(m, 1);
635        assert_eq!(d, 22);
636    }
637
638    fn seollal_on_or_before(fixed: RataDie) -> RataDie {
639        let prev_solstice = winter_solstice_on_or_before::<Dangi>(fixed);
640        new_year_on_or_before_fixed_date::<Dangi>(fixed, prev_solstice).0
641    }
642
643    #[test]
644    fn test_month_structure() {
645        // Mostly just tests that the assertions aren't hit
646        for year in 1900..2050 {
647            let fixed = fixed_from_gregorian(year, 1, 1);
648            let chinese_year = chinese_based_date_from_fixed::<Chinese>(fixed);
649            let (month_lengths, leap) = month_structure_for_year::<Chinese>(
650                chinese_year.year_bounds.new_year,
651                chinese_year.year_bounds.next_new_year,
652            );
653
654            for (i, month_is_30) in month_lengths.into_iter().enumerate() {
655                if leap.is_none() && i == 12 {
656                    // month_days has no defined behavior for month 13 on non-leap-years
657                    continue;
658                }
659                let month_len = 29 + i32::from(month_is_30);
660                let month_days = month_days::<Chinese>(chinese_year.year, i as u8 + 1);
661                assert_eq!(
662                    month_len,
663                    i32::from(month_days),
664                    "Month length for month {} must be the same",
665                    i + 1
666                );
667            }
668            println!(
669                "{year} (chinese {}): {month_lengths:?} {leap:?}",
670                chinese_year.year
671            );
672        }
673    }
674
675    #[test]
676    fn test_seollal() {
677        #[derive(Debug)]
678        struct TestCase {
679            gregorian_year: i32,
680            gregorian_month: u8,
681            gregorian_day: u8,
682            expected_year: i32,
683            expected_month: u8,
684            expected_day: u8,
685        }
686
687        let cases = [
688            TestCase {
689                gregorian_year: 2024,
690                gregorian_month: 6,
691                gregorian_day: 6,
692                expected_year: 2024,
693                expected_month: 2,
694                expected_day: 10,
695            },
696            TestCase {
697                gregorian_year: 2024,
698                gregorian_month: 2,
699                gregorian_day: 9,
700                expected_year: 2023,
701                expected_month: 1,
702                expected_day: 22,
703            },
704            TestCase {
705                gregorian_year: 2023,
706                gregorian_month: 1,
707                gregorian_day: 22,
708                expected_year: 2023,
709                expected_month: 1,
710                expected_day: 22,
711            },
712            TestCase {
713                gregorian_year: 2023,
714                gregorian_month: 1,
715                gregorian_day: 21,
716                expected_year: 2022,
717                expected_month: 2,
718                expected_day: 1,
719            },
720            TestCase {
721                gregorian_year: 2022,
722                gregorian_month: 6,
723                gregorian_day: 6,
724                expected_year: 2022,
725                expected_month: 2,
726                expected_day: 1,
727            },
728            TestCase {
729                gregorian_year: 2021,
730                gregorian_month: 6,
731                gregorian_day: 6,
732                expected_year: 2021,
733                expected_month: 2,
734                expected_day: 12,
735            },
736            TestCase {
737                gregorian_year: 2020,
738                gregorian_month: 6,
739                gregorian_day: 6,
740                expected_year: 2020,
741                expected_month: 1,
742                expected_day: 25,
743            },
744            TestCase {
745                gregorian_year: 2019,
746                gregorian_month: 6,
747                gregorian_day: 6,
748                expected_year: 2019,
749                expected_month: 2,
750                expected_day: 5,
751            },
752            TestCase {
753                gregorian_year: 2018,
754                gregorian_month: 6,
755                gregorian_day: 6,
756                expected_year: 2018,
757                expected_month: 2,
758                expected_day: 16,
759            },
760            TestCase {
761                gregorian_year: 2025,
762                gregorian_month: 6,
763                gregorian_day: 6,
764                expected_year: 2025,
765                expected_month: 1,
766                expected_day: 29,
767            },
768            TestCase {
769                gregorian_year: 2026,
770                gregorian_month: 8,
771                gregorian_day: 8,
772                expected_year: 2026,
773                expected_month: 2,
774                expected_day: 17,
775            },
776            TestCase {
777                gregorian_year: 2027,
778                gregorian_month: 4,
779                gregorian_day: 4,
780                expected_year: 2027,
781                expected_month: 2,
782                expected_day: 7,
783            },
784            TestCase {
785                gregorian_year: 2028,
786                gregorian_month: 9,
787                gregorian_day: 21,
788                expected_year: 2028,
789                expected_month: 1,
790                expected_day: 27,
791            },
792        ];
793
794        for case in cases {
795            let fixed = fixed_from_gregorian(
796                case.gregorian_year,
797                case.gregorian_month,
798                case.gregorian_day,
799            );
800            let seollal = seollal_on_or_before(fixed);
801            let (y, m, d) = gregorian_from_fixed(seollal).unwrap();
802            assert_eq!(
803                y, case.expected_year,
804                "Year check failed for case: {case:?}"
805            );
806            assert_eq!(
807                m, case.expected_month,
808                "Month check failed for case: {case:?}"
809            );
810            assert_eq!(d, case.expected_day, "Day check failed for case: {case:?}");
811        }
812    }
813}
814
815#[test]
816fn test_chinese_leap_months() {
817    let expected = [
818        (1933, 6),
819        (1938, 8),
820        (1984, 11),
821        (2009, 6),
822        (2017, 7),
823        (2028, 6),
824    ];
825
826    for (year, expected_month) in expected {
827        let bounds = YearBounds::compute::<Chinese>(fixed_from_gregorian(year, 6, 1));
828
829        assert!(bounds.is_leap(), "{year} should be a leap year");
830        assert_eq!(
831            expected_month,
832            get_leap_month_from_new_year::<Chinese>(bounds.new_year),
833            "{year} have leap month {expected_month}"
834        );
835    }
836}