praytime_rs/
lib.rs

1//! # PrayTime - Prayer Times Calculator
2//!
3//! A Rust implementation of the PrayTimes.org library for calculating Islamic prayer times.
4//! 
5//! This crate provides accurate prayer time calculations based on astronomical formulas
6//! and supports multiple calculation methods used around the world.
7//!
8//! ## Basic Usage
9//!
10//! ```rust
11//! use praytime_rs::PrayTime;
12//!
13//! let mut praytime = PrayTime::new("ISNA");
14//! praytime.location(43.0, -80.0).timezone("America/Toronto");
15//! let times = praytime.get_times(None);
16//! ```
17
18use chrono::{DateTime, NaiveDate, Utc};
19use chrono_tz::Tz;
20use serde::{Deserialize, Serialize};
21use std::collections::HashMap;
22
23/// Prayer time calculation methods
24#[derive(Debug, Clone, Copy, PartialEq)]
25pub enum CalculationMethod {
26    /// Muslim World League
27    MWL,
28    /// Islamic Society of North America
29    ISNA,
30    /// Egyptian General Authority of Survey
31    Egypt,
32    /// Umm Al-Qura University, Makkah
33    Makkah,
34    /// University of Islamic Sciences, Karachi
35    Karachi,
36    /// Institute of Geophysics, University of Tehran
37    Tehran,
38    /// Shia Ithna-Ashari (Jafari)
39    Jafari,
40    /// France
41    France,
42    /// Russia
43    Russia,
44    /// Singapore
45    Singapore,
46}
47
48/// High latitude adjustment method
49#[derive(Debug, Clone, Copy, PartialEq)]
50pub enum HighLatitudeRule {
51    /// No adjustment
52    None,
53    /// Middle of night
54    NightMiddle,
55    /// 1/7th of night
56    OneSeventh,
57    /// Angle-based method
58    AngleBased,
59}
60
61/// Asr calculation method
62#[derive(Debug, Clone, Copy, PartialEq)]
63pub enum AsrMethod {
64    /// Standard (Shafi, Maliki, Hanbali)
65    Standard,
66    /// Hanafi
67    Hanafi,
68}
69
70/// Midnight calculation method
71#[derive(Debug, Clone, Copy, PartialEq)]
72pub enum MidnightMethod {
73    /// Standard (Mid Sunset to Sunrise)
74    Standard,
75    /// Jafari (Mid Sunset to Fajr)
76    Jafari,
77}
78
79/// Time format options
80#[derive(Debug, Clone, Copy, PartialEq)]
81pub enum TimeFormat {
82    /// 24-hour format
83    Hour24,
84    /// 12-hour format with AM/PM
85    Hour12,
86    /// 12-hour format without AM/PM
87    Hour12NoSuffix,
88    /// Unix timestamp
89    Timestamp,
90}
91
92/// Rounding method for times
93#[derive(Debug, Clone, Copy, PartialEq)]
94pub enum RoundingMethod {
95    /// No rounding
96    None,
97    /// Round to nearest minute
98    Nearest,
99    /// Round up
100    Up,
101    /// Round down
102    Down,
103}
104
105/// Prayer times structure
106#[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/// Main PrayTime calculator
120#[derive(Debug, Clone)]
121pub struct PrayTime {
122    method: CalculationMethod,
123    latitude: f64,
124    longitude: f64,
125    timezone: Option<Tz>,
126    utc_offset: Option<i32>, // minutes
127    high_latitude_rule: HighLatitudeRule,
128    asr_method: AsrMethod,
129    midnight_method: MidnightMethod,
130    time_format: TimeFormat,
131    rounding: RoundingMethod,
132    iterations: u8,
133    
134    // Method parameters
135    fajr_angle: f64,
136    isha_angle: f64,
137    maghrib_angle: f64,
138    maghrib_minutes: f64,
139    isha_minutes: f64,
140    
141    // Time adjustments
142    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    // Calculation cache
152    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    /// Create a new PrayTime instance with the specified calculation method
164    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    /// Set calculation method
202    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        // Set default parameters first
218        self.fajr_angle = 18.0;
219        self.isha_angle = 14.0;
220        self.maghrib_angle = 0.833;
221        self.maghrib_minutes = 1.0; // Default '1 min' like JavaScript
222        self.isha_minutes = 0.0; // Will be overridden by specific methods
223        self.midnight_method = MidnightMethod::Standard;
224        
225        // Apply method-specific parameters
226        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    /// Set location coordinates
275    pub fn location(&mut self, latitude: f64, longitude: f64) -> &mut Self {
276        self.latitude = latitude;
277        self.longitude = longitude;
278        self
279    }
280
281    /// Set timezone
282    pub fn timezone(&mut self, tz_name: &str) -> &mut Self {
283        self.timezone = tz_name.parse().ok();
284        self.utc_offset = None; // Clear UTC offset when timezone is set
285        self
286    }
287
288    /// Set UTC offset in minutes
289    pub fn utc_offset(&mut self, minutes: i32) -> &mut Self {
290        self.utc_offset = Some(minutes);
291        self.timezone = None; // Clear timezone when UTC offset is set
292        self
293    }
294
295    /// Set time format
296    pub fn format(&mut self, format: TimeFormat) -> &mut Self {
297        self.time_format = format;
298        self
299    }
300
301    /// Set rounding method
302    pub fn rounding(&mut self, method: RoundingMethod) -> &mut Self {
303        self.rounding = method;
304        self
305    }
306
307    /// Set high latitude adjustment rule
308    pub fn high_latitude_rule(&mut self, rule: HighLatitudeRule) -> &mut Self {
309        self.high_latitude_rule = rule;
310        self
311    }
312
313    /// Set ASR calculation method
314    pub fn asr_method(&mut self, method: AsrMethod) -> &mut Self {
315        self.asr_method = method;
316        self
317    }
318
319    /// Get prayer times for a specific date
320    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    /// Compute prayer times
331    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(&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    /// Process prayer times through astronomical calculations
355    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    /// Update times with method-specific adjustments
380    fn update_times(&mut self, times: &mut HashMap<String, f64>) {
381        // Maghrib time adjustment (for methods that use minutes)
382        if self.maghrib_minutes > 1.0 { // Only if > default 1 min
383            times.insert("maghrib".to_string(), 
384                        times["sunset"] + self.maghrib_minutes / 60.0);
385        }
386
387        // Isha time adjustment (only for Makkah method)
388        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        // Midnight calculation for Jafari method
394        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    /// Apply time tuning adjustments
403    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    /// Convert times to timestamps
415    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    /// Round time according to rounding method
424    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    /// Compute sun position for given time
434    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    /// Compute midday time
450    fn mid_day(&self, time: f64) -> f64 {
451        let (_, equation) = self.sun_position(time);
452        self.mod_time(12.0 - equation)
453    }
454
455    /// Compute time when sun reaches a specific angle
456    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    /// Compute ASR angle
466    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    /// Adjust times for higher latitudes
477    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    /// Adjust individual time for higher latitudes
495    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    /// Format times according to the specified format
513    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    /// Format individual time
527    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    /// Convert timestamp to formatted time string
539    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    // Trigonometric functions (degrees)
560    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    // Utility functions
589    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/// Time tuning adjustments for all supported prayer times
599#[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    /// Preferred: tune prayer times using a grouping struct
613    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); // -5 hours in minutes
680        
681        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        // Test basic trigonometric functions
701        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        // Test angle modulo
715        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        // Test time modulo  
719        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        // Verify all fields are present and not invalid
733        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}