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/// # Arguments
193/// * `azimuth` - Azimuth angle in degrees
194///
195/// # Returns
196/// Normalized azimuth angle or error if invalid
197///
198/// # Errors
199/// Returns `ComputationError` if azimuth is not finite.
200pub fn check_azimuth(azimuth: f64) -> Result<f64> {
201    if !azimuth.is_finite() {
202        return Err(Error::computation_error("azimuth is not finite"));
203    }
204    Ok(normalize_degrees_0_to_360(azimuth))
205}
206
207/// Validates a zenith angle to be within the range [0, 180] degrees.
208///
209/// # Arguments
210/// * `zenith` - Zenith angle in degrees
211///
212/// # Returns
213/// The zenith angle if valid, or error if invalid
214///
215/// # Errors
216/// Returns `ComputationError` if zenith angle is not finite or outside valid range.
217pub fn check_zenith_angle(zenith: f64) -> Result<f64> {
218    if !zenith.is_finite() {
219        return Err(Error::computation_error("zenith angle is not finite"));
220    }
221    if !(0.0..=180.0).contains(&zenith) {
222        return Err(Error::computation_error(
223            "zenith angle must be between 0° and 180°",
224        ));
225    }
226    Ok(zenith)
227}
228
229/// Check if pressure and temperature parameters are usable for refraction correction.
230///
231/// # Arguments
232/// * `pressure` - Atmospheric pressure in millibars
233/// * `temperature` - Temperature in degrees Celsius
234///
235/// # Returns
236/// `true` if both parameters are finite and within reasonable ranges
237#[must_use]
238pub fn check_refraction_params_usable(pressure: f64, temperature: f64) -> bool {
239    pressure.is_finite()
240        && temperature.is_finite()
241        && pressure > 0.0
242        && pressure < 3000.0
243        && temperature > -273.0
244        && temperature < 273.0
245}
246
247#[cfg(test)]
248mod tests {
249    use super::*;
250
251    #[test]
252    fn test_latitude_validation() {
253        assert!(check_latitude(0.0).is_ok());
254        assert!(check_latitude(90.0).is_ok());
255        assert!(check_latitude(-90.0).is_ok());
256        assert!(check_latitude(45.5).is_ok());
257
258        assert!(check_latitude(91.0).is_err());
259        assert!(check_latitude(-91.0).is_err());
260        assert!(check_latitude(f64::NAN).is_err());
261        assert!(check_latitude(f64::INFINITY).is_err());
262    }
263
264    #[test]
265    fn test_longitude_validation() {
266        assert!(check_longitude(0.0).is_ok());
267        assert!(check_longitude(180.0).is_ok());
268        assert!(check_longitude(-180.0).is_ok());
269        assert!(check_longitude(122.5).is_ok());
270
271        assert!(check_longitude(181.0).is_err());
272        assert!(check_longitude(-181.0).is_err());
273        assert!(check_longitude(f64::NAN).is_err());
274        assert!(check_longitude(f64::INFINITY).is_err());
275    }
276
277    #[test]
278    fn test_pressure_validation() {
279        assert!(check_pressure(1013.25).is_ok());
280        assert!(check_pressure(1000.0).is_ok());
281        assert!(check_pressure(500.0).is_ok());
282
283        assert!(check_pressure(0.0).is_err());
284        assert!(check_pressure(-100.0).is_err());
285        assert!(check_pressure(3000.0).is_err());
286    }
287
288    #[test]
289    fn test_temperature_validation() {
290        assert!(check_temperature(15.0).is_ok());
291        assert!(check_temperature(0.0).is_ok());
292        assert!(check_temperature(-40.0).is_ok());
293        assert!(check_temperature(50.0).is_ok());
294
295        assert!(check_temperature(-300.0).is_err());
296        assert!(check_temperature(150.0).is_err());
297    }
298
299    #[test]
300    fn test_error_display() {
301        let err = Error::invalid_latitude(95.0);
302        assert_eq!(
303            err.to_string(),
304            "invalid latitude 95° (must be between -90° and +90°)"
305        );
306
307        let err = Error::invalid_longitude(185.0);
308        assert_eq!(
309            err.to_string(),
310            "invalid longitude 185° (must be between -180° and +180°)"
311        );
312
313        let err = Error::computation_error("convergence failed");
314        assert_eq!(err.to_string(), "computation error: convergence failed");
315    }
316
317    #[test]
318    fn test_check_azimuth() {
319        assert!(check_azimuth(0.0).is_ok());
320        assert!(check_azimuth(180.0).is_ok());
321        assert!(check_azimuth(360.0).is_ok());
322        assert!(check_azimuth(450.0).is_ok());
323        assert!(check_azimuth(-90.0).is_ok());
324
325        // Check normalization
326        assert_eq!(check_azimuth(-90.0).unwrap(), 270.0);
327        assert_eq!(check_azimuth(450.0).unwrap(), 90.0);
328
329        assert!(check_azimuth(f64::NAN).is_err());
330        assert!(check_azimuth(f64::INFINITY).is_err());
331    }
332
333    #[test]
334    fn test_check_zenith_angle() {
335        assert!(check_zenith_angle(0.0).is_ok());
336        assert!(check_zenith_angle(90.0).is_ok());
337        assert!(check_zenith_angle(180.0).is_ok());
338
339        assert!(check_zenith_angle(-1.0).is_err());
340        assert!(check_zenith_angle(181.0).is_err());
341        assert!(check_zenith_angle(f64::NAN).is_err());
342        assert!(check_zenith_angle(f64::INFINITY).is_err());
343    }
344}