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}