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
9pub const MEAN_YEAR_LENGTH: f64 = MEAN_SYNODIC_MONTH * 12.;
11
12pub const ISLAMIC_EPOCH_FRIDAY: RataDie = crate::julian::fixed_from_julian(622, 7, 16);
15
16pub const ISLAMIC_EPOCH_THURSDAY: RataDie = crate::julian::fixed_from_julian(622, 7, 15);
19
20pub 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
28pub 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
36pub 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
54pub 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
67fn 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
84pub 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(); let age = date.to_f64_date() - last_new_moon;
91 let tau = if age <= 3.0 && !adjusted_saudi_criterion(date) {
93 last_new_moon - 30.0 } else {
96 last_new_moon
97 };
98
99 next(RataDie::new(tau as i64), adjusted_saudi_criterion) }
101
102pub 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
114pub 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
126pub 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}
141pub 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; 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; (year, month, day)
154}
155
156pub 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
172pub fn saudi_islamic_month_days(year: i32, month: u8) -> u8 {
174 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 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 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 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 let epoch_year_from_fixed = crate::iso::iso_year_from_fixed(RataDie::new(epoch));
227 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 let epoch_year_from_fixed = crate::iso::iso_year_from_fixed(RataDie::new(epoch));
236 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}