#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum ConicRegime {
Elliptic,
Parabolic,
Hyperbolic,
}
#[derive(Debug, Clone, Copy, PartialEq, thiserror::Error)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum EccentricityError {
#[error("eccentricity must be non-negative, got {0}")]
Negative(f64),
#[error("eccentricity must be finite, got {0}")]
NotFinite(f64),
#[error("elliptic eccentricity requires e < 1, got {0}")]
NotElliptic(f64),
#[error("hyperbolic eccentricity requires e > 1, got {0}")]
NotHyperbolic(f64),
}
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Eccentricity(f64);
impl Eccentricity {
#[must_use]
pub fn new(e: f64) -> Option<Self> {
(e.is_finite() && e >= 0.0).then_some(Self(e))
}
pub fn try_new(e: f64) -> Result<Self, EccentricityError> {
if !e.is_finite() {
return Err(EccentricityError::NotFinite(e));
}
if e < 0.0 {
return Err(EccentricityError::Negative(e));
}
Ok(Self(e))
}
pub fn new_elliptic(e: f64) -> Result<Self, EccentricityError> {
let ec = Self::try_new(e)?;
if e >= 1.0 {
return Err(EccentricityError::NotElliptic(e));
}
Ok(ec)
}
pub fn new_hyperbolic(e: f64) -> Result<Self, EccentricityError> {
let ec = Self::try_new(e)?;
if e <= 1.0 {
return Err(EccentricityError::NotHyperbolic(e));
}
Ok(ec)
}
#[must_use]
pub const fn parabolic() -> Self {
Self(1.0)
}
#[must_use]
pub const fn new_unchecked(e: f64) -> Self {
Self(e)
}
#[must_use]
pub const fn value(self) -> f64 {
self.0
}
#[must_use]
pub fn classify(self, eps: f64) -> ConicRegime {
if (self.0 - 1.0).abs() <= eps {
ConicRegime::Parabolic
} else if self.0 < 1.0 {
ConicRegime::Elliptic
} else {
ConicRegime::Hyperbolic
}
}
#[must_use]
pub fn is_elliptic(self) -> bool {
self.0 < 1.0
}
#[must_use]
pub fn is_parabolic(self, eps: f64) -> bool {
(self.0 - 1.0).abs() <= eps
}
#[must_use]
pub fn is_hyperbolic(self) -> bool {
self.0 > 1.0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn is_hyperbolic_classifies_strictly_above_one() {
assert!(Eccentricity::new_unchecked(1.5).is_hyperbolic());
assert!(!Eccentricity::new_unchecked(0.5).is_hyperbolic());
}
#[test]
fn try_new_accepts_valid_values() {
let e = Eccentricity::try_new(0.5).unwrap();
assert!((e.value() - 0.5).abs() < 1e-15);
assert_eq!(Eccentricity::try_new(0.0).unwrap().value(), 0.0);
}
#[test]
fn try_new_rejects_invalid_values() {
assert!(matches!(
Eccentricity::try_new(-0.1),
Err(EccentricityError::Negative(_))
));
assert!(matches!(
Eccentricity::try_new(f64::NAN),
Err(EccentricityError::NotFinite(_))
));
assert!(matches!(
Eccentricity::try_new(f64::INFINITY),
Err(EccentricityError::NotFinite(_))
));
}
#[test]
fn new_elliptic_validates_regime() {
let e = Eccentricity::new_elliptic(0.3).unwrap();
assert!((e.value() - 0.3).abs() < 1e-15);
assert!(matches!(
Eccentricity::new_elliptic(1.0),
Err(EccentricityError::NotElliptic(_))
));
assert!(matches!(
Eccentricity::new_elliptic(1.5),
Err(EccentricityError::NotElliptic(_))
));
assert!(matches!(
Eccentricity::new_elliptic(-0.1),
Err(EccentricityError::Negative(_))
));
}
#[test]
fn new_hyperbolic_validates_regime() {
let e = Eccentricity::new_hyperbolic(1.5).unwrap();
assert!((e.value() - 1.5).abs() < 1e-15);
assert!(matches!(
Eccentricity::new_hyperbolic(1.0),
Err(EccentricityError::NotHyperbolic(_))
));
assert!(matches!(
Eccentricity::new_hyperbolic(0.5),
Err(EccentricityError::NotHyperbolic(_))
));
assert!(matches!(
Eccentricity::new_hyperbolic(f64::NAN),
Err(EccentricityError::NotFinite(_))
));
}
#[test]
fn parabolic_constructor_is_exactly_one() {
assert_eq!(Eccentricity::parabolic().value(), 1.0);
}
#[test]
fn classify_uses_tolerance_band() {
assert_eq!(
Eccentricity::new_unchecked(0.5).classify(1e-10),
ConicRegime::Elliptic
);
assert_eq!(
Eccentricity::new_unchecked(1.0).classify(1e-10),
ConicRegime::Parabolic
);
assert_eq!(
Eccentricity::new_unchecked(1.0 + 5e-11).classify(1e-10),
ConicRegime::Parabolic
);
assert_eq!(
Eccentricity::new_unchecked(1.5).classify(1e-10),
ConicRegime::Hyperbolic
);
}
}