Skip to main content

celestial_time/sidereal/
angle.rs

1use celestial_core::math::fmod;
2use std::fmt;
3
4#[cfg(feature = "serde")]
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, Copy, PartialEq)]
8#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
9pub struct SiderealAngle {
10    angle_hours: f64,
11    exact_radians: Option<f64>,
12}
13
14impl SiderealAngle {
15    pub fn from_hours(hours: f64) -> Self {
16        Self {
17            angle_hours: Self::normalize_hours(hours),
18            exact_radians: None,
19        }
20    }
21
22    pub fn from_degrees(degrees: f64) -> Self {
23        Self::from_hours(degrees / 15.0)
24    }
25
26    pub fn from_radians(radians: f64) -> Self {
27        Self::from_hours(radians * 12.0 / celestial_core::constants::PI)
28    }
29
30    pub(crate) fn from_radians_exact(radians: f64) -> Self {
31        let hours = radians * 12.0 / celestial_core::constants::PI;
32        Self {
33            angle_hours: Self::normalize_hours(hours),
34            exact_radians: Some(radians),
35        }
36    }
37
38    pub fn hours(&self) -> f64 {
39        self.angle_hours
40    }
41
42    pub fn degrees(&self) -> f64 {
43        self.angle_hours * 15.0
44    }
45
46    pub fn radians(&self) -> f64 {
47        if let Some(exact) = self.exact_radians {
48            exact
49        } else {
50            self.angle_hours * celestial_core::constants::PI / 12.0
51        }
52    }
53
54    fn normalize_hours(hours: f64) -> f64 {
55        let mut normalized = fmod(hours, 24.0);
56        if normalized < 0.0 {
57            normalized += 24.0;
58        }
59        normalized
60    }
61
62    pub fn hour_angle_to_target(&self, target_ra_hours: f64) -> f64 {
63        self.hours() - target_ra_hours
64    }
65}
66
67impl fmt::Display for SiderealAngle {
68    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69        write!(f, "{:.6}h", self.angle_hours)
70    }
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76
77    #[test]
78    fn test_angle_conversions() {
79        let angle = SiderealAngle::from_hours(6.0);
80
81        assert_eq!(angle.hours(), 6.0);
82        assert_eq!(angle.degrees(), 90.0);
83        assert!((angle.radians() - celestial_core::constants::HALF_PI).abs() < 1e-15);
84    }
85
86    #[test]
87    fn test_normalization() {
88        let angle1 = SiderealAngle::from_hours(25.5);
89        assert_eq!(angle1.hours(), 1.5);
90
91        let angle2 = SiderealAngle::from_hours(-1.5);
92        assert_eq!(angle2.hours(), 22.5);
93    }
94
95    #[test]
96    fn test_hour_angle_calculation() {
97        let lst = SiderealAngle::from_hours(12.0);
98        let target_ra = 6.0;
99        let hour_angle = lst.hour_angle_to_target(target_ra);
100        assert_eq!(hour_angle, 6.0);
101    }
102}