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
7// Different islamic calendars use different epochs (Thursday vs Friday) due to disagreement on the exact date of Mohammed's migration to Mecca.
8/// Lisp code reference: <https://github.com/EdReingold/calendar-code2/blob/main/calendar.l#L2066>
9const FIXED_ISLAMIC_EPOCH_FRIDAY: RataDie = crate::julian::fixed_from_julian(622, 7, 16);
10const FIXED_ISLAMIC_EPOCH_THURSDAY: RataDie = crate::julian::fixed_from_julian(622, 7, 15);
11
12/// Lisp code reference: <https://github.com/EdReingold/calendar-code2/blob/main/calendar.l#L6898>
13const CAIRO: Location = Location {
14    latitude: 30.1,
15    longitude: 31.3,
16    elevation: 200.0,
17    zone: (1_f64 / 12_f64),
18};
19
20/// Common abstraction over islamic-style calendars
21pub trait IslamicBasedMarker {
22    /// The epoch of the calendar. Different calendars use a different epoch (Thu or Fri) due to disagreement on the exact date of Mohammed's migration to Mecca.
23    const EPOCH: RataDie;
24    /// The name of the calendar for debugging.
25    const DEBUG_NAME: &'static str;
26    /// Whether this calendar is known to have 353-day years.
27    /// This is probably a bug; see <https://github.com/unicode-org/icu4x/issues/4930>
28    const HAS_353_DAY_YEARS: bool;
29    /// Given the extended year, calculate the approximate new year using the mean synodic month
30    fn mean_synodic_ny(extended_year: i32) -> RataDie {
31        Self::EPOCH + (f64::from((extended_year - 1) * 12) * MEAN_SYNODIC_MONTH).floor() as i64
32    }
33    /// Given an iso date, calculate the *approximate* islamic year it corresponds to (for quick cache lookup)
34    fn approximate_islamic_from_fixed(date: RataDie) -> i32 {
35        let diff = date - Self::EPOCH;
36        let months = diff as f64 / MEAN_SYNODIC_MONTH;
37        let years = months / 12.;
38        (years + 1.).floor() as i32
39    }
40    /// Convert an islamic date in this calendar to a R.D.
41    fn fixed_from_islamic(year: i32, month: u8, day: u8) -> RataDie;
42    /// Convert an R.D. To an islamic date in this calendar
43    fn islamic_from_fixed(date: RataDie) -> (i32, u8, u8);
44
45    /// Given an extended year, calculate whether each month is 29 or 30 days long
46    fn month_lengths_for_year(extended_year: i32, ny: RataDie) -> [bool; 12] {
47        let next_ny = Self::fixed_from_islamic(extended_year + 1, 1, 1);
48        match next_ny - ny {
49            355 | 354 => (),
50            353 if Self::HAS_353_DAY_YEARS => {
51                #[cfg(feature = "logging")]
52                log::trace!(
53                    "({}) Found year {extended_year} AH with length {}. See <https://github.com/unicode-org/icu4x/issues/4930>",
54                    Self::DEBUG_NAME,
55                    next_ny - ny
56                );
57            }
58            other => {
59                debug_assert!(
60                    false,
61                    "({}) Found year {extended_year} AH with length {}!",
62                    Self::DEBUG_NAME,
63                    other
64                )
65            }
66        }
67        let mut prev_rd = ny;
68        let mut excess_days = 0;
69        let mut lengths = core::array::from_fn(|month_idx| {
70            let month_idx = month_idx as u8;
71            let new_rd = if month_idx < 11 {
72                Self::fixed_from_islamic(extended_year, month_idx + 2, 1)
73            } else {
74                next_ny
75            };
76            let diff = new_rd - prev_rd;
77            prev_rd = new_rd;
78            match diff {
79                29 => false,
80                30 => true,
81                31 => {
82                    #[cfg(feature = "logging")]
83                    log::trace!(
84                        "({}) Found year {extended_year} AH with month length {diff} for month {}.",
85                        Self::DEBUG_NAME,
86                        month_idx + 1
87                    );
88                    excess_days += 1;
89                    true
90                }
91                _ => {
92                    debug_assert!(
93                        false,
94                        "({}) Found year {extended_year} AH with month length {diff} for month {}!",
95                        Self::DEBUG_NAME,
96                        month_idx + 1
97                    );
98                    false
99                }
100            }
101        });
102        // To maintain invariants for calendar arithmetic, if astronomy finds
103        // a 31-day month, "move" the day to the first 29-day month in the
104        // same year to maintain all months at 29 or 30 days.
105        if excess_days != 0 {
106            debug_assert_eq!(
107                excess_days,
108                1,
109                "({}) Found year {extended_year} AH with more than one excess day!",
110                Self::DEBUG_NAME
111            );
112            if let Some(l) = lengths.iter_mut().find(|l| !(**l)) {
113                *l = true;
114            }
115        }
116        lengths
117    }
118}
119
120/// Marker type for observational islamic calendar, for use with [`IslamicBasedMarker`]
121#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq)]
122#[allow(clippy::exhaustive_structs)] // marker
123pub struct ObservationalIslamicMarker;
124
125/// Marker type for Saudi islamic calendar, for use with [`IslamicBasedMarker`]
126#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq)]
127#[allow(clippy::exhaustive_structs)] // marker
128pub struct SaudiIslamicMarker;
129
130/// Marker type for civil islamic calendar, for use with [`IslamicBasedMarker`]
131#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq)]
132#[allow(clippy::exhaustive_structs)] // marker
133pub struct CivilIslamicMarker;
134
135/// Marker type for observational islamic calendar, for use with [`IslamicBasedMarker`]
136#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq)]
137#[allow(clippy::exhaustive_structs)] // marker
138pub struct TabularIslamicMarker;
139
140impl IslamicBasedMarker for ObservationalIslamicMarker {
141    const EPOCH: RataDie = FIXED_ISLAMIC_EPOCH_FRIDAY;
142    const DEBUG_NAME: &'static str = "ObservationalIslamic";
143    const HAS_353_DAY_YEARS: bool = true;
144    fn fixed_from_islamic(year: i32, month: u8, day: u8) -> RataDie {
145        fixed_from_islamic_observational(year, month, day)
146    }
147    fn islamic_from_fixed(date: RataDie) -> (i32, u8, u8) {
148        observational_islamic_from_fixed(date)
149    }
150}
151
152impl IslamicBasedMarker for SaudiIslamicMarker {
153    const EPOCH: RataDie = FIXED_ISLAMIC_EPOCH_FRIDAY;
154    const DEBUG_NAME: &'static str = "SaudiIslamic";
155    const HAS_353_DAY_YEARS: bool = true;
156    fn fixed_from_islamic(year: i32, month: u8, day: u8) -> RataDie {
157        fixed_from_saudi_islamic(year, month, day)
158    }
159    fn islamic_from_fixed(date: RataDie) -> (i32, u8, u8) {
160        saudi_islamic_from_fixed(date)
161    }
162}
163
164impl IslamicBasedMarker for CivilIslamicMarker {
165    const EPOCH: RataDie = FIXED_ISLAMIC_EPOCH_FRIDAY;
166    const DEBUG_NAME: &'static str = "CivilIslamic";
167    const HAS_353_DAY_YEARS: bool = false;
168    fn fixed_from_islamic(year: i32, month: u8, day: u8) -> RataDie {
169        fixed_from_islamic_civil(year, month, day)
170    }
171    fn islamic_from_fixed(date: RataDie) -> (i32, u8, u8) {
172        islamic_civil_from_fixed(date)
173    }
174}
175
176impl IslamicBasedMarker for TabularIslamicMarker {
177    const EPOCH: RataDie = FIXED_ISLAMIC_EPOCH_THURSDAY;
178    const DEBUG_NAME: &'static str = "TabularIslamic";
179    const HAS_353_DAY_YEARS: bool = false;
180    fn fixed_from_islamic(year: i32, month: u8, day: u8) -> RataDie {
181        fixed_from_islamic_tabular(year, month, day)
182    }
183    fn islamic_from_fixed(date: RataDie) -> (i32, u8, u8) {
184        islamic_tabular_from_fixed(date)
185    }
186}
187
188/// Lisp code reference: <https://github.com/EdReingold/calendar-code2/blob/main/calendar.l#L6904>
189pub fn fixed_from_islamic_observational(year: i32, month: u8, day: u8) -> RataDie {
190    let year = i64::from(year);
191    let month = i64::from(month);
192    let day = i64::from(day);
193    let midmonth = FIXED_ISLAMIC_EPOCH_FRIDAY.to_f64_date()
194        + (((year - 1) as f64) * 12.0 + month as f64 - 0.5) * MEAN_SYNODIC_MONTH;
195    let lunar_phase = Astronomical::calculate_new_moon_at_or_before(RataDie::new(midmonth as i64));
196    Astronomical::phasis_on_or_before(RataDie::new(midmonth as i64), CAIRO, Some(lunar_phase)) + day
197        - 1
198}
199
200/// Lisp code reference: <https://github.com/EdReingold/calendar-code2/blob/1ee51ecfaae6f856b0d7de3e36e9042100b4f424/calendar.l#L6983-L6995>
201pub fn observational_islamic_from_fixed(date: RataDie) -> (i32, u8, u8) {
202    let lunar_phase = Astronomical::calculate_new_moon_at_or_before(date);
203    let crescent = Astronomical::phasis_on_or_before(date, CAIRO, Some(lunar_phase));
204    let elapsed_months =
205        ((crescent - FIXED_ISLAMIC_EPOCH_FRIDAY) as f64 / MEAN_SYNODIC_MONTH).round() as i32;
206    let year = elapsed_months.div_euclid(12) + 1;
207    let month = elapsed_months.rem_euclid(12) + 1;
208    let day = (date - crescent + 1) as u8;
209
210    (year, month as u8, day)
211}
212
213// Saudi visibility criterion on eve of fixed date in Mecca.
214// The start of the new month only happens if both of these criterias are met: The moon is a waxing crescent at sunset of the previous day
215// and the moon sets after the sun on that same evening.
216/// Lisp code reference: <https://github.com/EdReingold/calendar-code2/blob/main/calendar.l#L6957>
217fn saudi_criterion(date: RataDie) -> Option<bool> {
218    let sunset = Astronomical::sunset((date - 1).as_moment(), MECCA)?;
219    let tee = Location::universal_from_standard(sunset, MECCA);
220    let phase = Astronomical::lunar_phase(tee, Astronomical::julian_centuries(tee));
221    let moonlag = Astronomical::moonlag((date - 1).as_moment(), MECCA)?;
222
223    Some(phase > 0.0 && phase < 90.0 && moonlag > 0.0)
224}
225
226pub(crate) fn adjusted_saudi_criterion(date: RataDie) -> bool {
227    saudi_criterion(date).unwrap_or_default()
228}
229
230// Closest fixed date on or before date when Saudi visibility criterion is held.
231/// Lisp code reference: <https://github.com/EdReingold/calendar-code2/blob/main/calendar.l#L6966>
232pub fn saudi_new_month_on_or_before(date: RataDie) -> RataDie {
233    let last_new_moon = (Astronomical::lunar_phase_at_or_before(0.0, date.as_moment()))
234        .inner()
235        .floor(); // Gets the R.D Date of the prior new moon
236    let age = date.to_f64_date() - last_new_moon;
237    // Explanation of why the value 3.0 is chosen: https://github.com/unicode-org/icu4x/pull/3673/files#r1267460916
238    let tau = if age <= 3.0 && !adjusted_saudi_criterion(date) {
239        // Checks if the criterion is not yet visible on the evening of date
240        last_new_moon - 30.0 // Goes back a month
241    } else {
242        last_new_moon
243    };
244
245    next(RataDie::new(tau as i64), adjusted_saudi_criterion) // Loop that increments the day and checks if the criterion is now visible
246}
247
248/// Lisp code reference: <https://github.com/EdReingold/calendar-code2/blob/main/calendar.l#L6996>
249pub fn saudi_islamic_from_fixed(date: RataDie) -> (i32, u8, u8) {
250    let crescent = saudi_new_month_on_or_before(date);
251    let elapsed_months =
252        ((crescent - FIXED_ISLAMIC_EPOCH_FRIDAY) as f64 / MEAN_SYNODIC_MONTH).round() as i64;
253    let year = i64_to_saturated_i32(elapsed_months.div_euclid(12) + 1);
254    let month = (elapsed_months.rem_euclid(12) + 1) as u8;
255    let day = ((date - crescent) + 1) as u8;
256
257    (year, month, day)
258}
259
260/// Lisp code reference: <https://github.com/EdReingold/calendar-code2/blob/main/calendar.l#L6981>
261pub fn fixed_from_saudi_islamic(year: i32, month: u8, day: u8) -> RataDie {
262    let midmonth = RataDie::new(
263        FIXED_ISLAMIC_EPOCH_FRIDAY.to_i64_date()
264            + (((year as f64 - 1.0) * 12.0 + month as f64 - 0.5) * MEAN_SYNODIC_MONTH).floor()
265                as i64,
266    );
267    let first_day_of_month = saudi_new_month_on_or_before(midmonth).to_i64_date();
268
269    RataDie::new(first_day_of_month + day as i64 - 1)
270}
271
272/// Lisp code reference: <https://github.com/EdReingold/calendar-code2/blob/main/calendar.l#L2076>
273pub fn fixed_from_islamic_civil(year: i32, month: u8, day: u8) -> RataDie {
274    let year = i64::from(year);
275    let month = i64::from(month);
276    let day = i64::from(day);
277
278    RataDie::new(
279        (FIXED_ISLAMIC_EPOCH_FRIDAY.to_i64_date() - 1)
280            + (year - 1) * 354
281            + (3 + year * 11).div_euclid(30)
282            + 29 * (month - 1)
283            + month.div_euclid(2)
284            + day,
285    )
286}
287/// Lisp code reference: <https://github.com/EdReingold/calendar-code2/blob/main/calendar.l#L2090>
288pub fn islamic_civil_from_fixed(date: RataDie) -> (i32, u8, u8) {
289    let year =
290        i64_to_saturated_i32(((date - FIXED_ISLAMIC_EPOCH_FRIDAY) * 30 + 10646).div_euclid(10631));
291    let prior_days = date.to_f64_date() - fixed_from_islamic_civil(year, 1, 1).to_f64_date();
292    debug_assert!(prior_days >= 0.0);
293    debug_assert!(prior_days <= 354.);
294    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
295    debug_assert!(month <= 12);
296    let day =
297        (date.to_f64_date() - fixed_from_islamic_civil(year, month, 1).to_f64_date() + 1.0) as u8; // The value will always be number between 1-30 because of the difference between the date and lunar ordinals function.
298
299    (year, month, day)
300}
301
302/// Lisp code reference:https: //github.com/EdReingold/calendar-code2/blob/main/calendar.l#L2076
303pub fn fixed_from_islamic_tabular(year: i32, month: u8, day: u8) -> RataDie {
304    let year = i64::from(year);
305    let month = i64::from(month);
306    let day = i64::from(day);
307    RataDie::new(
308        (FIXED_ISLAMIC_EPOCH_THURSDAY.to_i64_date() - 1)
309            + (year - 1) * 354
310            + (3 + year * 11).div_euclid(30)
311            + 29 * (month - 1)
312            + month.div_euclid(2)
313            + day,
314    )
315}
316/// Lisp code reference: <https://github.com/EdReingold/calendar-code2/blob/main/calendar.l#L2090>
317pub fn islamic_tabular_from_fixed(date: RataDie) -> (i32, u8, u8) {
318    let year = i64_to_saturated_i32(
319        ((date - FIXED_ISLAMIC_EPOCH_THURSDAY) * 30 + 10646).div_euclid(10631),
320    );
321    let prior_days = date.to_f64_date() - fixed_from_islamic_tabular(year, 1, 1).to_f64_date();
322    debug_assert!(prior_days >= 0.0);
323    debug_assert!(prior_days <= 354.);
324    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
325    debug_assert!(month <= 12);
326    let day =
327        (date.to_f64_date() - fixed_from_islamic_tabular(year, month, 1).to_f64_date() + 1.0) as u8; // The value will always be number between 1-30 because of the difference between the date and lunar ordinals function.
328
329    (year, month, day)
330}
331
332/// The number of days in a month for the observational islamic calendar
333pub fn observational_islamic_month_days(year: i32, month: u8) -> u8 {
334    let midmonth = FIXED_ISLAMIC_EPOCH_FRIDAY.to_f64_date()
335        + (((year - 1) as f64) * 12.0 + month as f64 - 0.5) * MEAN_SYNODIC_MONTH;
336
337    let lunar_phase: f64 =
338        Astronomical::calculate_new_moon_at_or_before(RataDie::new(midmonth as i64));
339    let f_date =
340        Astronomical::phasis_on_or_before(RataDie::new(midmonth as i64), CAIRO, Some(lunar_phase));
341
342    Astronomical::month_length(f_date, CAIRO)
343}
344
345/// The number of days in a month for the Saudi (Umm Al-Qura) calendar
346pub fn saudi_islamic_month_days(year: i32, month: u8) -> u8 {
347    // We cannot use month_days from the book here, that is for the observational calendar
348    //
349    // Instead we subtract the two new months calculated using the saudi criterion
350    let midmonth = Moment::new(
351        FIXED_ISLAMIC_EPOCH_FRIDAY.to_f64_date()
352            + (((year - 1) as f64) * 12.0 + month as f64 - 0.5) * MEAN_SYNODIC_MONTH,
353    );
354    let midmonth_next = midmonth + MEAN_SYNODIC_MONTH;
355
356    let month_start = saudi_new_month_on_or_before(midmonth.as_rata_die());
357    let next_month_start = saudi_new_month_on_or_before(midmonth_next.as_rata_die());
358
359    let diff = next_month_start - month_start;
360    debug_assert!(
361        diff <= 30,
362        "umm-al-qura months must not be more than 30 days"
363    );
364    u8::try_from(diff).unwrap_or(30)
365}
366
367#[cfg(test)]
368mod tests {
369    use super::*;
370
371    static TEST_FIXED_DATE: [i64; 33] = [
372        -214193, -61387, 25469, 49217, 171307, 210155, 253427, 369740, 400085, 434355, 452605,
373        470160, 473837, 507850, 524156, 544676, 567118, 569477, 601716, 613424, 626596, 645554,
374        664224, 671401, 694799, 704424, 708842, 709409, 709580, 727274, 728714, 744313, 764652,
375    ];
376    // Removed: 601716 and 727274 fixed dates
377    static TEST_FIXED_DATE_UMMALQURA: [i64; 31] = [
378        -214193, -61387, 25469, 49217, 171307, 210155, 253427, 369740, 400085, 434355, 452605,
379        470160, 473837, 507850, 524156, 544676, 567118, 569477, 613424, 626596, 645554, 664224,
380        671401, 694799, 704424, 708842, 709409, 709580, 728714, 744313, 764652,
381    ];
382    // Values from lisp code
383    static SAUDI_CRITERION_EXPECTED: [bool; 33] = [
384        false, false, true, false, false, true, false, true, false, false, true, false, false,
385        true, true, true, true, false, false, true, true, true, false, false, false, false, false,
386        false, true, false, true, false, true,
387    ];
388    // Values from lisp code, removed two expected months.
389    static SAUDI_NEW_MONTH_OR_BEFORE_EXPECTED: [f64; 31] = [
390        -214203.0, -61412.0, 25467.0, 49210.0, 171290.0, 210152.0, 253414.0, 369735.0, 400063.0,
391        434348.0, 452598.0, 470139.0, 473830.0, 507850.0, 524150.0, 544674.0, 567118.0, 569450.0,
392        613421.0, 626592.0, 645551.0, 664214.0, 671391.0, 694779.0, 704405.0, 708835.0, 709396.0,
393        709573.0, 728709.0, 744301.0, 764647.0,
394    ];
395    #[test]
396    fn test_islamic_epoch_friday() {
397        let epoch = FIXED_ISLAMIC_EPOCH_FRIDAY.to_i64_date();
398        // Iso year of Islamic Epoch
399        let epoch_year_from_fixed = crate::iso::iso_year_from_fixed(RataDie::new(epoch));
400        // 622 is the correct ISO year for the Islamic Epoch
401        assert_eq!(epoch_year_from_fixed, 622);
402    }
403
404    #[test]
405    fn test_islamic_epoch_thursday() {
406        let epoch = FIXED_ISLAMIC_EPOCH_THURSDAY.to_i64_date();
407        // Iso year of Islamic Epoch
408        let epoch_year_from_fixed = crate::iso::iso_year_from_fixed(RataDie::new(epoch));
409        // 622 is the correct ISO year for the Islamic Epoch
410        assert_eq!(epoch_year_from_fixed, 622);
411    }
412
413    #[test]
414    fn test_saudi_criterion() {
415        for (boolean, f_date) in SAUDI_CRITERION_EXPECTED.iter().zip(TEST_FIXED_DATE.iter()) {
416            let bool_result = saudi_criterion(RataDie::new(*f_date)).unwrap();
417            assert_eq!(*boolean, bool_result, "{f_date:?}");
418        }
419    }
420
421    #[test]
422    fn test_saudi_new_month_or_before() {
423        for (date, f_date) in SAUDI_NEW_MONTH_OR_BEFORE_EXPECTED
424            .iter()
425            .zip(TEST_FIXED_DATE_UMMALQURA.iter())
426        {
427            let date_result = saudi_new_month_on_or_before(RataDie::new(*f_date)).to_f64_date();
428            assert_eq!(*date, date_result, "{f_date:?}");
429        }
430    }
431}