1use chrono::{DateTime, Datelike, Duration, NaiveDate, Utc};
13
14use crate::astronomy::ops;
15use crate::astronomy::solar::SolarTime;
16use crate::astronomy::unit::{Angle, Coordinates, Stride};
17use crate::models::method::Method;
18use crate::models::parameters::Parameters;
19use crate::models::prayer::Prayer;
20use crate::models::rounding::Rounding;
21
22#[derive(PartialEq, Debug, Copy, Clone)]
25pub struct PrayerTimes {
26 fajr: DateTime<Utc>,
27 sunrise: DateTime<Utc>,
28 dhuhr: DateTime<Utc>,
29 asr: DateTime<Utc>,
30 maghrib: DateTime<Utc>,
31 isha: DateTime<Utc>,
32 middle_of_the_night: DateTime<Utc>,
33 qiyam: DateTime<Utc>,
34 fajr_tomorrow: DateTime<Utc>,
35 coordinates: Coordinates,
36 date: DateTime<Utc>,
37 parameters: Parameters,
38}
39
40impl PrayerTimes {
41 pub fn new(date: NaiveDate, coordinates: Coordinates, parameters: Parameters) -> PrayerTimes {
42 let prayer_date = date
43 .and_hms_opt(0, 0, 0)
44 .expect("Invalid date provided")
45 .and_utc();
46 let tomorrow = prayer_date.tomorrow();
47 let solar_time = SolarTime::new(prayer_date, coordinates);
48 let solar_time_tomorrow = SolarTime::new(tomorrow, coordinates);
49
50 let asr = solar_time.afternoon(parameters.madhab.shadow().into());
51 let night = solar_time_tomorrow
52 .sunrise
53 .signed_duration_since(solar_time.sunset);
54
55 let final_fajr =
56 PrayerTimes::calculate_fajr(parameters, solar_time, night, coordinates, prayer_date)
57 .rounded_minute(parameters.rounding);
58 let final_sunrise = solar_time
59 .sunrise
60 .adjust_time(parameters.time_adjustments(Prayer::Sunrise))
61 .rounded_minute(parameters.rounding);
62 let final_dhuhr = solar_time
63 .transit
64 .adjust_time(parameters.time_adjustments(Prayer::Dhuhr))
65 .rounded_minute(parameters.rounding);
66 let final_asr = asr
67 .adjust_time(parameters.time_adjustments(Prayer::Asr))
68 .rounded_minute(parameters.rounding);
69 let final_maghrib = ops::adjust_time(
70 &solar_time.sunset,
71 parameters.time_adjustments(Prayer::Maghrib),
72 )
73 .rounded_minute(parameters.rounding);
74 let final_isha =
75 PrayerTimes::calculate_isha(parameters, solar_time, night, coordinates, prayer_date)
76 .rounded_minute(parameters.rounding);
77
78 let (final_middle_of_night, final_qiyam, final_fajr_tomorrow) =
80 PrayerTimes::calculate_qiyam(
81 final_maghrib,
82 parameters,
83 solar_time_tomorrow,
84 coordinates,
85 tomorrow,
86 );
87
88 PrayerTimes {
89 fajr: final_fajr,
90 sunrise: final_sunrise,
91 dhuhr: final_dhuhr,
92 asr: final_asr,
93 maghrib: final_maghrib,
94 isha: final_isha,
95 middle_of_the_night: final_middle_of_night,
96 qiyam: final_qiyam,
97 fajr_tomorrow: final_fajr_tomorrow,
98 coordinates: coordinates,
99 date: prayer_date,
100 parameters: parameters,
101 }
102 }
103
104 pub fn time(&self, prayer: Prayer) -> DateTime<Utc> {
105 match prayer {
106 Prayer::Fajr => self.fajr,
107 Prayer::Sunrise => self.sunrise,
108 Prayer::Dhuhr => self.dhuhr,
109 Prayer::Asr => self.asr,
110 Prayer::Maghrib => self.maghrib,
111 Prayer::Isha => self.isha,
112 Prayer::Qiyam => self.qiyam,
113 Prayer::FajrTomorrow => self.fajr_tomorrow,
114 }
115 }
116
117 pub fn current(&self) -> Prayer {
118 self.current_time(Utc::now()).expect("Out of bounds")
119 }
120
121 pub fn next(&self) -> Prayer {
122 match self.current() {
123 Prayer::Fajr => Prayer::Sunrise,
124 Prayer::Sunrise => Prayer::Dhuhr,
125 Prayer::Dhuhr => Prayer::Asr,
126 Prayer::Asr => Prayer::Maghrib,
127 Prayer::Maghrib => Prayer::Isha,
128 Prayer::Isha => Prayer::Qiyam,
129 Prayer::Qiyam => Prayer::FajrTomorrow,
130 _ => Prayer::FajrTomorrow,
131 }
132 }
133
134 pub fn time_remaining(&self) -> (u32, u32) {
135 let next_time = self.time(self.next());
136 let now = Utc::now();
137 let now_to_next = next_time.signed_duration_since(now).num_seconds() as f64;
138 let whole: f64 = now_to_next / 60.0 / 60.0;
139 let fract = whole.fract();
140 let hours = whole.trunc() as u32;
141 let minutes = (fract * 60.0).round() as u32;
142
143 (hours, minutes)
144 }
145
146 fn current_time(&self, time: DateTime<Utc>) -> Option<Prayer> {
147 let current_prayer: Option<Prayer>;
148
149 if self.fajr_tomorrow.signed_duration_since(time).num_seconds() <= 0 {
150 current_prayer = Some(Prayer::FajrTomorrow)
151 } else if self.qiyam.signed_duration_since(time).num_seconds() <= 0 {
152 current_prayer = Some(Prayer::Qiyam)
153 } else if self.isha.signed_duration_since(time).num_seconds() <= 0 {
154 current_prayer = Some(Prayer::Isha);
155 } else if self.maghrib.signed_duration_since(time).num_seconds() <= 0 {
156 current_prayer = Some(Prayer::Maghrib);
157 } else if self.asr.signed_duration_since(time).num_seconds() <= 0 {
158 current_prayer = Some(Prayer::Asr);
159 } else if self.dhuhr.signed_duration_since(time).num_seconds() <= 0 {
160 current_prayer = Some(Prayer::Dhuhr);
161 } else if self.sunrise.signed_duration_since(time).num_seconds() <= 0 {
162 current_prayer = Some(Prayer::Sunrise);
163 } else if self.fajr.signed_duration_since(time).num_seconds() <= 0 {
164 current_prayer = Some(Prayer::Fajr);
165 } else {
166 current_prayer = None;
167 }
168
169 current_prayer
170 }
171
172 fn calculate_fajr(
173 parameters: Parameters,
174 solar_time: SolarTime,
175 night: Duration,
176 coordinates: Coordinates,
177 prayer_date: DateTime<Utc>,
178 ) -> DateTime<Utc> {
179 let mut fajr = solar_time.time_for_solar_angle(Angle::new(-parameters.fajr_angle), false);
180
181 if parameters.method == Method::MoonsightingCommittee && coordinates.latitude >= 55.0 {
183 let night_fraction = night.num_seconds() / 7;
184 fajr = solar_time
185 .sunrise
186 .checked_add_signed(Duration::seconds(-night_fraction))
187 .unwrap();
188 } else {
189 }
191
192 let safe_fajr = if parameters.method == Method::MoonsightingCommittee {
193 let day_of_year = prayer_date.ordinal();
194 ops::season_adjusted_morning_twilight(
195 coordinates.latitude,
196 day_of_year,
197 prayer_date.year() as u32,
198 solar_time.sunrise,
199 )
200 } else {
201 let portion = parameters.night_portions().0;
202 let night_fraction = portion * (night.num_seconds() as f64);
203
204 solar_time
205 .sunrise
206 .checked_add_signed(Duration::seconds(-night_fraction as i64))
207 .unwrap()
208 };
209
210 if fajr < safe_fajr {
211 fajr = safe_fajr;
212 } else {
213 }
215
216 fajr.adjust_time(parameters.time_adjustments(Prayer::Fajr))
217 }
218
219 fn calculate_isha(
220 parameters: Parameters,
221 solar_time: SolarTime,
222 night: Duration,
223 coordinates: Coordinates,
224 prayer_date: DateTime<Utc>,
225 ) -> DateTime<Utc> {
226 let mut isha: DateTime<Utc>;
227
228 if parameters.isha_interval > 0 {
229 isha = solar_time
230 .sunset
231 .checked_add_signed(Duration::seconds((parameters.isha_interval * 60) as i64))
232 .unwrap();
233 } else {
234 isha = solar_time.time_for_solar_angle(Angle::new(-parameters.isha_angle), true);
235
236 if parameters.method == Method::MoonsightingCommittee && coordinates.latitude >= 55.0 {
238 let night_fraction = night.num_seconds() / 7;
239 isha = solar_time
240 .sunset
241 .checked_add_signed(Duration::seconds(night_fraction))
242 .unwrap();
243 } else {
244 }
246
247 let safe_isha = if parameters.method == Method::MoonsightingCommittee {
248 let day_of_year = prayer_date.ordinal();
249
250 ops::season_adjusted_evening_twilight(
251 coordinates.latitude,
252 day_of_year,
253 prayer_date.year() as u32,
254 solar_time.sunset,
255 parameters.shafaq,
256 )
257 } else {
258 let portion = parameters.night_portions().1;
259 let night_fraction = portion * (night.num_seconds() as f64);
260
261 solar_time
262 .sunset
263 .checked_add_signed(Duration::seconds(night_fraction as i64))
264 .unwrap()
265 };
266
267 if isha > safe_isha {
268 isha = safe_isha;
269 } else {
270 }
272 }
273
274 isha.adjust_time(parameters.time_adjustments(Prayer::Isha))
275 }
276
277 fn calculate_qiyam(
278 current_maghrib: DateTime<Utc>,
279 parameters: Parameters,
280 solar_time: SolarTime,
281 coordinates: Coordinates,
282 prayer_date: DateTime<Utc>,
283 ) -> (DateTime<Utc>, DateTime<Utc>, DateTime<Utc>) {
284 let tomorrow = prayer_date.tomorrow();
285 let solar_time_tomorrow = SolarTime::new(tomorrow, coordinates);
286 let night = solar_time_tomorrow
287 .sunrise
288 .signed_duration_since(solar_time.sunset);
289
290 let tomorrow_fajr =
291 PrayerTimes::calculate_fajr(parameters, solar_time, night, coordinates, prayer_date);
292 let night_duration = tomorrow_fajr
293 .signed_duration_since(current_maghrib)
294 .num_seconds() as f64;
295 let middle_night_portion = (night_duration / 2.0) as i64;
296 let last_third_portion = (night_duration * (2.0 / 3.0)) as i64;
297 let middle_of_night = current_maghrib
298 .checked_add_signed(Duration::seconds(middle_night_portion))
299 .unwrap()
300 .rounded_minute(Rounding::Nearest);
301 let last_third_of_night = current_maghrib
302 .checked_add_signed(Duration::seconds(last_third_portion))
303 .unwrap()
304 .rounded_minute(Rounding::Nearest);
305
306 (middle_of_night, last_third_of_night, tomorrow_fajr)
307 }
308}
309
310pub struct PrayerSchedule {
312 date: Option<NaiveDate>,
313 coordinates: Option<Coordinates>,
314 params: Option<Parameters>,
315}
316
317impl PrayerSchedule {
318 pub fn new() -> PrayerSchedule {
319 PrayerSchedule {
320 date: None,
321 coordinates: None,
322 params: None,
323 }
324 }
325
326 pub fn on<'a>(&'a mut self, date: NaiveDate) -> &'a mut PrayerSchedule {
327 self.date = Some(date);
328 self
329 }
330
331 pub fn for_location<'a>(&'a mut self, location: Coordinates) -> &'a mut PrayerSchedule {
332 self.coordinates = Some(location);
333 self
334 }
335
336 pub fn with_configuration<'a>(&'a mut self, params: Parameters) -> &'a mut PrayerSchedule {
337 self.params = Some(params);
338 self
339 }
340
341 pub fn calculate(&self) -> Result<PrayerTimes, String> {
342 if self.date.is_some() && self.coordinates.is_some() && self.params.is_some() {
343 Ok(PrayerTimes::new(
344 self.date.unwrap(),
345 self.coordinates.unwrap(),
346 self.params.unwrap(),
347 ))
348 } else {
349 Err(String::from(
350 "Required information is needed in order to calculate the prayer times.",
351 ))
352 }
353 }
354}
355
356#[cfg(test)]
357mod tests {
358 use super::*;
359 use crate::models::madhab::Madhab;
360 use crate::Configuration;
361 use chrono::{NaiveDate, TimeZone, Utc};
362
363 #[test]
364 fn current_prayer_should_be_fajr() {
365 let local_date = NaiveDate::from_ymd_opt(2015, 7, 12).expect("Invalid date provided");
367 let params = Configuration::with(Method::NorthAmerica, Madhab::Hanafi);
368 let coordinates = Coordinates::new(35.7750, -78.6336);
369 let times = PrayerTimes::new(local_date, coordinates, params);
370 let current_prayer_time = local_date.and_hms_opt(9, 0, 0).unwrap().and_utc();
371
372 assert_eq!(times.current_time(current_prayer_time), Some(Prayer::Fajr));
373 }
374
375 #[test]
376 fn current_prayer_should_be_sunrise() {
377 let local_date = NaiveDate::from_ymd_opt(2015, 7, 12).expect("Invalid date provided");
379 let params = Configuration::with(Method::NorthAmerica, Madhab::Hanafi);
380 let coordinates = Coordinates::new(35.7750, -78.6336);
381 let times = PrayerTimes::new(local_date, coordinates, params);
382 let current_prayer_time = local_date.and_hms_opt(11, 0, 0).unwrap().and_utc();
383
384 assert_eq!(
385 times.current_time(current_prayer_time),
386 Some(Prayer::Sunrise)
387 );
388 }
389
390 #[test]
391 fn current_prayer_should_be_dhuhr() {
392 let local_date = NaiveDate::from_ymd_opt(2015, 7, 12).expect("Invalid date provided");
394 let params = Configuration::with(Method::NorthAmerica, Madhab::Hanafi);
395 let coordinates = Coordinates::new(35.7750, -78.6336);
396 let times = PrayerTimes::new(local_date, coordinates, params);
397 let current_prayer_time = local_date.and_hms_opt(19, 0, 0).unwrap().and_utc();
398
399 assert_eq!(times.current_time(current_prayer_time), Some(Prayer::Dhuhr));
400 }
401
402 #[test]
403 fn current_prayer_should_be_asr() {
404 let local_date = NaiveDate::from_ymd_opt(2015, 7, 12).expect("Invalid date provided");
406 let params = Configuration::with(Method::NorthAmerica, Madhab::Hanafi);
407 let coordinates = Coordinates::new(35.7750, -78.6336);
408 let times = PrayerTimes::new(local_date, coordinates, params);
409 let current_prayer_time = local_date.and_hms_opt(22, 26, 0).unwrap().and_utc();
410
411 assert_eq!(times.current_time(current_prayer_time), Some(Prayer::Asr));
412 }
413
414 #[test]
415 fn current_prayer_should_be_maghrib() {
416 let local_date = NaiveDate::from_ymd_opt(2015, 7, 12).expect("Invalid data provided");
418 let params = Configuration::with(Method::NorthAmerica, Madhab::Hanafi);
419 let coordinates = Coordinates::new(35.7750, -78.6336);
420 let times = PrayerTimes::new(local_date, coordinates, params);
421 let current_prayer_time = Utc.with_ymd_and_hms(2015, 7, 13, 01, 0, 0).unwrap();
422
423 assert_eq!(
424 times.current_time(current_prayer_time),
425 Some(Prayer::Maghrib)
426 );
427 }
428
429 #[test]
430 fn current_prayer_should_be_isha() {
431 let local_date = NaiveDate::from_ymd_opt(2015, 7, 12).expect("Invalid date provided");
433 let params = Configuration::with(Method::NorthAmerica, Madhab::Hanafi);
434 let coordinates = Coordinates::new(35.7750, -78.6336);
435 let times = PrayerTimes::new(local_date, coordinates, params);
436 let current_prayer_time = Utc.with_ymd_and_hms(2015, 7, 13, 02, 0, 0).unwrap();
437
438 assert_eq!(times.current_time(current_prayer_time), Some(Prayer::Isha));
439 }
440
441 #[test]
442 fn current_prayer_should_be_none() {
443 let local_date = NaiveDate::from_ymd_opt(2015, 7, 12).expect("Invalid data provided");
444 let params = Configuration::with(Method::NorthAmerica, Madhab::Hanafi);
445 let coordinates = Coordinates::new(35.7750, -78.6336);
446 let times = PrayerTimes::new(local_date, coordinates, params);
447 let current_prayer_time = local_date.and_hms_opt(8, 0, 0).unwrap().and_utc();
448
449 assert_eq!(times.current_time(current_prayer_time), None);
450 }
451
452 #[test]
453 fn calculate_times_for_moonsighting_method() {
454 let date = NaiveDate::from_ymd_opt(2016, 1, 31).expect("Invalid date provided");
455 let params = Configuration::with(Method::MoonsightingCommittee, Madhab::Shafi);
456 let coordinates = Coordinates::new(35.7750, -78.6336);
457 let result = PrayerSchedule::new()
458 .on(date)
459 .for_location(coordinates)
460 .with_configuration(params)
461 .calculate();
462
463 match result {
464 Ok(schedule) => {
465 assert_eq!(
472 schedule.time(Prayer::Fajr).format("%-l:%M %p").to_string(),
473 "10:48 AM"
474 );
475 assert_eq!(
476 schedule
477 .time(Prayer::Sunrise)
478 .format("%-l:%M %p")
479 .to_string(),
480 "12:16 PM"
481 );
482 assert_eq!(
483 schedule.time(Prayer::Dhuhr).format("%-l:%M %p").to_string(),
484 "5:33 PM"
485 );
486 assert_eq!(
487 schedule.time(Prayer::Asr).format("%-l:%M %p").to_string(),
488 "8:20 PM"
489 );
490 assert_eq!(
491 schedule
492 .time(Prayer::Maghrib)
493 .format("%-l:%M %p")
494 .to_string(),
495 "10:43 PM"
496 );
497 assert_eq!(
498 schedule.time(Prayer::Isha).format("%-l:%M %p").to_string(),
499 "12:05 AM"
500 );
501 }
502
503 Err(_err) => assert!(false),
504 }
505 }
506
507 #[test]
508 fn calculate_times_for_moonsighting_method_with_high_latitude() {
509 let date = NaiveDate::from_ymd_opt(2016, 1, 1).expect("Invalid date provided");
510 let params = Configuration::with(Method::MoonsightingCommittee, Madhab::Hanafi);
511 let coordinates = Coordinates::new(59.9094, 10.7349);
512 let result = PrayerSchedule::new()
513 .on(date)
514 .for_location(coordinates)
515 .with_configuration(params)
516 .calculate();
517
518 match result {
519 Ok(schedule) => {
520 assert_eq!(
527 schedule.time(Prayer::Fajr).format("%-l:%M %p").to_string(),
528 "6:34 AM"
529 );
530 assert_eq!(
531 schedule
532 .time(Prayer::Sunrise)
533 .format("%-l:%M %p")
534 .to_string(),
535 "8:19 AM"
536 );
537 assert_eq!(
538 schedule.time(Prayer::Dhuhr).format("%-l:%M %p").to_string(),
539 "11:25 AM"
540 );
541 assert_eq!(
542 schedule.time(Prayer::Asr).format("%-l:%M %p").to_string(),
543 "12:36 PM"
544 );
545 assert_eq!(
546 schedule
547 .time(Prayer::Maghrib)
548 .format("%-l:%M %p")
549 .to_string(),
550 "2:25 PM"
551 );
552 assert_eq!(
553 schedule.time(Prayer::Isha).format("%-l:%M %p").to_string(),
554 "4:02 PM"
555 );
556 }
557
558 Err(_err) => assert!(false),
559 }
560 }
561}