salah/
schedule.rs

1// Salah
2//
3// See LICENSE for more details.
4// Copyright (c) 2019-2022 Farhan Ahmed. All rights reserved.
5//
6
7//! # Prayer Schedule
8//!
9//! This module provides the main objects that are used for calculating
10//! the prayer times.
11
12use 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/// A data struct to hold the timing for all
23/// prayers.
24#[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        // Calculate the middle of the night and qiyam times
79        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        // special case for moonsighting committee above latitude 55
182        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            // Nothing to do.
190        }
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            // Nothing to do.
214        }
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            // special case for moonsighting committee above latitude 55
237            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                // Nothing to do.
245            }
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                // Nothing to do.
271            }
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
310/// A builder for the [PrayerTimes](struct.PrayerTimes.html) struct.
311pub 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        // Given the above DateTime, the Fajr prayer is at 2015-07-12T08:42:00Z
366        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        // Given the below DateTime, sunrise is at 2015-07-12T10:08:00Z
378        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        // Given the above DateTime, dhuhr prayer is at 2015-07-12T17:21:00Z
393        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        // Given the below DateTime, asr is at 2015-07-12T22:22:00Z
405        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        // Given the below DateTime, maghrib is at 2015-07-13T00:32:00Z
417        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        // Given the below DateTime, isha is at 2015-07-13T01:57:00Z
432        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                // fajr    = 2016-01-31 10:48:00 UTC
466                // sunrise = 2016-01-31 12:16:00 UTC
467                // dhuhr   = 2016-01-31 17:33:00 UTC
468                // asr     = 2016-01-31 20:20:00 UTC
469                // maghrib = 2016-01-31 22:43:00 UTC
470                // isha    = 2016-02-01 00:05:00 UTC
471                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                // fajr    = 2016-01-01 06:34:00 UTC
521                // sunrise = 2016-01-01 08:19:00 UTC
522                // dhuhr   = 2016-01-01 11:25:00 UTC
523                // asr     = 2016-01-01 12:36:00 UTC
524                // maghrib = 2016-01-01 14:25:00 UTC
525                // isha    = 2016-01-01 16:02:00 UTC
526                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}