calendrical_calculations/
islamic.rs

1use crate::astronomy::*;
2use crate::helpers::{i64_to_saturated_i32, next};
3use crate::rata_die::{Moment, RataDie};
4#[allow(unused_imports)]
5use core_maths::*;
6
7pub use crate::astronomy::Location;
8
9/// The average length of an Islamic year, equal to 12 moon cycles
10pub const MEAN_YEAR_LENGTH: f64 = MEAN_SYNODIC_MONTH * 12.;
11
12/// Different islamic calendars use different epochs (Thursday vs Friday) due to disagreement on the exact date of Mohammed's migration to Mecca.
13/// Lisp code reference: <https://github.com/EdReingold/calendar-code2/blob/main/calendar.l#L2066>
14pub const ISLAMIC_EPOCH_FRIDAY: RataDie = crate::julian::fixed_from_julian(622, 7, 16);
15
16/// Different islamic calendars use different epochs (Thursday vs Friday) due to disagreement on the exact date of Mohammed's migration to Mecca.
17/// Lisp code reference: <https://github.com/EdReingold/calendar-code2/blob/main/calendar.l#L2066>
18pub const ISLAMIC_EPOCH_THURSDAY: RataDie = crate::julian::fixed_from_julian(622, 7, 15);
19
20// Inline to copy over docs. This can be made into a separate value as per need.
21#[doc(inline)]
22pub use crate::chinese_based::WELL_BEHAVED_ASTRONOMICAL_RANGE;
23
24/// Lisp code reference: <https://github.com/EdReingold/calendar-code2/blob/main/calendar.l#L6898>
25pub const CAIRO: Location = Location {
26    latitude: 30.1,
27    longitude: 31.3,
28    elevation: 200.0,
29    utc_offset: (1_f64 / 12_f64),
30};
31
32/// The location of Mecca; used for Islamic calendar calculations.
33pub const MECCA: Location = Location {
34    latitude: 6427.0 / 300.0,
35    longitude: 11947.0 / 300.0,
36    elevation: 298.0,
37    utc_offset: (1_f64 / 8_f64),
38};
39
40/// Lisp code reference: <https://github.com/EdReingold/calendar-code2/blob/main/calendar.l#L6904>
41pub fn fixed_from_observational_islamic(
42    year: i32,
43    month: u8,
44    day: u8,
45    location: Location,
46) -> RataDie {
47    let year = i64::from(year);
48    let month = i64::from(month);
49    let day = i64::from(day);
50    let midmonth = ISLAMIC_EPOCH_FRIDAY.to_f64_date()
51        + (((year - 1) as f64) * 12.0 + month as f64 - 0.5) * MEAN_SYNODIC_MONTH;
52    let lunar_phase = Astronomical::calculate_new_moon_at_or_before(RataDie::new(midmonth as i64));
53    Astronomical::phasis_on_or_before(RataDie::new(midmonth as i64), location, Some(lunar_phase))
54        + day
55        - 1
56}
57
58/// Lisp code reference: <https://github.com/EdReingold/calendar-code2/blob/1ee51ecfaae6f856b0d7de3e36e9042100b4f424/calendar.l#L6983-L6995>
59pub fn observational_islamic_from_fixed(date: RataDie, location: Location) -> (i32, u8, u8) {
60    let lunar_phase = Astronomical::calculate_new_moon_at_or_before(date);
61    let crescent = Astronomical::phasis_on_or_before(date, location, Some(lunar_phase));
62    let elapsed_months =
63        ((crescent - ISLAMIC_EPOCH_FRIDAY) as f64 / MEAN_SYNODIC_MONTH).round() as i32;
64    let year = elapsed_months.div_euclid(12) + 1;
65    let month = elapsed_months.rem_euclid(12) + 1;
66    let day = (date - crescent + 1) as u8;
67
68    (year, month as u8, day)
69}
70
71// Saudi visibility criterion on eve of fixed date in Mecca.
72// The start of the new month only happens if both of these criteria are met: The moon is a waxing crescent at sunset of the previous day
73// and the moon sets after the sun on that same evening.
74/// Lisp code reference: <https://github.com/EdReingold/calendar-code2/blob/main/calendar.l#L6957>
75fn saudi_criterion(date: RataDie) -> Option<bool> {
76    let sunset = Astronomical::sunset((date - 1).as_moment(), MECCA)?;
77    let tee = Location::universal_from_standard(sunset, MECCA);
78    let phase = Astronomical::lunar_phase(tee, Astronomical::julian_centuries(tee));
79    let moonlag = Astronomical::moonlag((date - 1).as_moment(), MECCA)?;
80
81    Some(phase > 0.0 && phase < 90.0 && moonlag > 0.0)
82}
83
84fn adjusted_saudi_criterion(date: RataDie) -> bool {
85    saudi_criterion(date).unwrap_or_default()
86}
87
88// Closest fixed date on or before date when Saudi visibility criterion is held.
89/// Lisp code reference: <https://github.com/EdReingold/calendar-code2/blob/main/calendar.l#L6966>
90pub fn saudi_new_month_on_or_before(date: RataDie) -> RataDie {
91    let last_new_moon = (Astronomical::lunar_phase_at_or_before(0.0, date.as_moment()))
92        .inner()
93        .floor(); // Gets the R.D Date of the prior new moon
94    let age = date.to_f64_date() - last_new_moon;
95    // Explanation of why the value 3.0 is chosen: https://github.com/unicode-org/icu4x/pull/3673/files#r1267460916
96    let tau = if age <= 3.0 && !adjusted_saudi_criterion(date) {
97        // Checks if the criterion is not yet visible on the evening of date
98        last_new_moon - 30.0 // Goes back a month
99    } else {
100        last_new_moon
101    };
102
103    next(RataDie::new(tau as i64), adjusted_saudi_criterion) // Loop that increments the day and checks if the criterion is now visible
104}
105
106/// Lisp code reference: <https://github.com/EdReingold/calendar-code2/blob/main/calendar.l#L6996>
107pub fn saudi_islamic_from_fixed(date: RataDie) -> (i32, u8, u8) {
108    let crescent = saudi_new_month_on_or_before(date);
109    let elapsed_months =
110        ((crescent - ISLAMIC_EPOCH_FRIDAY) as f64 / MEAN_SYNODIC_MONTH).round() as i64;
111    let year = i64_to_saturated_i32(elapsed_months.div_euclid(12) + 1);
112    let month = (elapsed_months.rem_euclid(12) + 1) as u8;
113    let day = ((date - crescent) + 1) as u8;
114
115    (year, month, day)
116}
117
118/// Lisp code reference: <https://github.com/EdReingold/calendar-code2/blob/main/calendar.l#L6981>
119pub fn fixed_from_saudi_islamic(year: i32, month: u8, day: u8) -> RataDie {
120    let midmonth = RataDie::new(
121        ISLAMIC_EPOCH_FRIDAY.to_i64_date()
122            + (((year as f64 - 1.0) * 12.0 + month as f64 - 0.5) * MEAN_SYNODIC_MONTH).floor()
123                as i64,
124    );
125    let first_day_of_month = saudi_new_month_on_or_before(midmonth).to_i64_date();
126
127    RataDie::new(first_day_of_month + day as i64 - 1)
128}
129
130/// Lisp code reference: <https://github.com/EdReingold/calendar-code2/blob/main/calendar.l#L2076>
131pub fn fixed_from_tabular_islamic(year: i32, month: u8, day: u8, epoch: RataDie) -> RataDie {
132    let year = i64::from(year);
133    let month = i64::from(month);
134    let day = i64::from(day);
135
136    RataDie::new(
137        (epoch.to_i64_date() - 1)
138            + (year - 1) * 354
139            + (3 + year * 11).div_euclid(30)
140            + 29 * (month - 1)
141            + month.div_euclid(2)
142            + day,
143    )
144}
145/// Lisp code reference: <https://github.com/EdReingold/calendar-code2/blob/main/calendar.l#L2090>
146pub fn tabular_islamic_from_fixed(date: RataDie, epoch: RataDie) -> (i32, u8, u8) {
147    let year = i64_to_saturated_i32(((date - epoch) * 30 + 10646).div_euclid(10631));
148    let prior_days =
149        date.to_f64_date() - fixed_from_tabular_islamic(year, 1, 1, epoch).to_f64_date();
150    debug_assert!(prior_days >= 0.0);
151    debug_assert!(prior_days <= 354.);
152    let month = (((prior_days * 11.0) + 330.0) / 325.0) as u8; // Prior days is maximum 354 (when year length is 355), making the value always less than 12
153    debug_assert!(month <= 12);
154    let day = (date.to_f64_date() - fixed_from_tabular_islamic(year, month, 1, epoch).to_f64_date()
155        + 1.0) as u8; // The value will always be number between 1-30 because of the difference between the date and lunar ordinals function.
156
157    (year, month, day)
158}
159
160/// The number of days in a month for the observational islamic calendar
161pub fn observational_islamic_month_days(year: i32, month: u8, location: Location) -> u8 {
162    let midmonth = ISLAMIC_EPOCH_FRIDAY.to_f64_date()
163        + (((year - 1) as f64) * 12.0 + month as f64 - 0.5) * MEAN_SYNODIC_MONTH;
164
165    let lunar_phase: f64 =
166        Astronomical::calculate_new_moon_at_or_before(RataDie::new(midmonth as i64));
167    let f_date = Astronomical::phasis_on_or_before(
168        RataDie::new(midmonth as i64),
169        location,
170        Some(lunar_phase),
171    );
172
173    Astronomical::month_length(f_date, location)
174}
175
176/// The number of days in a month for the Saudi (Umm Al-Qura) calendar
177pub fn saudi_islamic_month_days(year: i32, month: u8) -> u8 {
178    // We cannot use month_days from the book here, that is for the observational calendar
179    //
180    // Instead we subtract the two new months calculated using the saudi criterion
181    let midmonth = Moment::new(
182        ISLAMIC_EPOCH_FRIDAY.to_f64_date()
183            + (((year - 1) as f64) * 12.0 + month as f64 - 0.5) * MEAN_SYNODIC_MONTH,
184    );
185    let midmonth_next = midmonth + MEAN_SYNODIC_MONTH;
186
187    let month_start = saudi_new_month_on_or_before(midmonth.as_rata_die());
188    let next_month_start = saudi_new_month_on_or_before(midmonth_next.as_rata_die());
189
190    let diff = next_month_start - month_start;
191    debug_assert!(
192        diff <= 30 || !WELL_BEHAVED_ASTRONOMICAL_RANGE.contains(&month_start),
193        "umm-al-qura months must not be more than 30 days"
194    );
195    u8::try_from(diff).unwrap_or(30)
196}
197
198#[cfg(test)]
199mod tests {
200    use super::*;
201
202    static TEST_FIXED_DATE: [i64; 33] = [
203        -214193, -61387, 25469, 49217, 171307, 210155, 253427, 369740, 400085, 434355, 452605,
204        470160, 473837, 507850, 524156, 544676, 567118, 569477, 601716, 613424, 626596, 645554,
205        664224, 671401, 694799, 704424, 708842, 709409, 709580, 727274, 728714, 744313, 764652,
206    ];
207    // Removed: 601716 and 727274 fixed dates
208    static TEST_FIXED_DATE_UMMALQURA: [i64; 31] = [
209        -214193, -61387, 25469, 49217, 171307, 210155, 253427, 369740, 400085, 434355, 452605,
210        470160, 473837, 507850, 524156, 544676, 567118, 569477, 613424, 626596, 645554, 664224,
211        671401, 694799, 704424, 708842, 709409, 709580, 728714, 744313, 764652,
212    ];
213    // Values from lisp code
214    static SAUDI_CRITERION_EXPECTED: [bool; 33] = [
215        false, false, true, false, false, true, false, true, false, false, true, false, false,
216        true, true, true, true, false, false, true, true, true, false, false, false, false, false,
217        false, true, false, true, false, true,
218    ];
219    // Values from lisp code, removed two expected months.
220    static SAUDI_NEW_MONTH_OR_BEFORE_EXPECTED: [f64; 31] = [
221        -214203.0, -61412.0, 25467.0, 49210.0, 171290.0, 210152.0, 253414.0, 369735.0, 400063.0,
222        434348.0, 452598.0, 470139.0, 473830.0, 507850.0, 524150.0, 544674.0, 567118.0, 569450.0,
223        613421.0, 626592.0, 645551.0, 664214.0, 671391.0, 694779.0, 704405.0, 708835.0, 709396.0,
224        709573.0, 728709.0, 744301.0, 764647.0,
225    ];
226    #[test]
227    fn test_islamic_epoch_friday() {
228        let epoch = ISLAMIC_EPOCH_FRIDAY.to_i64_date();
229        // Proleptic Gregorian year of Islamic Epoch
230        let epoch_year_from_fixed = crate::gregorian::year_from_fixed(RataDie::new(epoch)).unwrap();
231        // 622 is the correct proleptic Gregorian year for the Islamic Epoch
232        assert_eq!(epoch_year_from_fixed, 622);
233    }
234
235    #[test]
236    fn test_islamic_epoch_thursday() {
237        let epoch = ISLAMIC_EPOCH_THURSDAY.to_i64_date();
238        // Proleptic Gregorian year of Islamic Epoch
239        let epoch_year_from_fixed = crate::gregorian::year_from_fixed(RataDie::new(epoch)).unwrap();
240        // 622 is the correct proleptic Gregorian year for the Islamic Epoch
241        assert_eq!(epoch_year_from_fixed, 622);
242    }
243
244    #[test]
245    fn test_saudi_criterion() {
246        for (boolean, f_date) in SAUDI_CRITERION_EXPECTED.iter().zip(TEST_FIXED_DATE.iter()) {
247            let bool_result = saudi_criterion(RataDie::new(*f_date)).unwrap();
248            assert_eq!(*boolean, bool_result, "{f_date:?}");
249        }
250    }
251
252    #[test]
253    fn test_saudi_new_month_or_before() {
254        for (date, f_date) in SAUDI_NEW_MONTH_OR_BEFORE_EXPECTED
255            .iter()
256            .zip(TEST_FIXED_DATE_UMMALQURA.iter())
257        {
258            let date_result = saudi_new_month_on_or_before(RataDie::new(*f_date)).to_f64_date();
259            assert_eq!(*date, date_result, "{f_date:?}");
260        }
261    }
262}