calendrical_calculations/
chinese_based.rs

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