use thiserror::Error;
#[derive(Debug, Clone, PartialEq, Error)]
pub enum AstroError {
#[error("Invalid {coord_type}: {value} (valid range: {valid_range})")]
InvalidCoordinate {
coord_type: &'static str,
value: f64,
valid_range: &'static str,
},
#[error("Invalid date/time: {reason}")]
InvalidDateTime {
reason: String,
},
#[error("Calculation error in {calculation}: {reason}")]
CalculationError {
calculation: &'static str,
reason: String,
},
#[error("{}", if *.always_above { "Object is circumpolar (never sets)" } else { "Object never rises above horizon" })]
NeverRisesOrSets {
always_above: bool,
},
#[error("Invalid DMS format '{input}' (expected: {expected})")]
InvalidDmsFormat {
input: String,
expected: &'static str,
},
#[error("{parameter} value {value} is out of range [{min}, {max}]")]
OutOfRange {
parameter: &'static str,
value: f64,
min: f64,
max: f64,
},
#[error("Projection error: {reason}")]
ProjectionError {
reason: String,
},
}
pub type Result<T> = std::result::Result<T, AstroError>;
#[inline]
pub fn validate_range(value: f64, min: f64, max: f64, parameter: &'static str) -> Result<()> {
if value < min || value > max {
Err(AstroError::OutOfRange { parameter, value, min, max })
} else {
Ok(())
}
}
#[inline]
pub fn validate_ra(ra: f64) -> Result<()> {
validate_finite(ra, "RA")?;
if !(0.0..360.0).contains(&ra) {
Err(AstroError::InvalidCoordinate {
coord_type: "RA",
value: ra,
valid_range: "[0, 360)",
})
} else {
Ok(())
}
}
#[inline]
pub fn validate_dec(dec: f64) -> Result<()> {
validate_finite(dec, "Declination")?;
if !(-90.0..=90.0).contains(&dec) {
Err(AstroError::InvalidCoordinate {
coord_type: "Declination",
value: dec,
valid_range: "[-90, 90]",
})
} else {
Ok(())
}
}
#[inline]
pub fn validate_finite(value: f64, _parameter: &'static str) -> Result<()> {
if !value.is_finite() {
if value.is_nan() {
return Err(AstroError::CalculationError {
calculation: "numeric validation",
reason: "Value is NaN (Not a Number)".to_string(),
});
} else if value.is_infinite() {
return Err(AstroError::CalculationError {
calculation: "numeric validation",
reason: format!("Value is infinite: {}", if value.is_sign_positive() { "+∞" } else { "-∞" }),
});
}
}
Ok(())
}
#[inline]
pub fn validate_coordinate_safe(value: f64, min: f64, max: f64, parameter: &'static str) -> Result<()> {
validate_finite(value, parameter)?;
validate_range(value, min, max, parameter)
}
#[inline]
pub fn validate_latitude(lat: f64) -> Result<()> {
if !(-90.0..=90.0).contains(&lat) {
Err(AstroError::InvalidCoordinate {
coord_type: "Latitude",
value: lat,
valid_range: "[-90, 90]",
})
} else {
Ok(())
}
}
#[inline]
pub fn validate_longitude(lon: f64) -> Result<()> {
if !(-180.0..=180.0).contains(&lon) {
Err(AstroError::InvalidCoordinate {
coord_type: "Longitude",
value: lon,
valid_range: "[-180, 180]",
})
} else {
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_display() {
let err = AstroError::InvalidCoordinate {
coord_type: "RA",
value: 400.0,
valid_range: "[0, 360)",
};
assert_eq!(err.to_string(), "Invalid RA: 400 (valid range: [0, 360))");
}
#[test]
fn test_validate_ra() {
assert!(validate_ra(0.0).is_ok());
assert!(validate_ra(359.9).is_ok());
assert!(validate_ra(-1.0).is_err());
assert!(validate_ra(360.0).is_err());
}
#[test]
fn test_validate_dec() {
assert!(validate_dec(0.0).is_ok());
assert!(validate_dec(90.0).is_ok());
assert!(validate_dec(-90.0).is_ok());
assert!(validate_dec(91.0).is_err());
assert!(validate_dec(-91.0).is_err());
}
}