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) {
60 let lunar_phase = Astronomical::calculate_new_moon_at_or_before(date);
61 let crescent = Astronomical::phasis_on_or_before(date, location, Some(lunar_phase));
62 let elapsed_months =
63 ((crescent - ISLAMIC_EPOCH_FRIDAY) as f64 / MEAN_SYNODIC_MONTH).round() as i32;
64 let year = elapsed_months.div_euclid(12) + 1;
65 let month = elapsed_months.rem_euclid(12) + 1;
66 let day = (date - crescent + 1) as u8;
67
68 (year, month as u8, day)
69}
70
71fn saudi_criterion(date: RataDie) -> Option<bool> {
76 let sunset = Astronomical::sunset((date - 1).as_moment(), MECCA)?;
77 let tee = Location::universal_from_standard(sunset, MECCA);
78 let phase = Astronomical::lunar_phase(tee, Astronomical::julian_centuries(tee));
79 let moonlag = Astronomical::moonlag((date - 1).as_moment(), MECCA)?;
80
81 Some(phase > 0.0 && phase < 90.0 && moonlag > 0.0)
82}
83
84fn adjusted_saudi_criterion(date: RataDie) -> bool {
85 saudi_criterion(date).unwrap_or_default()
86}
87
88pub fn saudi_new_month_on_or_before(date: RataDie) -> RataDie {
91 let last_new_moon = (Astronomical::lunar_phase_at_or_before(0.0, date.as_moment()))
92 .inner()
93 .floor(); let age = date.to_f64_date() - last_new_moon;
95 let tau = if age <= 3.0 && !adjusted_saudi_criterion(date) {
97 last_new_moon - 30.0 } else {
100 last_new_moon
101 };
102
103 next(RataDie::new(tau as i64), adjusted_saudi_criterion) }
105
106pub fn saudi_islamic_from_fixed(date: RataDie) -> (i32, u8, u8) {
108 let crescent = saudi_new_month_on_or_before(date);
109 let elapsed_months =
110 ((crescent - ISLAMIC_EPOCH_FRIDAY) as f64 / MEAN_SYNODIC_MONTH).round() as i64;
111 let year = i64_to_saturated_i32(elapsed_months.div_euclid(12) + 1);
112 let month = (elapsed_months.rem_euclid(12) + 1) as u8;
113 let day = ((date - crescent) + 1) as u8;
114
115 (year, month, day)
116}
117
118pub fn fixed_from_saudi_islamic(year: i32, month: u8, day: u8) -> RataDie {
120 let midmonth = RataDie::new(
121 ISLAMIC_EPOCH_FRIDAY.to_i64_date()
122 + (((year as f64 - 1.0) * 12.0 + month as f64 - 0.5) * MEAN_SYNODIC_MONTH).floor()
123 as i64,
124 );
125 let first_day_of_month = saudi_new_month_on_or_before(midmonth).to_i64_date();
126
127 RataDie::new(first_day_of_month + day as i64 - 1)
128}
129
130pub fn fixed_from_tabular_islamic(year: i32, month: u8, day: u8, epoch: RataDie) -> RataDie {
132 let year = i64::from(year);
133 let month = i64::from(month);
134 let day = i64::from(day);
135
136 RataDie::new(
137 (epoch.to_i64_date() - 1)
138 + (year - 1) * 354
139 + (3 + year * 11).div_euclid(30)
140 + 29 * (month - 1)
141 + month.div_euclid(2)
142 + day,
143 )
144}
145pub fn tabular_islamic_from_fixed(date: RataDie, epoch: RataDie) -> (i32, u8, u8) {
147 let year = i64_to_saturated_i32(((date - epoch) * 30 + 10646).div_euclid(10631));
148 let prior_days =
149 date.to_f64_date() - fixed_from_tabular_islamic(year, 1, 1, epoch).to_f64_date();
150 debug_assert!(prior_days >= 0.0);
151 debug_assert!(prior_days <= 354.);
152 let month = (((prior_days * 11.0) + 330.0) / 325.0) as u8; debug_assert!(month <= 12);
154 let day = (date.to_f64_date() - fixed_from_tabular_islamic(year, month, 1, epoch).to_f64_date()
155 + 1.0) as u8; (year, month, day)
158}
159
160pub fn observational_islamic_month_days(year: i32, month: u8, location: Location) -> u8 {
162 let midmonth = ISLAMIC_EPOCH_FRIDAY.to_f64_date()
163 + (((year - 1) as f64) * 12.0 + month as f64 - 0.5) * MEAN_SYNODIC_MONTH;
164
165 let lunar_phase: f64 =
166 Astronomical::calculate_new_moon_at_or_before(RataDie::new(midmonth as i64));
167 let f_date = Astronomical::phasis_on_or_before(
168 RataDie::new(midmonth as i64),
169 location,
170 Some(lunar_phase),
171 );
172
173 Astronomical::month_length(f_date, location)
174}
175
176pub fn saudi_islamic_month_days(year: i32, month: u8) -> u8 {
178 let midmonth = Moment::new(
182 ISLAMIC_EPOCH_FRIDAY.to_f64_date()
183 + (((year - 1) as f64) * 12.0 + month as f64 - 0.5) * MEAN_SYNODIC_MONTH,
184 );
185 let midmonth_next = midmonth + MEAN_SYNODIC_MONTH;
186
187 let month_start = saudi_new_month_on_or_before(midmonth.as_rata_die());
188 let next_month_start = saudi_new_month_on_or_before(midmonth_next.as_rata_die());
189
190 let diff = next_month_start - month_start;
191 debug_assert!(
192 diff <= 30 || !WELL_BEHAVED_ASTRONOMICAL_RANGE.contains(&month_start),
193 "umm-al-qura months must not be more than 30 days"
194 );
195 u8::try_from(diff).unwrap_or(30)
196}
197
198#[cfg(test)]
199mod tests {
200 use super::*;
201
202 static TEST_FIXED_DATE: [i64; 33] = [
203 -214193, -61387, 25469, 49217, 171307, 210155, 253427, 369740, 400085, 434355, 452605,
204 470160, 473837, 507850, 524156, 544676, 567118, 569477, 601716, 613424, 626596, 645554,
205 664224, 671401, 694799, 704424, 708842, 709409, 709580, 727274, 728714, 744313, 764652,
206 ];
207 static TEST_FIXED_DATE_UMMALQURA: [i64; 31] = [
209 -214193, -61387, 25469, 49217, 171307, 210155, 253427, 369740, 400085, 434355, 452605,
210 470160, 473837, 507850, 524156, 544676, 567118, 569477, 613424, 626596, 645554, 664224,
211 671401, 694799, 704424, 708842, 709409, 709580, 728714, 744313, 764652,
212 ];
213 static SAUDI_CRITERION_EXPECTED: [bool; 33] = [
215 false, false, true, false, false, true, false, true, false, false, true, false, false,
216 true, true, true, true, false, false, true, true, true, false, false, false, false, false,
217 false, true, false, true, false, true,
218 ];
219 static SAUDI_NEW_MONTH_OR_BEFORE_EXPECTED: [f64; 31] = [
221 -214203.0, -61412.0, 25467.0, 49210.0, 171290.0, 210152.0, 253414.0, 369735.0, 400063.0,
222 434348.0, 452598.0, 470139.0, 473830.0, 507850.0, 524150.0, 544674.0, 567118.0, 569450.0,
223 613421.0, 626592.0, 645551.0, 664214.0, 671391.0, 694779.0, 704405.0, 708835.0, 709396.0,
224 709573.0, 728709.0, 744301.0, 764647.0,
225 ];
226 #[test]
227 fn test_islamic_epoch_friday() {
228 let epoch = ISLAMIC_EPOCH_FRIDAY.to_i64_date();
229 let epoch_year_from_fixed = crate::gregorian::year_from_fixed(RataDie::new(epoch)).unwrap();
231 assert_eq!(epoch_year_from_fixed, 622);
233 }
234
235 #[test]
236 fn test_islamic_epoch_thursday() {
237 let epoch = ISLAMIC_EPOCH_THURSDAY.to_i64_date();
238 let epoch_year_from_fixed = crate::gregorian::year_from_fixed(RataDie::new(epoch)).unwrap();
240 assert_eq!(epoch_year_from_fixed, 622);
242 }
243
244 #[test]
245 fn test_saudi_criterion() {
246 for (boolean, f_date) in SAUDI_CRITERION_EXPECTED.iter().zip(TEST_FIXED_DATE.iter()) {
247 let bool_result = saudi_criterion(RataDie::new(*f_date)).unwrap();
248 assert_eq!(*boolean, bool_result, "{f_date:?}");
249 }
250 }
251
252 #[test]
253 fn test_saudi_new_month_or_before() {
254 for (date, f_date) in SAUDI_NEW_MONTH_OR_BEFORE_EXPECTED
255 .iter()
256 .zip(TEST_FIXED_DATE_UMMALQURA.iter())
257 {
258 let date_result = saudi_new_month_on_or_before(RataDie::new(*f_date)).to_f64_date();
259 assert_eq!(*date, date_result, "{f_date:?}");
260 }
261 }
262}