Skip to main content

astro_math/
error.rs

1//! Error types for astro-math calculations.
2//!
3//! Handles validation and error reporting for coordinate conversions,
4//! time calculations, and astronomical computations.
5//!
6//! # Error Types
7//!
8//! The main error type is [`AstroError`], which covers all possible errors in the crate:
9//!
10//! - **Coordinate errors**: Invalid RA, Dec, latitude, or longitude values
11//! - **Range errors**: Values outside acceptable ranges for calculations
12//! - **Format errors**: Invalid string formats (e.g., DMS parsing)
13//! - **Calculation errors**: Mathematical failures or edge cases
14//! - **Projection errors**: Points that cannot be projected
15//!
16//! # Examples
17//!
18//! ```
19//! use astro_math::error::{AstroError, validate_ra, validate_dec};
20//!
21//! // Validate coordinates before use
22//! match validate_ra(400.0) {
23//!     Ok(_) => println!("Valid RA"),
24//!     Err(e) => println!("Error: {}", e), // "Invalid RA: 400 (valid range: [0, 360))"
25//! }
26//!
27//! // Functions return Result types
28//! use astro_math::{ra_dec_to_alt_az, Location};
29//! use chrono::Utc;
30//!
31//! let location = Location { latitude_deg: 40.0, longitude_deg: -74.0, altitude_m: 0.0 };
32//! let result = ra_dec_to_alt_az(400.0, 45.0, Utc::now(), &location);
33//! 
34//! match result {
35//!     Ok((alt, az)) => println!("Alt: {}, Az: {}", alt, az),
36//!     Err(AstroError::InvalidCoordinate { coord_type, value, .. }) => {
37//!         println!("Invalid {}: {}", coord_type, value);
38//!     }
39//!     Err(e) => println!("Other error: {}", e),
40//! }
41//! ```
42
43use thiserror::Error;
44
45/// Main error type for astro-math operations.
46///
47/// This enum represents all possible errors that can occur during astronomical
48/// calculations. Each variant provides specific information about what went wrong.
49#[derive(Debug, Clone, PartialEq, Error)]
50pub enum AstroError {
51    /// Invalid coordinate value
52    #[error("Invalid {coord_type}: {value} (valid range: {valid_range})")]
53    InvalidCoordinate {
54        /// Type of coordinate (e.g., "RA", "Dec", "Latitude")
55        coord_type: &'static str,
56        /// The invalid value
57        value: f64,
58        /// Valid range description
59        valid_range: &'static str,
60    },
61    
62    /// Invalid time/date
63    #[error("Invalid date/time: {reason}")]
64    InvalidDateTime {
65        /// Description of the issue
66        reason: String,
67    },
68    
69    /// Calculation failed
70    #[error("Calculation error in {calculation}: {reason}")]
71    CalculationError {
72        /// What calculation failed
73        calculation: &'static str,
74        /// Why it failed
75        reason: String,
76    },
77    
78    /// Object never rises or sets
79    #[error("{}", if *.always_above { "Object is circumpolar (never sets)" } else { "Object never rises above horizon" })]
80    NeverRisesOrSets {
81        /// Whether it's always above or below horizon
82        always_above: bool,
83    },
84    
85    /// Invalid DMS string format
86    #[error("Invalid DMS format '{input}' (expected: {expected})")]
87    InvalidDmsFormat {
88        /// The invalid string
89        input: String,
90        /// Expected format
91        expected: &'static str,
92    },
93    
94    /// Value out of valid range
95    #[error("{parameter} value {value} is out of range [{min}, {max}]")]
96    OutOfRange {
97        /// Parameter name
98        parameter: &'static str,
99        /// The invalid value
100        value: f64,
101        /// Min value (inclusive)
102        min: f64,
103        /// Max value (inclusive)
104        max: f64,
105    },
106    
107    /// Projection error (e.g., point on opposite side of sky)
108    #[error("Projection error: {reason}")]
109    ProjectionError {
110        /// Description of the issue
111        reason: String,
112    },
113}
114
115/// Type alias for Results in this crate.
116/// 
117/// All fallible operations in astro-math return this Result type.
118pub type Result<T> = std::result::Result<T, AstroError>;
119
120/// Validate that a value is within a range.
121///
122/// # Arguments
123/// * `value` - The value to check
124/// * `min` - Minimum valid value (inclusive)
125/// * `max` - Maximum valid value (inclusive)
126/// * `parameter` - Name of the parameter for error messages
127///
128/// # Errors
129/// Returns `AstroError::OutOfRange` if the value is outside [min, max].
130///
131/// # Example
132/// ```
133/// use astro_math::error::validate_range;
134/// 
135/// assert!(validate_range(45.0, 0.0, 90.0, "altitude").is_ok());
136/// assert!(validate_range(100.0, 0.0, 90.0, "altitude").is_err());
137/// ```
138#[inline]
139pub fn validate_range(value: f64, min: f64, max: f64, parameter: &'static str) -> Result<()> {
140    if value < min || value > max {
141        Err(AstroError::OutOfRange { parameter, value, min, max })
142    } else {
143        Ok(())
144    }
145}
146
147/// Validate right ascension (0 <= RA < 360).
148///
149/// Right ascension must be in the range [0, 360) degrees.
150///
151/// # Errors
152/// Returns `AstroError::InvalidCoordinate` if RA is outside the valid range.
153///
154/// # Example
155/// ```
156/// use astro_math::error::validate_ra;
157/// 
158/// assert!(validate_ra(0.0).is_ok());
159/// assert!(validate_ra(359.9).is_ok());
160/// assert!(validate_ra(360.0).is_err()); // 360 is invalid (use 0 instead)
161/// assert!(validate_ra(-1.0).is_err());
162/// ```
163#[inline]
164pub fn validate_ra(ra: f64) -> Result<()> {
165    validate_finite(ra, "RA")?;
166    if !(0.0..360.0).contains(&ra) {
167        Err(AstroError::InvalidCoordinate {
168            coord_type: "RA",
169            value: ra,
170            valid_range: "[0, 360)",
171        })
172    } else {
173        Ok(())
174    }
175}
176
177/// Validate declination (-90 <= Dec <= 90).
178///
179/// Declination must be in the range [-90, 90] degrees.
180///
181/// # Errors
182/// Returns `AstroError::InvalidCoordinate` if Dec is outside the valid range.
183///
184/// # Example
185/// ```
186/// use astro_math::error::validate_dec;
187/// 
188/// assert!(validate_dec(0.0).is_ok());
189/// assert!(validate_dec(90.0).is_ok());
190/// assert!(validate_dec(-90.0).is_ok());
191/// assert!(validate_dec(91.0).is_err());
192/// assert!(validate_dec(-91.0).is_err());
193/// ```
194#[inline]
195pub fn validate_dec(dec: f64) -> Result<()> {
196    validate_finite(dec, "Declination")?;
197    if !(-90.0..=90.0).contains(&dec) {
198        Err(AstroError::InvalidCoordinate {
199            coord_type: "Declination",
200            value: dec,
201            valid_range: "[-90, 90]",
202        })
203    } else {
204        Ok(())
205    }
206}
207
208/// Validate that a floating point value is finite (not NaN or infinite)
209#[inline]
210pub fn validate_finite(value: f64, _parameter: &'static str) -> Result<()> {
211    if !value.is_finite() {
212        if value.is_nan() {
213            return Err(AstroError::CalculationError {
214                calculation: "numeric validation",
215                reason: "Value is NaN (Not a Number)".to_string(),
216            });
217        } else if value.is_infinite() {
218            return Err(AstroError::CalculationError {
219                calculation: "numeric validation", 
220                reason: format!("Value is infinite: {}", if value.is_sign_positive() { "+∞" } else { "-∞" }),
221            });
222        }
223    }
224    Ok(())
225}
226
227/// Comprehensive coordinate validation including finite check
228#[inline]
229pub fn validate_coordinate_safe(value: f64, min: f64, max: f64, parameter: &'static str) -> Result<()> {
230    validate_finite(value, parameter)?;
231    validate_range(value, min, max, parameter)
232}
233
234/// Validate latitude (-90 <= lat <= 90)
235#[inline]
236pub fn validate_latitude(lat: f64) -> Result<()> {
237    if !(-90.0..=90.0).contains(&lat) {
238        Err(AstroError::InvalidCoordinate {
239            coord_type: "Latitude",
240            value: lat,
241            valid_range: "[-90, 90]",
242        })
243    } else {
244        Ok(())
245    }
246}
247
248/// Validate longitude (-180 <= lon <= 180)
249#[inline]
250pub fn validate_longitude(lon: f64) -> Result<()> {
251    if !(-180.0..=180.0).contains(&lon) {
252        Err(AstroError::InvalidCoordinate {
253            coord_type: "Longitude",
254            value: lon,
255            valid_range: "[-180, 180]",
256        })
257    } else {
258        Ok(())
259    }
260}
261
262#[cfg(test)]
263mod tests {
264    use super::*;
265    
266    #[test]
267    fn test_error_display() {
268        let err = AstroError::InvalidCoordinate {
269            coord_type: "RA",
270            value: 400.0,
271            valid_range: "[0, 360)",
272        };
273        assert_eq!(err.to_string(), "Invalid RA: 400 (valid range: [0, 360))");
274    }
275    
276    #[test]
277    fn test_validate_ra() {
278        assert!(validate_ra(0.0).is_ok());
279        assert!(validate_ra(359.9).is_ok());
280        assert!(validate_ra(-1.0).is_err());
281        assert!(validate_ra(360.0).is_err());
282    }
283    
284    #[test]
285    fn test_validate_dec() {
286        assert!(validate_dec(0.0).is_ok());
287        assert!(validate_dec(90.0).is_ok());
288        assert!(validate_dec(-90.0).is_ok());
289        assert!(validate_dec(91.0).is_err());
290        assert!(validate_dec(-91.0).is_err());
291    }
292}