#![forbid(unsafe_code)]
use crate::LaError;
#[must_use]
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Tolerance {
value: f64,
}
impl Tolerance {
const fn new_unchecked(value: f64) -> Self {
Self { value }
}
pub(crate) const ZERO: Self = Self::new_unchecked(0.0);
#[inline]
pub const fn try_new(value: f64) -> Result<Self, LaError> {
if value >= 0.0 && value.is_finite() {
Ok(Self::new_unchecked(value))
} else {
Err(LaError::invalid_tolerance(value))
}
}
#[inline]
#[must_use]
pub const fn get(self) -> f64 {
self.value
}
}
pub const DEFAULT_SINGULAR_TOL: Tolerance = Tolerance::new_unchecked(1e-12);
#[cfg(test)]
mod tests {
use core::assert_matches;
use approx::assert_abs_diff_eq;
use super::*;
#[test]
fn default_singular_tol_is_expected() {
assert_abs_diff_eq!(DEFAULT_SINGULAR_TOL.get(), 1e-12, epsilon = 0.0);
assert_eq!(Tolerance::ZERO.get().to_bits(), 0.0f64.to_bits());
}
#[test]
fn try_new_accepts_finite_non_negative_values() {
assert_eq!(
Tolerance::try_new(0.0).unwrap().get().to_bits(),
0.0f64.to_bits()
);
assert_eq!(
Tolerance::try_new(1e-12).unwrap().get().to_bits(),
1e-12f64.to_bits()
);
assert_eq!(
Tolerance::try_new(f64::MAX).unwrap().get().to_bits(),
f64::MAX.to_bits()
);
}
#[test]
fn try_new_accepts_and_preserves_negative_zero() {
let tolerance = Tolerance::try_new(-0.0).unwrap();
assert_eq!(tolerance.get().to_bits(), (-0.0f64).to_bits());
}
#[test]
fn try_new_rejects_negative_finite_values() {
assert_eq!(
Tolerance::try_new(-1.0),
Err(LaError::InvalidTolerance {
value: -1.0,
reason: crate::InvalidToleranceReason::Negative,
})
);
}
#[test]
fn try_new_rejects_non_finite_values_with_structured_reason() {
for value in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
assert_matches!(
Tolerance::try_new(value),
Err(LaError::InvalidTolerance {
value: observed,
reason: crate::InvalidToleranceReason::NotFinite,
}) if observed.to_bits() == value.to_bits()
);
}
}
}