Skip to main content

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/// Calculates an Islamic date from a [`RataDie`] and [`Location`].
59///
60/// This uses the phasis criterion proposed by S. K. Shaukat[^1], explained in Reingold section 14.9.
61///
62/// Lisp code reference: <https://github.com/EdReingold/calendar-code2/blob/1ee51ecfaae6f856b0d7de3e36e9042100b4f424/calendar.l#L6983-L6995>
63///
64/// [^1]: K. Abdali, O. Afzal, I. A. Ahmad, M. Durrani, A. Salama, and S. K. Shaukat, “Crescent Moon Visibility: Consensus on Moon-Sighting and Determination of an Islamic Calendar,” manuscript, 1996.
65pub fn observational_islamic_from_fixed(date: RataDie, location: Location) -> (i32, u8, u8) {
66    let lunar_phase = Astronomical::calculate_new_moon_at_or_before(date);
67    let crescent = Astronomical::phasis_on_or_before(date, location, Some(lunar_phase));
68    let elapsed_months =
69        ((crescent - ISLAMIC_EPOCH_FRIDAY) as f64 / MEAN_SYNODIC_MONTH).round() as i32;
70    let year = elapsed_months.div_euclid(12) + 1;
71    let month = elapsed_months.rem_euclid(12) + 1;
72    let day = (date - crescent + 1) as u8;
73
74    (year, month as u8, day)
75}
76
77// Saudi visibility criterion on eve of fixed date in Mecca.
78// 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
79// and the moon sets after the sun on that same evening.
80/// Lisp code reference: <https://github.com/EdReingold/calendar-code2/blob/main/calendar.l#L6957>
81fn saudi_criterion(date: RataDie) -> Option<bool> {
82    let sunset = Astronomical::sunset((date - 1).as_moment(), MECCA)?;
83    let tee = Location::universal_from_standard(sunset, MECCA);
84    let phase = Astronomical::lunar_phase(tee, Astronomical::julian_centuries(tee));
85    let moonlag = Astronomical::moonlag((date - 1).as_moment(), MECCA)?;
86
87    Some(phase > 0.0 && phase < 90.0 && moonlag > 0.0)
88}
89
90fn adjusted_saudi_criterion(date: RataDie) -> bool {
91    saudi_criterion(date).unwrap_or_default()
92}
93
94// Closest fixed date on or before date when Saudi visibility criterion is held.
95/// Lisp code reference: <https://github.com/EdReingold/calendar-code2/blob/main/calendar.l#L6966>
96pub fn saudi_new_month_on_or_before(date: RataDie) -> RataDie {
97    let last_new_moon = (Astronomical::lunar_phase_at_or_before(0.0, date.as_moment()))
98        .inner()
99        .floor(); // Gets the R.D Date of the prior new moon
100    let age = date.to_f64_date() - last_new_moon;
101    // Explanation of why the value 3.0 is chosen: https://github.com/unicode-org/icu4x/pull/3673/files#r1267460916
102    let tau = if age <= 3.0 && !adjusted_saudi_criterion(date) {
103        // Checks if the criterion is not yet visible on the evening of date
104        last_new_moon - 30.0 // Goes back a month
105    } else {
106        last_new_moon
107    };
108
109    next(RataDie::new(tau as i64), adjusted_saudi_criterion) // Loop that increments the day and checks if the criterion is now visible
110}
111
112/// Lisp code reference: <https://github.com/EdReingold/calendar-code2/blob/main/calendar.l#L6996>
113pub fn saudi_islamic_from_fixed(date: RataDie) -> (i32, u8, u8) {
114    let crescent = saudi_new_month_on_or_before(date);
115    let elapsed_months =
116        ((crescent - ISLAMIC_EPOCH_FRIDAY) as f64 / MEAN_SYNODIC_MONTH).round() as i64;
117    let year = i64_to_saturated_i32(elapsed_months.div_euclid(12) + 1);
118    let month = (elapsed_months.rem_euclid(12) + 1) as u8;
119    let day = ((date - crescent) + 1) as u8;
120
121    (year, month, day)
122}
123
124/// Lisp code reference: <https://github.com/EdReingold/calendar-code2/blob/main/calendar.l#L6981>
125pub fn fixed_from_saudi_islamic(year: i32, month: u8, day: u8) -> RataDie {
126    let midmonth = RataDie::new(
127        ISLAMIC_EPOCH_FRIDAY.to_i64_date()
128            + (((year as f64 - 1.0) * 12.0 + month as f64 - 0.5) * MEAN_SYNODIC_MONTH).floor()
129                as i64,
130    );
131    let first_day_of_month = saudi_new_month_on_or_before(midmonth).to_i64_date();
132
133    RataDie::new(first_day_of_month + day as i64 - 1)
134}
135
136/// Lisp code reference: <https://github.com/EdReingold/calendar-code2/blob/main/calendar.l#L2076>
137pub fn fixed_from_tabular_islamic(year: i32, month: u8, day: u8, epoch: RataDie) -> RataDie {
138    let year = i64::from(year);
139    let month = i64::from(month);
140    let day = i64::from(day);
141
142    RataDie::new(
143        (epoch.to_i64_date() - 1)
144            + (year - 1) * 354
145            + (3 + year * 11).div_euclid(30)
146            + 29 * (month - 1)
147            + month.div_euclid(2)
148            + day,
149    )
150}
151/// Lisp code reference: <https://github.com/EdReingold/calendar-code2/blob/main/calendar.l#L2090>
152pub fn tabular_islamic_from_fixed(date: RataDie, epoch: RataDie) -> (i32, u8, u8) {
153    let year = tabular_year_from_fixed(date, epoch);
154    let prior_days = date - fixed_from_tabular_islamic(year, 1, 1, epoch);
155    debug_assert!(prior_days >= 0);
156    debug_assert!(prior_days <= 354);
157    let month = (((prior_days * 11) + 330) / 325) as u8; // Prior days is maximum 354 (when year length is 355), making the value always less than 12
158    debug_assert!(month <= 12);
159    let day = (date - fixed_from_tabular_islamic(year, month, 1, epoch) + 1) as u8; // The value will always be number between 1-30 because of the difference between the date and lunar ordinals function.
160
161    (year, month, day)
162}
163
164/// Lisp code reference: <https://github.com/EdReingold/calendar-code2/blob/main/calendar.l#L2090>
165pub fn tabular_year_from_fixed(date: RataDie, epoch: RataDie) -> i32 {
166    // (354 * 30 + 11) / 30 is the mean year length for a tabular year
167    // This is slightly different from the `calendrical_calculations::islamic::MEAN_YEAR_LENGTH`, which is based on
168    // the (current) synodic month length.
169    //
170    // +1 because the epoch is new year of year 1
171    // Before the epoch the division will round up (towards 0), so we need to
172    // subtract 1, which is the same as not adding the 1.
173    i64_to_saturated_i32((date - epoch) * 30 / (354 * 30 + 11) + (date >= epoch) as i64)
174}
175
176/// The number of days in a month for the observational islamic calendar
177pub fn observational_islamic_month_days(year: i32, month: u8, location: Location) -> u8 {
178    let midmonth = ISLAMIC_EPOCH_FRIDAY.to_f64_date()
179        + (((year - 1) as f64) * 12.0 + month as f64 - 0.5) * MEAN_SYNODIC_MONTH;
180
181    let lunar_phase: f64 =
182        Astronomical::calculate_new_moon_at_or_before(RataDie::new(midmonth as i64));
183    let f_date = Astronomical::phasis_on_or_before(
184        RataDie::new(midmonth as i64),
185        location,
186        Some(lunar_phase),
187    );
188
189    Astronomical::month_length(f_date, location)
190}
191
192/// The number of days in a month for the Saudi (Umm Al-Qura) calendar
193pub fn saudi_islamic_month_days(year: i32, month: u8) -> u8 {
194    // We cannot use month_days from the book here, that is for the observational calendar
195    //
196    // Instead we subtract the two new months calculated using the saudi criterion
197    let midmonth = Moment::new(
198        ISLAMIC_EPOCH_FRIDAY.to_f64_date()
199            + (((year - 1) as f64) * 12.0 + month as f64 - 0.5) * MEAN_SYNODIC_MONTH,
200    );
201    let midmonth_next = midmonth + MEAN_SYNODIC_MONTH;
202
203    let month_start = saudi_new_month_on_or_before(midmonth.as_rata_die());
204    let next_month_start = saudi_new_month_on_or_before(midmonth_next.as_rata_die());
205
206    let diff = next_month_start - month_start;
207    debug_assert!(
208        diff <= 30 || !WELL_BEHAVED_ASTRONOMICAL_RANGE.contains(&month_start),
209        "umm-al-qura months must not be more than 30 days"
210    );
211    u8::try_from(diff).unwrap_or(30)
212}
213
214#[cfg(test)]
215mod tests {
216    use super::*;
217
218    static TEST_FIXED_DATE: [i64; 33] = [
219        -214193, -61387, 25469, 49217, 171307, 210155, 253427, 369740, 400085, 434355, 452605,
220        470160, 473837, 507850, 524156, 544676, 567118, 569477, 601716, 613424, 626596, 645554,
221        664224, 671401, 694799, 704424, 708842, 709409, 709580, 727274, 728714, 744313, 764652,
222    ];
223    // Removed: 601716 and 727274 fixed dates
224    static TEST_FIXED_DATE_UMMALQURA: [i64; 31] = [
225        -214193, -61387, 25469, 49217, 171307, 210155, 253427, 369740, 400085, 434355, 452605,
226        470160, 473837, 507850, 524156, 544676, 567118, 569477, 613424, 626596, 645554, 664224,
227        671401, 694799, 704424, 708842, 709409, 709580, 728714, 744313, 764652,
228    ];
229    // Values from lisp code
230    static SAUDI_CRITERION_EXPECTED: [bool; 33] = [
231        false, false, true, false, false, true, false, true, false, false, true, false, false,
232        true, true, true, true, false, false, true, true, true, false, false, false, false, false,
233        false, true, false, true, false, true,
234    ];
235    // Values from lisp code, removed two expected months.
236    static SAUDI_NEW_MONTH_OR_BEFORE_EXPECTED: [f64; 31] = [
237        -214203.0, -61412.0, 25467.0, 49210.0, 171290.0, 210152.0, 253414.0, 369735.0, 400063.0,
238        434348.0, 452598.0, 470139.0, 473830.0, 507850.0, 524150.0, 544674.0, 567118.0, 569450.0,
239        613421.0, 626592.0, 645551.0, 664214.0, 671391.0, 694779.0, 704405.0, 708835.0, 709396.0,
240        709573.0, 728709.0, 744301.0, 764647.0,
241    ];
242    #[test]
243    fn test_islamic_epoch_friday() {
244        let epoch = ISLAMIC_EPOCH_FRIDAY.to_i64_date();
245        // Proleptic Gregorian year of Islamic Epoch
246        let epoch_year_from_fixed = crate::gregorian::year_from_fixed(RataDie::new(epoch)).unwrap();
247        // 622 is the correct proleptic Gregorian year for the Islamic Epoch
248        assert_eq!(epoch_year_from_fixed, 622);
249    }
250
251    #[test]
252    fn test_islamic_epoch_thursday() {
253        let epoch = ISLAMIC_EPOCH_THURSDAY.to_i64_date();
254        // Proleptic Gregorian year of Islamic Epoch
255        let epoch_year_from_fixed = crate::gregorian::year_from_fixed(RataDie::new(epoch)).unwrap();
256        // 622 is the correct proleptic Gregorian year for the Islamic Epoch
257        assert_eq!(epoch_year_from_fixed, 622);
258    }
259
260    #[test]
261    fn test_saudi_criterion() {
262        for (boolean, f_date) in SAUDI_CRITERION_EXPECTED.iter().zip(TEST_FIXED_DATE.iter()) {
263            let bool_result = saudi_criterion(RataDie::new(*f_date)).unwrap();
264            assert_eq!(*boolean, bool_result, "{f_date:?}");
265        }
266    }
267
268    #[test]
269    fn test_saudi_new_month_or_before() {
270        for (date, f_date) in SAUDI_NEW_MONTH_OR_BEFORE_EXPECTED
271            .iter()
272            .zip(TEST_FIXED_DATE_UMMALQURA.iter())
273        {
274            let date_result = saudi_new_month_on_or_before(RataDie::new(*f_date)).to_f64_date();
275            assert_eq!(*date, date_result, "{f_date:?}");
276        }
277    }
278}