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