1use chrono::{DateTime, NaiveDate, Utc};
19use chrono_tz::Tz;
20use serde::{Deserialize, Serialize};
21use std::collections::HashMap;
22
23#[derive(Debug, Clone, Copy, PartialEq)]
25pub enum CalculationMethod {
26 MWL,
28 ISNA,
30 Egypt,
32 Makkah,
34 Karachi,
36 Tehran,
38 Jafari,
40 France,
42 Russia,
44 Singapore,
46}
47
48#[derive(Debug, Clone, Copy, PartialEq)]
50pub enum HighLatitudeRule {
51 None,
53 NightMiddle,
55 OneSeventh,
57 AngleBased,
59}
60
61#[derive(Debug, Clone, Copy, PartialEq)]
63pub enum AsrMethod {
64 Standard,
66 Hanafi,
68}
69
70#[derive(Debug, Clone, Copy, PartialEq)]
72pub enum MidnightMethod {
73 Standard,
75 Jafari,
77}
78
79#[derive(Debug, Clone, Copy, PartialEq)]
81pub enum TimeFormat {
82 Hour24,
84 Hour12,
86 Hour12NoSuffix,
88 Timestamp,
90}
91
92#[derive(Debug, Clone, Copy, PartialEq)]
94pub enum RoundingMethod {
95 None,
97 Nearest,
99 Up,
101 Down,
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct PrayerTimes {
108 pub fajr: String,
109 pub sunrise: String,
110 pub dhuhr: String,
111 pub asr: String,
112 pub sunset: String,
113 pub maghrib: String,
114 pub isha: String,
115 pub midnight: String,
116}
117
118
119#[derive(Debug, Clone)]
121pub struct PrayTime {
122 method: CalculationMethod,
123 latitude: f64,
124 longitude: f64,
125 timezone: Option<Tz>,
126 utc_offset: Option<i32>, high_latitude_rule: HighLatitudeRule,
128 asr_method: AsrMethod,
129 midnight_method: MidnightMethod,
130 time_format: TimeFormat,
131 rounding: RoundingMethod,
132 iterations: u8,
133
134 fajr_angle: f64,
136 isha_angle: f64,
137 maghrib_angle: f64,
138 maghrib_minutes: f64,
139 isha_minutes: f64,
140
141 tune_fajr: f64,
143 tune_sunrise: f64,
144 tune_dhuhr: f64,
145 tune_asr: f64,
146 tune_sunset: f64,
147 tune_maghrib: f64,
148 tune_isha: f64,
149 tune_midnight: f64,
150
151 utc_time: i64,
153 adjusted: bool,
154}
155
156impl Default for PrayTime {
157 fn default() -> Self {
158 Self::new("MWL")
159 }
160}
161
162impl PrayTime {
163 pub fn new(method: &str) -> Self {
165 let mut praytime = Self {
166 method: CalculationMethod::MWL,
167 latitude: 0.0,
168 longitude: 0.0,
169 timezone: None,
170 utc_offset: None,
171 high_latitude_rule: HighLatitudeRule::NightMiddle,
172 asr_method: AsrMethod::Standard,
173 midnight_method: MidnightMethod::Standard,
174 time_format: TimeFormat::Hour24,
175 rounding: RoundingMethod::Nearest,
176 iterations: 1,
177
178 fajr_angle: 18.0,
179 isha_angle: 17.0,
180 maghrib_angle: 0.833,
181 maghrib_minutes: 1.0,
182 isha_minutes: 14.0,
183
184 tune_fajr: 0.0,
185 tune_sunrise: 0.0,
186 tune_dhuhr: 0.0,
187 tune_asr: 0.0,
188 tune_sunset: 0.0,
189 tune_maghrib: 0.0,
190 tune_isha: 0.0,
191 tune_midnight: 0.0,
192
193 utc_time: 0,
194 adjusted: false,
195 };
196
197 praytime.set_method(method);
198 praytime
199 }
200
201 pub fn set_method(&mut self, method: &str) -> &mut Self {
203 self.method = match method {
204 "MWL" => CalculationMethod::MWL,
205 "ISNA" => CalculationMethod::ISNA,
206 "Egypt" => CalculationMethod::Egypt,
207 "Makkah" => CalculationMethod::Makkah,
208 "Karachi" => CalculationMethod::Karachi,
209 "Tehran" => CalculationMethod::Tehran,
210 "Jafari" => CalculationMethod::Jafari,
211 "France" => CalculationMethod::France,
212 "Russia" => CalculationMethod::Russia,
213 "Singapore" => CalculationMethod::Singapore,
214 _ => CalculationMethod::MWL,
215 };
216
217 self.fajr_angle = 18.0;
219 self.isha_angle = 14.0;
220 self.maghrib_angle = 0.833;
221 self.maghrib_minutes = 1.0; self.isha_minutes = 0.0; self.midnight_method = MidnightMethod::Standard;
224
225 match self.method {
227 CalculationMethod::MWL => {
228 self.fajr_angle = 18.0;
229 self.isha_angle = 17.0;
230 }
231 CalculationMethod::ISNA => {
232 self.fajr_angle = 15.0;
233 self.isha_angle = 15.0;
234 }
235 CalculationMethod::Egypt => {
236 self.fajr_angle = 19.5;
237 self.isha_angle = 17.5;
238 }
239 CalculationMethod::Makkah => {
240 self.fajr_angle = 18.5;
241 self.isha_minutes = 90.0;
242 }
243 CalculationMethod::Karachi => {
244 self.fajr_angle = 18.0;
245 self.isha_angle = 18.0;
246 }
247 CalculationMethod::Tehran => {
248 self.fajr_angle = 17.7;
249 self.maghrib_minutes = 4.5;
250 self.midnight_method = MidnightMethod::Jafari;
251 }
252 CalculationMethod::Jafari => {
253 self.fajr_angle = 16.0;
254 self.maghrib_minutes = 4.0;
255 self.midnight_method = MidnightMethod::Jafari;
256 }
257 CalculationMethod::France => {
258 self.fajr_angle = 12.0;
259 self.isha_angle = 12.0;
260 }
261 CalculationMethod::Russia => {
262 self.fajr_angle = 16.0;
263 self.isha_angle = 15.0;
264 }
265 CalculationMethod::Singapore => {
266 self.fajr_angle = 20.0;
267 self.isha_angle = 18.0;
268 }
269 }
270
271 self
272 }
273
274 pub fn location(&mut self, latitude: f64, longitude: f64) -> &mut Self {
276 self.latitude = latitude;
277 self.longitude = longitude;
278 self
279 }
280
281 pub fn timezone(&mut self, tz_name: &str) -> &mut Self {
283 self.timezone = tz_name.parse().ok();
284 self.utc_offset = None; self
286 }
287
288 pub fn utc_offset(&mut self, minutes: i32) -> &mut Self {
290 self.utc_offset = Some(minutes);
291 self.timezone = None; self
293 }
294
295 pub fn format(&mut self, format: TimeFormat) -> &mut Self {
297 self.time_format = format;
298 self
299 }
300
301 pub fn rounding(&mut self, method: RoundingMethod) -> &mut Self {
303 self.rounding = method;
304 self
305 }
306
307 pub fn high_latitude_rule(&mut self, rule: HighLatitudeRule) -> &mut Self {
309 self.high_latitude_rule = rule;
310 self
311 }
312
313 pub fn asr_method(&mut self, method: AsrMethod) -> &mut Self {
315 self.asr_method = method;
316 self
317 }
318
319 pub fn get_times(&mut self, date: Option<NaiveDate>) -> PrayerTimes {
321 let date = date.unwrap_or_else(|| Utc::now().date_naive());
322 self.utc_time = DateTime::<Utc>::from_naive_utc_and_offset(
323 date.and_hms_opt(0, 0, 0).unwrap(), Utc
324 ).timestamp_millis();
325
326 let times = self.compute_times();
327 self.format_times(times)
328 }
329
330 fn compute_times(&mut self) -> HashMap<String, f64> {
332 let mut times = HashMap::new();
333 times.insert("fajr".to_string(), 5.0);
334 times.insert("sunrise".to_string(), 6.0);
335 times.insert("dhuhr".to_string(), 12.0);
336 times.insert("asr".to_string(), 13.0);
337 times.insert("sunset".to_string(), 18.0);
338 times.insert("maghrib".to_string(), 18.0);
339 times.insert("isha".to_string(), 18.0);
340 times.insert("midnight".to_string(), 24.0);
341
342 for _ in 0..self.iterations {
343 times = self.process_times(×);
344 }
345
346 self.adjust_high_lats(&mut times);
347 self.update_times(&mut times);
348 self.tune_times(&mut times);
349 self.convert_times(&mut times);
350
351 times
352 }
353
354 fn process_times(&mut self, times: &HashMap<String, f64>) -> HashMap<String, f64> {
356 let mut result = HashMap::new();
357 let horizon = 0.833;
358
359 result.insert("fajr".to_string(),
360 self.angle_time(self.fajr_angle, times["fajr"], -1.0));
361 result.insert("sunrise".to_string(),
362 self.angle_time(horizon, times["sunrise"], -1.0));
363 result.insert("dhuhr".to_string(),
364 self.mid_day(times["dhuhr"]));
365 result.insert("asr".to_string(),
366 self.angle_time(self.asr_angle(times["asr"]), times["asr"], 1.0));
367 result.insert("sunset".to_string(),
368 self.angle_time(horizon, times["sunset"], 1.0));
369 result.insert("maghrib".to_string(),
370 self.angle_time(self.maghrib_angle, times["maghrib"], 1.0));
371 result.insert("isha".to_string(),
372 self.angle_time(self.isha_angle, times["isha"], 1.0));
373 result.insert("midnight".to_string(),
374 self.mid_day(times["midnight"]) + 12.0);
375
376 result
377 }
378
379 fn update_times(&mut self, times: &mut HashMap<String, f64>) {
381 if self.maghrib_minutes > 1.0 { times.insert("maghrib".to_string(),
384 times["sunset"] + self.maghrib_minutes / 60.0);
385 }
386
387 if matches!(self.method, CalculationMethod::Makkah) && self.isha_minutes > 0.0 {
389 times.insert("isha".to_string(),
390 times["maghrib"] + self.isha_minutes / 60.0);
391 }
392
393 if matches!(self.midnight_method, MidnightMethod::Jafari) {
395 let next_fajr = self.angle_time(self.fajr_angle, 29.0, -1.0) + 24.0;
396 let fajr_time = if self.adjusted { times["fajr"] + 24.0 } else { next_fajr };
397 times.insert("midnight".to_string(),
398 (times["sunset"] + fajr_time) / 2.0);
399 }
400 }
401
402 fn tune_times(&self, times: &mut HashMap<String, f64>) {
404 *times.get_mut("fajr").unwrap() += self.tune_fajr / 60.0;
405 *times.get_mut("sunrise").unwrap() += self.tune_sunrise / 60.0;
406 *times.get_mut("dhuhr").unwrap() += self.tune_dhuhr / 60.0;
407 *times.get_mut("asr").unwrap() += self.tune_asr / 60.0;
408 *times.get_mut("sunset").unwrap() += self.tune_sunset / 60.0;
409 *times.get_mut("maghrib").unwrap() += self.tune_maghrib / 60.0;
410 *times.get_mut("isha").unwrap() += self.tune_isha / 60.0;
411 *times.get_mut("midnight").unwrap() += self.tune_midnight / 60.0;
412 }
413
414 fn convert_times(&self, times: &mut HashMap<String, f64>) {
416 for (_, time) in times.iter_mut() {
417 let adjusted_time = *time - self.longitude / 15.0;
418 let timestamp = self.utc_time + (adjusted_time * 3600.0 * 1000.0) as i64;
419 *time = self.round_time(timestamp as f64);
420 }
421 }
422
423 fn round_time(&self, timestamp: f64) -> f64 {
425 match self.rounding {
426 RoundingMethod::None => timestamp,
427 RoundingMethod::Up => (timestamp / 60000.0).ceil() * 60000.0,
428 RoundingMethod::Down => (timestamp / 60000.0).floor() * 60000.0,
429 RoundingMethod::Nearest => (timestamp / 60000.0).round() * 60000.0,
430 }
431 }
432
433 fn sun_position(&self, time: f64) -> (f64, f64) {
435 let d = self.utc_time as f64 / 86400000.0 - 10957.5 + time / 24.0 - self.longitude / 360.0;
436
437 let g = self.mod_angle(357.529 + 0.98560028 * d);
438 let q = self.mod_angle(280.459 + 0.98564736 * d);
439 let l = self.mod_angle(q + 1.915 * self.sin(g) + 0.020 * self.sin(2.0 * g));
440 let e = 23.439 - 0.00000036 * d;
441 let ra = self.mod_angle(self.arctan2(self.cos(e) * self.sin(l), self.cos(l))) / 15.0;
442
443 let declination = self.arcsin(self.sin(e) * self.sin(l));
444 let equation = q / 15.0 - ra;
445
446 (declination, equation)
447 }
448
449 fn mid_day(&self, time: f64) -> f64 {
451 let (_, equation) = self.sun_position(time);
452 self.mod_time(12.0 - equation)
453 }
454
455 fn angle_time(&self, angle: f64, time: f64, direction: f64) -> f64 {
457 let (declination, _) = self.sun_position(time);
458 let numerator = -self.sin(angle) - self.sin(self.latitude) * self.sin(declination);
459 let denominator = self.cos(self.latitude) * self.cos(declination);
460 let diff = self.arccos(numerator / denominator) / 15.0;
461
462 self.mid_day(time) + diff * direction
463 }
464
465 fn asr_angle(&self, time: f64) -> f64 {
467 let shadow_factor = match self.asr_method {
468 AsrMethod::Standard => 1.0,
469 AsrMethod::Hanafi => 2.0,
470 };
471
472 let (declination, _) = self.sun_position(time);
473 -self.arccot(shadow_factor + self.tan((self.latitude - declination).abs()))
474 }
475
476 fn adjust_high_lats(&mut self, times: &mut HashMap<String, f64>) {
478 if matches!(self.high_latitude_rule, HighLatitudeRule::None) {
479 return;
480 }
481
482 self.adjusted = false;
483 let night = 24.0 + times["sunrise"] - times["sunset"];
484
485 let fajr = self.adjust_time(times["fajr"], times["sunrise"], self.fajr_angle, night, -1.0);
486 let isha = self.adjust_time(times["isha"], times["sunset"], self.isha_angle, night, 1.0);
487 let maghrib = self.adjust_time(times["maghrib"], times["sunset"], self.maghrib_angle, night, 1.0);
488
489 times.insert("fajr".to_string(), fajr);
490 times.insert("isha".to_string(), isha);
491 times.insert("maghrib".to_string(), maghrib);
492 }
493
494 fn adjust_time(&mut self, time: f64, base: f64, angle: f64, night: f64, direction: f64) -> f64 {
496 let portion = match self.high_latitude_rule {
497 HighLatitudeRule::NightMiddle => night / 2.0,
498 HighLatitudeRule::OneSeventh => night / 7.0,
499 HighLatitudeRule::AngleBased => night * angle / 60.0,
500 HighLatitudeRule::None => 0.0,
501 };
502
503 let time_diff = (time - base) * direction;
504 if time.is_nan() || time_diff > portion {
505 self.adjusted = true;
506 base + portion * direction
507 } else {
508 time
509 }
510 }
511
512 fn format_times(&self, times: HashMap<String, f64>) -> PrayerTimes {
514 PrayerTimes {
515 fajr: self.format_time(times["fajr"]),
516 sunrise: self.format_time(times["sunrise"]),
517 dhuhr: self.format_time(times["dhuhr"]),
518 asr: self.format_time(times["asr"]),
519 sunset: self.format_time(times["sunset"]),
520 maghrib: self.format_time(times["maghrib"]),
521 isha: self.format_time(times["isha"]),
522 midnight: self.format_time(times["midnight"]),
523 }
524 }
525
526 fn format_time(&self, timestamp: f64) -> String {
528 if timestamp.is_nan() {
529 return "-----".to_string();
530 }
531
532 match self.time_format {
533 TimeFormat::Timestamp => (timestamp as i64).to_string(),
534 _ => self.time_to_string(timestamp as i64),
535 }
536 }
537
538 fn time_to_string(&self, timestamp: i64) -> String {
540 let offset_millis = self.utc_offset.map(|o| o as i64 * 60 * 1000).unwrap_or(0);
541 let adjusted_timestamp = timestamp + offset_millis;
542
543 let datetime = DateTime::from_timestamp_millis(adjusted_timestamp)
544 .unwrap_or_else(Utc::now);
545
546 let format_str = match self.time_format {
547 TimeFormat::Hour24 => "%H:%M",
548 TimeFormat::Hour12 => "%I:%M %p",
549 TimeFormat::Hour12NoSuffix => "%I:%M",
550 TimeFormat::Timestamp => return timestamp.to_string(),
551 };
552
553 match self.timezone {
554 Some(tz) => datetime.with_timezone(&tz).format(format_str).to_string(),
555 None => datetime.format(format_str).to_string(),
556 }
557 }
558
559 fn sin(&self, degrees: f64) -> f64 {
561 (degrees * std::f64::consts::PI / 180.0).sin()
562 }
563
564 fn cos(&self, degrees: f64) -> f64 {
565 (degrees * std::f64::consts::PI / 180.0).cos()
566 }
567
568 fn tan(&self, degrees: f64) -> f64 {
569 (degrees * std::f64::consts::PI / 180.0).tan()
570 }
571
572 fn arcsin(&self, x: f64) -> f64 {
573 x.asin() * 180.0 / std::f64::consts::PI
574 }
575
576 fn arccos(&self, x: f64) -> f64 {
577 x.acos() * 180.0 / std::f64::consts::PI
578 }
579
580 fn arccot(&self, x: f64) -> f64 {
581 (1.0 / x).atan() * 180.0 / std::f64::consts::PI
582 }
583
584 fn arctan2(&self, y: f64, x: f64) -> f64 {
585 y.atan2(x) * 180.0 / std::f64::consts::PI
586 }
587
588 fn mod_angle(&self, angle: f64) -> f64 {
590 ((angle % 360.0) + 360.0) % 360.0
591 }
592
593 fn mod_time(&self, time: f64) -> f64 {
594 ((time % 24.0) + 24.0) % 24.0
595 }
596}
597
598#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
600pub struct TuneAdjustments {
601 pub fajr: f64,
602 pub sunrise: f64,
603 pub dhuhr: f64,
604 pub asr: f64,
605 pub sunset: f64,
606 pub maghrib: f64,
607 pub isha: f64,
608 pub midnight: f64,
609}
610
611impl PrayTime {
612 pub fn tune_with(&mut self, tune: TuneAdjustments) -> &mut Self {
614 self.tune_fajr = tune.fajr;
615 self.tune_sunrise = tune.sunrise;
616 self.tune_dhuhr = tune.dhuhr;
617 self.tune_asr = tune.asr;
618 self.tune_sunset = tune.sunset;
619 self.tune_maghrib = tune.maghrib;
620 self.tune_isha = tune.isha;
621 self.tune_midnight = tune.midnight;
622 self
623 }
624}
625
626#[cfg(test)]
627mod tests {
628 use super::*;
629 use approx::assert_relative_eq;
630 use chrono::NaiveDate;
631
632 #[test]
633 fn test_new_praytime() {
634 let praytime = PrayTime::new("ISNA");
635 assert_eq!(praytime.method, CalculationMethod::ISNA);
636 assert_eq!(praytime.fajr_angle, 15.0);
637 assert_eq!(praytime.isha_angle, 15.0);
638 }
639
640 #[test]
641 fn test_method_parameters() {
642 let mut praytime = PrayTime::new("MWL");
643 assert_eq!(praytime.fajr_angle, 18.0);
644 assert_eq!(praytime.isha_angle, 17.0);
645
646 praytime.set_method("Makkah");
647 assert_eq!(praytime.method, CalculationMethod::Makkah);
648 assert_eq!(praytime.fajr_angle, 18.5);
649 assert_eq!(praytime.isha_minutes, 90.0);
650
651 praytime.set_method("Jafari");
652 assert_eq!(praytime.method, CalculationMethod::Jafari);
653 assert_eq!(praytime.fajr_angle, 16.0);
654 assert_eq!(praytime.maghrib_minutes, 4.0);
655 assert!(matches!(praytime.midnight_method, MidnightMethod::Jafari));
656 }
657
658 #[test]
659 fn test_location_setting() {
660 let mut praytime = PrayTime::new("ISNA");
661 praytime.location(43.0, -80.0);
662
663 assert_relative_eq!(praytime.latitude, 43.0, epsilon = 1e-10);
664 assert_relative_eq!(praytime.longitude, -80.0, epsilon = 1e-10);
665 }
666
667 #[test]
668 fn test_timezone_setting() {
669 let mut praytime = PrayTime::new("ISNA");
670 praytime.timezone("America/Toronto");
671
672 assert!(praytime.timezone.is_some());
673 assert!(praytime.utc_offset.is_none());
674 }
675
676 #[test]
677 fn test_utc_offset_setting() {
678 let mut praytime = PrayTime::new("ISNA");
679 praytime.utc_offset(-300); assert_eq!(praytime.utc_offset, Some(-300));
682 assert!(praytime.timezone.is_none());
683 }
684
685 #[test]
686 fn test_time_format_setting() {
687 let mut praytime = PrayTime::new("ISNA");
688
689 praytime.format(TimeFormat::Hour12);
690 assert_eq!(praytime.time_format, TimeFormat::Hour12);
691
692 praytime.format(TimeFormat::Hour24);
693 assert_eq!(praytime.time_format, TimeFormat::Hour24);
694 }
695
696 #[test]
697 fn test_trigonometric_functions() {
698 let praytime = PrayTime::new("ISNA");
699
700 assert_relative_eq!(praytime.sin(0.0), 0.0, epsilon = 1e-10);
702 assert_relative_eq!(praytime.sin(90.0), 1.0, epsilon = 1e-10);
703
704 assert_relative_eq!(praytime.cos(0.0), 1.0, epsilon = 1e-10);
705 assert_relative_eq!(praytime.cos(90.0), 0.0, epsilon = 1e-6);
706
707 assert_relative_eq!(praytime.tan(45.0), 1.0, epsilon = 1e-10);
708 }
709
710 #[test]
711 fn test_utility_functions() {
712 let praytime = PrayTime::new("ISNA");
713
714 assert_relative_eq!(praytime.mod_angle(450.0), 90.0, epsilon = 1e-10);
716 assert_relative_eq!(praytime.mod_angle(-90.0), 270.0, epsilon = 1e-10);
717
718 assert_relative_eq!(praytime.mod_time(25.0), 1.0, epsilon = 1e-10);
720 assert_relative_eq!(praytime.mod_time(-1.0), 23.0, epsilon = 1e-10);
721 }
722
723 #[test]
724 fn test_get_times_structure() {
725 let mut praytime = PrayTime::new("ISNA");
726 praytime.location(43.0, -80.0);
727 praytime.timezone("America/Toronto");
728
729 let date = NaiveDate::from_ymd_opt(2024, 6, 15).unwrap();
730 let times = praytime.get_times(Some(date));
731
732 assert_ne!(times.fajr, "-----");
734 assert_ne!(times.sunrise, "-----");
735 assert_ne!(times.dhuhr, "-----");
736 assert_ne!(times.asr, "-----");
737 assert_ne!(times.sunset, "-----");
738 assert_ne!(times.maghrib, "-----");
739 assert_ne!(times.isha, "-----");
740 assert_ne!(times.midnight, "-----");
741 }
742}