Skip to main content

icu_calendar/cal/east_asian_traditional/
simple.rs

1// This file is part of ICU4X. For terms of use, please see the file
2// called LICENSE at the top level of the ICU4X source tree
3// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ).
4
5use super::EastAsianTraditionalYearData;
6use calendrical_calculations::{gregorian::DAYS_IN_400_YEAR_CYCLE, rata_die::RataDie};
7
8macro_rules! day_fraction_to_ms {
9    ($n:tt $(/ $d:tt)+) => {{
10        Milliseconds((MILLISECONDS_IN_EPHEMERIS_DAY as i128 * $n as i128 $( / $d as i128)+) as i64)
11    }};
12    ($n:tt $(/ $d:tt)+, exact) => {{
13        let d = day_fraction_to_ms!($n $(/ $d)+);
14        assert!((d.0 as i128 $(* $d as i128)+) % MILLISECONDS_IN_EPHEMERIS_DAY as i128 == 0, "inexact");
15        d
16    }};
17}
18
19pub(super) const UTC_PLUS_8: Milliseconds = day_fraction_to_ms!(8 / 24);
20pub(super) const UTC_PLUS_9: Milliseconds = day_fraction_to_ms!(9 / 24);
21// Reference time was UTC+(1397/180)
22pub(super) const BEIJING_UTC_OFFSET: Milliseconds = day_fraction_to_ms!(1397 / 180 / 24);
23
24/// The mean year length according to the Gregorian solar cycle.
25const MEAN_GREGORIAN_YEAR_LENGTH: Milliseconds =
26    day_fraction_to_ms!(DAYS_IN_400_YEAR_CYCLE / 400, exact);
27
28/// The mean solar term length according to the Gregorian solar cycle
29const MEAN_GREGORIAN_SOLAR_TERM_LENGTH: Milliseconds =
30    day_fraction_to_ms!(DAYS_IN_400_YEAR_CYCLE / 400 / 12, exact);
31
32/// The mean synodic length on Jan 1 2000 according to the [Astronomical Almanac (1992)].
33///
34/// [Astronomical Almanac (1992)]: https://archive.org/details/131123ExplanatorySupplementAstronomicalAlmanac/page/n302/mode/1up
35const MEAN_SYNODIC_MONTH_LENGTH: Milliseconds = day_fraction_to_ms!(295305888531 / 10000000000i64);
36
37/// Number of milliseconds in a day.
38const MILLISECONDS_IN_EPHEMERIS_DAY: i64 = 24 * 60 * 60 * 1000;
39
40// 1999-12-22T07:44, https://aa.usno.navy.mil/calculated/seasons?year=2024&tz=0.00&tz_sign=-1&tz_label=false&dst=false
41const UTC_SOLSTICE: LocalMoment = LocalMoment {
42    rata_die: calendrical_calculations::gregorian::fixed_from_gregorian(1999, 12, 22),
43    local_milliseconds: ((7 * 60) + 44) * 60 * 1000,
44};
45
46// 2000-01-06T18:14 https://aa.usno.navy.mil/calculated/moon/phases?date=2000-01-01&nump=1&format=t
47const UTC_NEW_MOON: LocalMoment = LocalMoment {
48    rata_die: calendrical_calculations::gregorian::fixed_from_gregorian(2000, 1, 6),
49    local_milliseconds: ((18 * 60) + 14) * 60 * 1000,
50};
51
52#[derive(Debug, Copy, Clone, Default)]
53pub(super) struct Milliseconds(i64);
54
55#[derive(Debug, Copy, Clone)]
56struct LocalMoment {
57    rata_die: RataDie,
58    local_milliseconds: u32,
59}
60
61impl core::ops::Add<Milliseconds> for LocalMoment {
62    type Output = Self;
63
64    fn add(self, Milliseconds(duration): Milliseconds) -> Self::Output {
65        let temp = self.local_milliseconds as i64 + duration;
66        Self {
67            rata_die: self.rata_die + temp.div_euclid(MILLISECONDS_IN_EPHEMERIS_DAY),
68            local_milliseconds: temp.rem_euclid(MILLISECONDS_IN_EPHEMERIS_DAY) as u32,
69        }
70    }
71}
72
73impl super::EastAsianTraditionalYearData {
74    /// A fast approximation for the Chinese calendar, inspired by the _píngqì_ (平氣) rule
75    /// used in the Ming dynasty.
76    ///
77    /// Stays anchored in the Gregorian calendar, even as the Gregorian calendar drifts
78    /// from the seasons in the distant future and distant past.
79    pub(super) fn simple(
80        utc_offset: Milliseconds,
81        related_iso: i32,
82    ) -> EastAsianTraditionalYearData {
83        /// calculates the largest moment such that moment = base_moment + n * duration lands on rata_die (< rata_die + 1)
84        fn periodic_duration_on_or_before(
85            rata_die: RataDie,
86            base_moment: LocalMoment,
87            duration: Milliseconds,
88        ) -> LocalMoment {
89            let diff_millis = (rata_die - base_moment.rata_die) * MILLISECONDS_IN_EPHEMERIS_DAY
90                - base_moment.local_milliseconds as i64;
91
92            let num_periods =
93                (diff_millis + MILLISECONDS_IN_EPHEMERIS_DAY - 1).div_euclid(duration.0);
94
95            let millis = base_moment.rata_die.to_i64_date() * MILLISECONDS_IN_EPHEMERIS_DAY
96                + base_moment.local_milliseconds as i64
97                + num_periods * duration.0;
98
99            // Note: this is Euclidean div/rem, but this more optimized, because
100            // we know that our divisor is positive
101            let rata_die = millis / MILLISECONDS_IN_EPHEMERIS_DAY - (millis < 0) as i64;
102            let local_milliseconds = millis % MILLISECONDS_IN_EPHEMERIS_DAY
103                + (millis < 0) as i64 * MILLISECONDS_IN_EPHEMERIS_DAY;
104
105            LocalMoment {
106                rata_die: RataDie::new(rata_die),
107                local_milliseconds: local_milliseconds as u32,
108            }
109        }
110
111        let mut major_solar_term = periodic_duration_on_or_before(
112            calendrical_calculations::iso::day_before_year(related_iso),
113            UTC_SOLSTICE + utc_offset,
114            MEAN_GREGORIAN_YEAR_LENGTH,
115        );
116
117        let mut new_moon = periodic_duration_on_or_before(
118            major_solar_term.rata_die,
119            UTC_NEW_MOON + utc_offset,
120            MEAN_SYNODIC_MONTH_LENGTH,
121        );
122
123        let mut next_new_moon = new_moon + MEAN_SYNODIC_MONTH_LENGTH;
124
125        // The solstice is in the month of the 11th solar term of the previous year
126        let mut solar_term = -2;
127        let mut had_leap_in_sui = false;
128
129        // Skip the months before the year (M11, maybe M11L, M12, maybe M12L)
130        while solar_term < 0
131            || (next_new_moon.rata_die <= major_solar_term.rata_die && !had_leap_in_sui)
132        {
133            if next_new_moon.rata_die <= major_solar_term.rata_die && !had_leap_in_sui {
134                had_leap_in_sui = true;
135            } else {
136                solar_term += 1;
137                major_solar_term = major_solar_term + MEAN_GREGORIAN_SOLAR_TERM_LENGTH;
138            }
139
140            (new_moon, next_new_moon) = (next_new_moon, next_new_moon + MEAN_SYNODIC_MONTH_LENGTH);
141        }
142
143        debug_assert_eq!(solar_term, 0);
144
145        let start_day = new_moon.rata_die;
146        let mut month_lengths = [false; 13];
147        let mut leap_month = None;
148
149        // Iterate over the 12 solar terms, producing potentially 13 months
150        while solar_term < 12
151            || (next_new_moon.rata_die <= major_solar_term.rata_die && !had_leap_in_sui)
152        {
153            *month_lengths
154                .get_mut(solar_term as usize + leap_month.is_some() as usize)
155                .unwrap_or(&mut false) = next_new_moon.rata_die - new_moon.rata_die == 30;
156
157            if next_new_moon.rata_die <= major_solar_term.rata_die && !had_leap_in_sui {
158                had_leap_in_sui = true;
159                leap_month = Some(solar_term as u8 + 1);
160            } else {
161                solar_term += 1;
162                major_solar_term = major_solar_term + MEAN_GREGORIAN_SOLAR_TERM_LENGTH;
163            }
164
165            (new_moon, next_new_moon) = (next_new_moon, next_new_moon + MEAN_SYNODIC_MONTH_LENGTH);
166        }
167
168        debug_assert_eq!(solar_term, 12);
169
170        EastAsianTraditionalYearData::new(related_iso, start_day, month_lengths, leap_month)
171    }
172}
173
174#[test]
175fn bounds() {
176    EastAsianTraditionalYearData::simple(UTC_PLUS_9, 292_277_025);
177    assert!(
178        std::panic::catch_unwind(|| EastAsianTraditionalYearData::simple(UTC_PLUS_9, 292_277_026))
179            .is_err()
180    );
181
182    EastAsianTraditionalYearData::simple(BEIJING_UTC_OFFSET, -292_275_024);
183    assert!(
184        std::panic::catch_unwind(|| EastAsianTraditionalYearData::simple(
185            BEIJING_UTC_OFFSET,
186            -292_275_025
187        ))
188        .is_err()
189    );
190}