Skip to main content

solar_positioning/
error.rs

1//! Error types for the solar positioning library.
2
3use crate::math::normalize_degrees_0_to_360;
4use core::fmt;
5
6/// Result type alias for operations in this crate.
7pub type Result<T> = core::result::Result<T, Error>;
8
9/// Errors that can occur during solar position calculations.
10#[derive(Debug, Clone, PartialEq)]
11pub enum Error {
12    /// Invalid latitude value (must be between -90 and +90 degrees).
13    InvalidLatitude {
14        /// The invalid latitude value provided.
15        value: f64,
16    },
17    /// Invalid longitude value (must be between -180 and +180 degrees).
18    InvalidLongitude {
19        /// The invalid longitude value provided.
20        value: f64,
21    },
22    /// Invalid elevation angle for sunrise/sunset calculations.
23    InvalidElevationAngle {
24        /// The invalid elevation angle value provided.
25        value: f64,
26    },
27    /// Invalid pressure value for atmospheric refraction calculations.
28    InvalidPressure {
29        /// The invalid pressure value provided.
30        value: f64,
31    },
32    /// Invalid temperature value for atmospheric refraction calculations.
33    InvalidTemperature {
34        /// The invalid temperature value provided.
35        value: f64,
36    },
37    /// Invalid date/time for the algorithm's valid range.
38    InvalidDateTime {
39        /// Description of the date/time constraint violation.
40        message: &'static str,
41    },
42    /// Numerical computation error (e.g., convergence failure).
43    ComputationError {
44        /// Description of the computation error.
45        message: &'static str,
46    },
47}
48
49impl fmt::Display for Error {
50    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51        match self {
52            Self::InvalidLatitude { value } => {
53                write!(
54                    f,
55                    "invalid latitude {value}° (must be between -90° and +90°)"
56                )
57            }
58            Self::InvalidLongitude { value } => {
59                write!(
60                    f,
61                    "invalid longitude {value}° (must be between -180° and +180°)"
62                )
63            }
64            Self::InvalidElevationAngle { value } => {
65                write!(
66                    f,
67                    "invalid elevation angle {value}° (must be between -90° and +90°)"
68                )
69            }
70            Self::InvalidPressure { value } => {
71                write!(f, "invalid pressure {value} mbar (must be positive)")
72            }
73            Self::InvalidTemperature { value } => {
74                write!(
75                    f,
76                    "invalid temperature {value}°C (must be above absolute zero)"
77                )
78            }
79            Self::InvalidDateTime { message } => {
80                write!(f, "invalid date/time: {message}")
81            }
82            Self::ComputationError { message } => {
83                write!(f, "computation error: {message}")
84            }
85        }
86    }
87}
88
89#[cfg(feature = "std")]
90impl std::error::Error for Error {}
91
92/// Validates latitude is within the valid range (-90 to +90 degrees).
93///
94/// # Errors
95/// Returns `InvalidLatitude` if latitude is outside -90 to +90 degrees.
96pub fn check_latitude(latitude: f64) -> Result<()> {
97    if !(-90.0..=90.0).contains(&latitude) {
98        return Err(Error::InvalidLatitude { value: latitude });
99    }
100    Ok(())
101}
102
103/// Validates longitude is within the valid range (-180 to +180 degrees).
104///
105/// # Errors
106/// Returns `InvalidLongitude` if longitude is outside -180 to +180 degrees.
107pub fn check_longitude(longitude: f64) -> Result<()> {
108    if !(-180.0..=180.0).contains(&longitude) {
109        return Err(Error::InvalidLongitude { value: longitude });
110    }
111    Ok(())
112}
113
114/// Validates both latitude and longitude are within valid ranges.
115///
116/// # Errors
117/// Returns `InvalidLatitude` or `InvalidLongitude` for out-of-range coordinates.
118pub fn check_coordinates(latitude: f64, longitude: f64) -> Result<()> {
119    check_latitude(latitude)?;
120    check_longitude(longitude)
121}
122
123/// Validates an elevation angle is finite and within the valid range (-90 to +90 degrees).
124///
125/// # Errors
126/// Returns `InvalidElevationAngle` if elevation is not finite or outside -90 to +90 degrees.
127pub fn check_elevation_angle(elevation_angle: f64) -> Result<()> {
128    if !elevation_angle.is_finite() || !(-90.0..=90.0).contains(&elevation_angle) {
129        return Err(Error::InvalidElevationAngle {
130            value: elevation_angle,
131        });
132    }
133    Ok(())
134}
135
136/// Validates pressure is positive and reasonable for atmospheric calculations.
137///
138/// # Errors
139/// Returns `InvalidPressure` if pressure is not between 1 and 2000 hPa.
140pub fn check_pressure(pressure: f64) -> Result<()> {
141    if !pressure.is_finite() || pressure <= 0.0 || pressure > 2000.0 {
142        return Err(Error::InvalidPressure { value: pressure });
143    }
144    Ok(())
145}
146
147/// Validates temperature is above absolute zero and reasonable for atmospheric calculations.
148///
149/// # Errors
150/// Returns `InvalidTemperature` if temperature is outside -273.15 to 100°C.
151pub fn check_temperature(temperature: f64) -> Result<()> {
152    if !(-273.15..=100.0).contains(&temperature) {
153        return Err(Error::InvalidTemperature { value: temperature });
154    }
155    Ok(())
156}
157
158/// Validates and normalizes an azimuth angle to the range [0, 360) degrees.
159///
160/// # Errors
161/// Returns `ComputationError` if azimuth is not finite.
162pub fn check_azimuth(azimuth: f64) -> Result<f64> {
163    if !azimuth.is_finite() {
164        return Err(Error::ComputationError {
165            message: "azimuth is not finite",
166        });
167    }
168    Ok(normalize_degrees_0_to_360(azimuth))
169}
170
171/// Validates a zenith angle to be within the range [0, 180] degrees.
172///
173/// # Errors
174/// Returns `ComputationError` if zenith angle is not finite or outside valid range.
175pub fn check_zenith_angle(zenith: f64) -> Result<f64> {
176    if !zenith.is_finite() {
177        return Err(Error::ComputationError {
178            message: "zenith angle is not finite",
179        });
180    }
181    if !(0.0..=180.0).contains(&zenith) {
182        return Err(Error::ComputationError {
183            message: "zenith angle must be between 0° and 180°",
184        });
185    }
186    Ok(zenith)
187}
188
189#[cfg(test)]
190mod tests {
191    use super::*;
192
193    #[test]
194    fn test_latitude_validation() {
195        assert!(check_latitude(0.0).is_ok());
196        assert!(check_latitude(90.0).is_ok());
197        assert!(check_latitude(-90.0).is_ok());
198        assert!(check_latitude(45.5).is_ok());
199
200        assert!(check_latitude(91.0).is_err());
201        assert!(check_latitude(-91.0).is_err());
202        assert!(check_latitude(f64::NAN).is_err());
203        assert!(check_latitude(f64::INFINITY).is_err());
204    }
205
206    #[test]
207    fn test_longitude_validation() {
208        assert!(check_longitude(0.0).is_ok());
209        assert!(check_longitude(180.0).is_ok());
210        assert!(check_longitude(-180.0).is_ok());
211        assert!(check_longitude(122.5).is_ok());
212
213        assert!(check_longitude(181.0).is_err());
214        assert!(check_longitude(-181.0).is_err());
215        assert!(check_longitude(f64::NAN).is_err());
216        assert!(check_longitude(f64::INFINITY).is_err());
217    }
218
219    #[test]
220    fn test_pressure_validation() {
221        assert!(check_pressure(1013.25).is_ok());
222        assert!(check_pressure(1000.0).is_ok());
223        assert!(check_pressure(500.0).is_ok());
224
225        assert!(check_pressure(0.0).is_err());
226        assert!(check_pressure(-100.0).is_err());
227        assert!(check_pressure(3000.0).is_err());
228        assert!(check_pressure(f64::NAN).is_err());
229        assert!(check_pressure(f64::INFINITY).is_err());
230    }
231
232    #[test]
233    fn test_elevation_angle_validation() {
234        assert!(check_elevation_angle(-90.0).is_ok());
235        assert!(check_elevation_angle(0.0).is_ok());
236        assert!(check_elevation_angle(90.0).is_ok());
237
238        assert!(check_elevation_angle(-91.0).is_err());
239        assert!(check_elevation_angle(91.0).is_err());
240        assert!(check_elevation_angle(f64::NAN).is_err());
241        assert!(check_elevation_angle(f64::INFINITY).is_err());
242    }
243
244    #[test]
245    fn test_temperature_validation() {
246        assert!(check_temperature(15.0).is_ok());
247        assert!(check_temperature(0.0).is_ok());
248        assert!(check_temperature(-40.0).is_ok());
249        assert!(check_temperature(50.0).is_ok());
250
251        assert!(check_temperature(-300.0).is_err());
252        assert!(check_temperature(150.0).is_err());
253    }
254
255    #[test]
256    #[cfg(feature = "std")]
257    fn test_error_display() {
258        let err = Error::InvalidLatitude { value: 95.0 };
259        assert_eq!(
260            err.to_string(),
261            "invalid latitude 95° (must be between -90° and +90°)"
262        );
263
264        let err = Error::InvalidLongitude { value: 185.0 };
265        assert_eq!(
266            err.to_string(),
267            "invalid longitude 185° (must be between -180° and +180°)"
268        );
269
270        let err = Error::ComputationError {
271            message: "convergence failed",
272        };
273        assert_eq!(err.to_string(), "computation error: convergence failed");
274    }
275
276    #[test]
277    fn test_check_azimuth() {
278        assert!(check_azimuth(0.0).is_ok());
279        assert!(check_azimuth(180.0).is_ok());
280        assert!(check_azimuth(360.0).is_ok());
281        assert!(check_azimuth(450.0).is_ok());
282        assert!(check_azimuth(-90.0).is_ok());
283
284        // Check normalization
285        assert_eq!(check_azimuth(-90.0).unwrap(), 270.0);
286        assert_eq!(check_azimuth(450.0).unwrap(), 90.0);
287
288        assert!(check_azimuth(f64::NAN).is_err());
289        assert!(check_azimuth(f64::INFINITY).is_err());
290    }
291
292    #[test]
293    fn test_check_zenith_angle() {
294        assert!(check_zenith_angle(0.0).is_ok());
295        assert!(check_zenith_angle(90.0).is_ok());
296        assert!(check_zenith_angle(180.0).is_ok());
297
298        assert!(check_zenith_angle(-1.0).is_err());
299        assert!(check_zenith_angle(181.0).is_err());
300        assert!(check_zenith_angle(f64::NAN).is_err());
301        assert!(check_zenith_angle(f64::INFINITY).is_err());
302    }
303}