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
7const 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
12const CAIRO: Location = Location {
14 latitude: 30.1,
15 longitude: 31.3,
16 elevation: 200.0,
17 zone: (1_f64 / 12_f64),
18};
19
20pub trait IslamicBasedMarker {
22 const EPOCH: RataDie;
24 const DEBUG_NAME: &'static str;
26 const HAS_353_DAY_YEARS: bool;
29 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 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 fn fixed_from_islamic(year: i32, month: u8, day: u8) -> RataDie;
42 fn islamic_from_fixed(date: RataDie) -> (i32, u8, u8);
44
45 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 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#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq)]
122#[allow(clippy::exhaustive_structs)] pub struct ObservationalIslamicMarker;
124
125#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq)]
127#[allow(clippy::exhaustive_structs)] pub struct SaudiIslamicMarker;
129
130#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq)]
132#[allow(clippy::exhaustive_structs)] pub struct CivilIslamicMarker;
134
135#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq)]
137#[allow(clippy::exhaustive_structs)] pub 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
188pub 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
200pub 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
213fn 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
230pub 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(); let age = date.to_f64_date() - last_new_moon;
237 let tau = if age <= 3.0 && !adjusted_saudi_criterion(date) {
239 last_new_moon - 30.0 } else {
242 last_new_moon
243 };
244
245 next(RataDie::new(tau as i64), adjusted_saudi_criterion) }
247
248pub 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
260pub 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
272pub 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}
287pub 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; 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; (year, month, day)
300}
301
302pub 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}
316pub 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; 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; (year, month, day)
330}
331
332pub 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
345pub fn saudi_islamic_month_days(year: i32, month: u8) -> u8 {
347 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 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 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 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 let epoch_year_from_fixed = crate::iso::iso_year_from_fixed(RataDie::new(epoch));
400 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 let epoch_year_from_fixed = crate::iso::iso_year_from_fixed(RataDie::new(epoch));
409 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}