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