Skip to main content

celestial_core/angle/
validate.rs

1use super::core::Angle;
2use crate::constants::{HALF_PI, PI};
3use crate::{AstroError, MathErrorKind};
4
5pub fn validate_right_ascension(angle: Angle) -> Result<Angle, AstroError> {
6    let rad = angle.radians();
7    if rad.is_finite() {
8        let normalized = super::normalize::wrap_0_2pi(rad);
9        return Ok(Angle::from_radians(normalized));
10    }
11
12    Err(AstroError::math_error(
13        "validate_right_ascension",
14        MathErrorKind::NotFinite,
15        "RA Not Finite",
16    ))
17}
18
19/// Validates declination angle.
20///
21/// - `beyond_pole = false`: standard range [-90°, +90°]
22/// - `beyond_pole = true`: extended range [-180°, +180°] for GEM pier-flipped observations
23///
24/// The extended range supports the TPOINT beyond-the-pole convention where German Equatorial
25/// Mounts use Dec values from 90° to 180° for pier-flipped observations.
26pub fn validate_declination(angle: Angle, beyond_pole: bool) -> Result<Angle, AstroError> {
27    let rad = angle.radians();
28    if !rad.is_finite() {
29        return Err(AstroError::math_error(
30            "validate_declination",
31            MathErrorKind::NotFinite,
32            "Dec not Finite",
33        ));
34    }
35
36    let (limit, range_desc) = if beyond_pole {
37        (PI, "[-180°, +180°]")
38    } else {
39        (HALF_PI, "[-90°, +90°]")
40    };
41
42    if (-limit..=limit).contains(&rad) {
43        return Ok(angle);
44    }
45
46    Err(AstroError::math_error(
47        "validate_declination",
48        MathErrorKind::OutOfRange,
49        &format!("Dec {:.2}° out of range {}", angle.degrees(), range_desc),
50    ))
51}
52
53pub fn validate_latitude(angle: Angle) -> Result<Angle, AstroError> {
54    validate_declination(angle, false)
55}
56
57pub fn validate_longitude(angle: Angle, normalize: bool) -> Result<Angle, AstroError> {
58    let rad = angle.radians();
59    if !rad.is_finite() {
60        return Err(AstroError::math_error(
61            "validate_longitude",
62            MathErrorKind::NotFinite,
63            "Lon not finite",
64        ));
65    }
66
67    if normalize {
68        let normalized = super::normalize::wrap_0_2pi(rad);
69        return Ok(Angle::from_radians(normalized));
70    }
71
72    if (-PI..=PI).contains(&rad) {
73        return Ok(angle);
74    }
75
76    Err(AstroError::math_error(
77        "validate_longitude",
78        MathErrorKind::OutOfRange,
79        "Lon out of range",
80    ))
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86    use crate::constants::TWOPI;
87
88    #[test]
89    fn test_validate_right_ascension_valid() {
90        let angle = Angle::from_degrees(45.0);
91        let result = validate_right_ascension(angle);
92        assert!(result.is_ok());
93    }
94
95    #[test]
96    fn test_validate_right_ascension_not_finite() {
97        let angle = Angle::from_radians(f64::NAN);
98        let result = validate_right_ascension(angle);
99        assert!(result.is_err());
100        if let Err(AstroError::MathError { kind, .. }) = result {
101            assert_eq!(kind, MathErrorKind::NotFinite);
102        } else {
103            panic!("Expected MathError with NotFinite");
104        }
105    }
106
107    #[test]
108    fn test_validate_right_ascension_infinite() {
109        let angle = Angle::from_radians(f64::INFINITY);
110        let result = validate_right_ascension(angle);
111        assert!(result.is_err());
112        if let Err(AstroError::MathError { kind, .. }) = result {
113            assert_eq!(kind, MathErrorKind::NotFinite);
114        } else {
115            panic!("Expected MathError with NotFinite");
116        }
117    }
118
119    #[test]
120    fn test_validate_declination() {
121        // Valid standard range
122        assert!(validate_declination(Angle::from_degrees(45.0), false).is_ok());
123        assert!(validate_declination(Angle::from_degrees(-90.0), false).is_ok());
124
125        // Out of standard range
126        assert!(validate_declination(Angle::from_degrees(95.0), false).is_err());
127
128        // Valid with beyond_pole
129        assert!(validate_declination(Angle::from_degrees(120.0), true).is_ok());
130
131        // Not finite
132        assert!(validate_declination(Angle::from_radians(f64::NAN), false).is_err());
133    }
134
135    #[test]
136    fn test_validate_latitude_delegates_to_declination() {
137        let valid = Angle::from_degrees(45.0);
138        assert!(validate_latitude(valid).is_ok());
139
140        let invalid = Angle::from_degrees(95.0);
141        assert!(validate_latitude(invalid).is_err());
142    }
143
144    #[test]
145    fn test_validate_longitude_valid() {
146        let angle = Angle::from_degrees(45.0);
147        let result = validate_longitude(angle, false);
148        assert!(result.is_ok());
149    }
150
151    #[test]
152    fn test_validate_longitude_not_finite() {
153        let angle = Angle::from_radians(f64::NAN);
154        let result = validate_longitude(angle, false);
155        assert!(result.is_err());
156        if let Err(AstroError::MathError { kind, .. }) = result {
157            assert_eq!(kind, MathErrorKind::NotFinite);
158        } else {
159            panic!("Expected MathError with NotFinite");
160        }
161    }
162
163    #[test]
164    fn test_validate_longitude_normalized() {
165        let angle = Angle::from_degrees(370.0);
166        let result = validate_longitude(angle, true);
167        assert!(result.is_ok());
168        let normalized = result.unwrap();
169        assert!(normalized.radians() >= 0.0 && normalized.radians() < TWOPI);
170    }
171
172    #[test]
173    fn test_validate_longitude_out_of_range() {
174        let angle = Angle::from_degrees(190.0);
175        let result = validate_longitude(angle, false);
176        assert!(result.is_err());
177        if let Err(AstroError::MathError { kind, .. }) = result {
178            assert_eq!(kind, MathErrorKind::OutOfRange);
179        } else {
180            panic!("Expected MathError with OutOfRange");
181        }
182    }
183}