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
92impl Error {
93    /// Creates an invalid latitude error.
94    #[must_use]
95    pub const fn invalid_latitude(value: f64) -> Self {
96        Self::InvalidLatitude { value }
97    }
98
99    /// Creates an invalid longitude error.
100    #[must_use]
101    pub const fn invalid_longitude(value: f64) -> Self {
102        Self::InvalidLongitude { value }
103    }
104
105    /// Creates an invalid elevation angle error.
106    #[must_use]
107    pub const fn invalid_elevation_angle(value: f64) -> Self {
108        Self::InvalidElevationAngle { value }
109    }
110
111    /// Creates an invalid pressure error.
112    #[must_use]
113    pub const fn invalid_pressure(value: f64) -> Self {
114        Self::InvalidPressure { value }
115    }
116
117    /// Creates an invalid temperature error.
118    #[must_use]
119    pub const fn invalid_temperature(value: f64) -> Self {
120        Self::InvalidTemperature { value }
121    }
122
123    /// Creates an invalid date/time error.
124    #[must_use]
125    pub const fn invalid_datetime(message: &'static str) -> Self {
126        Self::InvalidDateTime { message }
127    }
128
129    /// Creates a computation error.
130    #[must_use]
131    pub const fn computation_error(message: &'static str) -> Self {
132        Self::ComputationError { message }
133    }
134}
135
136/// Validates latitude is within the valid range (-90 to +90 degrees).
137///
138/// # Errors
139/// Returns `InvalidLatitude` if latitude is outside -90 to +90 degrees.
140pub fn check_latitude(latitude: f64) -> Result<()> {
141    if !(-90.0..=90.0).contains(&latitude) {
142        return Err(Error::invalid_latitude(latitude));
143    }
144    Ok(())
145}
146
147/// Validates longitude is within the valid range (-180 to +180 degrees).
148///
149/// # Errors
150/// Returns `InvalidLongitude` if longitude is outside -180 to +180 degrees.
151pub fn check_longitude(longitude: f64) -> Result<()> {
152    if !(-180.0..=180.0).contains(&longitude) {
153        return Err(Error::invalid_longitude(longitude));
154    }
155    Ok(())
156}
157
158/// Validates both latitude and longitude are within valid ranges.
159///
160/// # Errors
161/// Returns `InvalidLatitude` or `InvalidLongitude` for out-of-range coordinates.
162pub fn check_coordinates(latitude: f64, longitude: f64) -> Result<()> {
163    check_latitude(latitude)?;
164    check_longitude(longitude)?;
165    Ok(())
166}
167
168/// Validates pressure is positive and reasonable for atmospheric calculations.
169///
170/// # Errors
171/// Returns `InvalidPressure` if pressure is not between 1 and 2000 hPa.
172pub fn check_pressure(pressure: f64) -> Result<()> {
173    if pressure <= 0.0 || pressure > 2000.0 {
174        return Err(Error::invalid_pressure(pressure));
175    }
176    Ok(())
177}
178
179/// Validates temperature is above absolute zero and reasonable for atmospheric calculations.
180///
181/// # Errors
182/// Returns `InvalidTemperature` if temperature is outside -273.15 to 100°C.
183pub fn check_temperature(temperature: f64) -> Result<()> {
184    if !(-273.15..=100.0).contains(&temperature) {
185        return Err(Error::invalid_temperature(temperature));
186    }
187    Ok(())
188}
189
190/// Validates and normalizes an azimuth angle to the range [0, 360) degrees.
191///
192/// # Errors
193/// Returns `ComputationError` if azimuth is not finite.
194pub fn check_azimuth(azimuth: f64) -> Result<f64> {
195    if !azimuth.is_finite() {
196        return Err(Error::computation_error("azimuth is not finite"));
197    }
198    Ok(normalize_degrees_0_to_360(azimuth))
199}
200
201/// Validates a zenith angle to be within the range [0, 180] degrees.
202///
203/// # Errors
204/// Returns `ComputationError` if zenith angle is not finite or outside valid range.
205pub fn check_zenith_angle(zenith: f64) -> Result<f64> {
206    if !zenith.is_finite() {
207        return Err(Error::computation_error("zenith angle is not finite"));
208    }
209    if !(0.0..=180.0).contains(&zenith) {
210        return Err(Error::computation_error(
211            "zenith angle must be between 0° and 180°",
212        ));
213    }
214    Ok(zenith)
215}
216
217/// Check if pressure and temperature parameters are usable for refraction correction.
218#[must_use]
219pub fn check_refraction_params_usable(pressure: f64, temperature: f64) -> bool {
220    pressure.is_finite()
221        && temperature.is_finite()
222        && pressure > 0.0
223        && pressure < 3000.0
224        && temperature > -273.0
225        && temperature < 273.0
226}
227
228#[cfg(test)]
229mod tests {
230    use super::*;
231
232    #[test]
233    fn test_latitude_validation() {
234        assert!(check_latitude(0.0).is_ok());
235        assert!(check_latitude(90.0).is_ok());
236        assert!(check_latitude(-90.0).is_ok());
237        assert!(check_latitude(45.5).is_ok());
238
239        assert!(check_latitude(91.0).is_err());
240        assert!(check_latitude(-91.0).is_err());
241        assert!(check_latitude(f64::NAN).is_err());
242        assert!(check_latitude(f64::INFINITY).is_err());
243    }
244
245    #[test]
246    fn test_longitude_validation() {
247        assert!(check_longitude(0.0).is_ok());
248        assert!(check_longitude(180.0).is_ok());
249        assert!(check_longitude(-180.0).is_ok());
250        assert!(check_longitude(122.5).is_ok());
251
252        assert!(check_longitude(181.0).is_err());
253        assert!(check_longitude(-181.0).is_err());
254        assert!(check_longitude(f64::NAN).is_err());
255        assert!(check_longitude(f64::INFINITY).is_err());
256    }
257
258    #[test]
259    fn test_pressure_validation() {
260        assert!(check_pressure(1013.25).is_ok());
261        assert!(check_pressure(1000.0).is_ok());
262        assert!(check_pressure(500.0).is_ok());
263
264        assert!(check_pressure(0.0).is_err());
265        assert!(check_pressure(-100.0).is_err());
266        assert!(check_pressure(3000.0).is_err());
267    }
268
269    #[test]
270    fn test_temperature_validation() {
271        assert!(check_temperature(15.0).is_ok());
272        assert!(check_temperature(0.0).is_ok());
273        assert!(check_temperature(-40.0).is_ok());
274        assert!(check_temperature(50.0).is_ok());
275
276        assert!(check_temperature(-300.0).is_err());
277        assert!(check_temperature(150.0).is_err());
278    }
279
280    #[test]
281    #[cfg(feature = "std")]
282    fn test_error_display() {
283        let err = Error::invalid_latitude(95.0);
284        assert_eq!(
285            err.to_string(),
286            "invalid latitude 95° (must be between -90° and +90°)"
287        );
288
289        let err = Error::invalid_longitude(185.0);
290        assert_eq!(
291            err.to_string(),
292            "invalid longitude 185° (must be between -180° and +180°)"
293        );
294
295        let err = Error::computation_error("convergence failed");
296        assert_eq!(err.to_string(), "computation error: convergence failed");
297    }
298
299    #[test]
300    fn test_check_azimuth() {
301        assert!(check_azimuth(0.0).is_ok());
302        assert!(check_azimuth(180.0).is_ok());
303        assert!(check_azimuth(360.0).is_ok());
304        assert!(check_azimuth(450.0).is_ok());
305        assert!(check_azimuth(-90.0).is_ok());
306
307        // Check normalization
308        assert_eq!(check_azimuth(-90.0).unwrap(), 270.0);
309        assert_eq!(check_azimuth(450.0).unwrap(), 90.0);
310
311        assert!(check_azimuth(f64::NAN).is_err());
312        assert!(check_azimuth(f64::INFINITY).is_err());
313    }
314
315    #[test]
316    fn test_check_zenith_angle() {
317        assert!(check_zenith_angle(0.0).is_ok());
318        assert!(check_zenith_angle(90.0).is_ok());
319        assert!(check_zenith_angle(180.0).is_ok());
320
321        assert!(check_zenith_angle(-1.0).is_err());
322        assert!(check_zenith_angle(181.0).is_err());
323        assert!(check_zenith_angle(f64::NAN).is_err());
324        assert!(check_zenith_angle(f64::INFINITY).is_err());
325    }
326}