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
20#[doc(inline)]
22pub use crate::chinese_based::WELL_BEHAVED_ASTRONOMICAL_RANGE;
23
24pub 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
32pub 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
40pub 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
58pub 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
77fn 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
94pub 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(); let age = date.to_f64_date() - last_new_moon;
101 let tau = if age <= 3.0 && !adjusted_saudi_criterion(date) {
103 last_new_moon - 30.0 } else {
106 last_new_moon
107 };
108
109 next(RataDie::new(tau as i64), adjusted_saudi_criterion) }
111
112pub 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
124pub 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
136pub 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}
151pub 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; debug_assert!(month <= 12);
159 let day = (date - fixed_from_tabular_islamic(year, month, 1, epoch) + 1) as u8; (year, month, day)
162}
163
164pub fn tabular_year_from_fixed(date: RataDie, epoch: RataDie) -> i32 {
166 i64_to_saturated_i32((date - epoch) * 30 / (354 * 30 + 11) + (date >= epoch) as i64)
174}
175
176pub 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
192pub fn saudi_islamic_month_days(year: i32, month: u8) -> u8 {
194 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 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 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 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 let epoch_year_from_fixed = crate::gregorian::year_from_fixed(RataDie::new(epoch)).unwrap();
247 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 let epoch_year_from_fixed = crate::gregorian::year_from_fixed(RataDie::new(epoch)).unwrap();
256 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}