checked_float/
lib.rs

1#![forbid(unsafe_code)]
2#![no_std]
3
4#![doc = include_str!("../README.md")]
5
6use core::marker::PhantomData;
7use core::num::FpCategory;
8use core::cmp::Ordering;
9use core::fmt;
10
11/// `num-traits` re-export:
12pub use num_traits::float::Float;
13
14#[cfg(test)] mod test;
15
16#[cfg(feature = "serde")]
17use serde::{Serialize, Deserialize};
18
19/// A property checker for a float type.
20pub trait FloatChecker<T: Float> {
21    /// A custom error resulting from a violated property check.
22    type Error;
23
24    /// Checks if a value satisfies a property.
25    /// The checker is allowed to alter the returned value to be stored (e.g., to apply modulo logic).
26    fn check(value: T) -> Result<T, Self::Error>;
27}
28
29macro_rules! prop_ops {
30    ($($f:ident : $t:ty),*$(,)?) => {$(
31        pub fn $f(self) -> $t {
32            self.0.$f()
33        }
34    )*}
35}
36macro_rules! noarg_ops {
37    ($($f:ident),*$(,)?) => {$(
38        pub fn $f() -> Result<Self, C::Error> {
39            Self::new(T::$f())
40        }
41    )*}
42}
43macro_rules! unary_ops {
44    ($($f:ident),*$(,)?) => {$(
45        pub fn $f(self) -> Result<Self, C::Error> {
46            Self::new(self.0.$f())
47        }
48    )*}
49}
50macro_rules! binary_ops {
51    ($($f:ident : $other:ident),*$(,)?) => {$(
52        pub fn $f(self, $other: Self) -> Result<Self, C::Error> {
53            Self::new(self.0.$f($other.0))
54        }
55    )*}
56}
57
58/// A checked floating point type.
59///
60/// Every instance of [`CheckedFloat`] is guaranteed to satisfy the property given by the provided [`FloatChecker`].
61/// In particular, this can be used to have a floating point type that forbids values like
62/// NaN, infinity, negatives, etc. all by providing different checkers.
63///
64/// [`CheckedFloat`] supports all the typical operations of a normal float type.
65/// However, all operations that yield another float type (e.g., `add`, `sub`, `sin`, etc.)
66/// instead yield a custom [`Result`] type specified by the [`FloatChecker`].
67///
68/// All methods from the [`Float`] type are supported by this wrapper in checked context.
69/// For documentation, see the original method definitions in [`Float`].
70///
71/// [`CheckedFloat`] also supports an implementation of [`Ord`], which allows for directly sorting [`CheckedFloat`] collections.
72/// The convention for this implementation has ordering `-NaN < -Inf < ... < -0 = +0 < ... < +Inf < +NaN`.
73///
74/// [`CheckedFloat::get`] can be used to get the underlying float value.
75#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
76pub struct CheckedFloat<T: Float, C: FloatChecker<T>>(T, PhantomData<C>);
77impl<T: Float, C: FloatChecker<T>> CheckedFloat<T, C> {
78    pub fn new(value: T) -> Result<Self, C::Error> {
79        C::check(value).map(|x| Self(x, PhantomData))
80    }
81    pub fn get(self) -> T {
82        self.0
83    }
84    prop_ops! {
85        classify: FpCategory, integer_decode: (u64, i16, i8), is_finite: bool, is_infinite: bool,
86        is_nan: bool, is_normal: bool, is_sign_negative: bool, is_sign_positive: bool,
87        is_zero: bool, is_one: bool,
88    }
89    noarg_ops! {
90        infinity, max_value, min_positive_value, min_value, nan, neg_infinity, neg_zero, zero,
91        one, epsilon,
92    }
93    unary_ops! {
94        abs, acos, acosh, asin, asinh, atan, atanh, cbrt, ceil, cos, cosh,
95        exp, exp2, exp_m1, floor, fract, ln, ln_1p, log10, log2, neg, recip,
96        round, signum, sin, sinh, sqrt, tan, tanh, trunc, to_degrees, to_radians,
97    }
98    binary_ops! {
99        abs_sub: other, add: other, atan2: other, div: other, hypot: other, log: base,
100        mul: other, powf: n, rem: other, sub: other, copysign: sign,
101    }
102    pub fn mul_add(self, a: Self, b: Self) -> Result<Self, C::Error> {
103        Self::new(self.0.mul_add(a.0, b.0))
104    }
105    pub fn powi(self, n: i32) -> Result<Self, C::Error> {
106        Self::new(self.0.powi(n))
107    }
108    pub fn sin_cos(self) -> (Result<Self, C::Error>, Result<Self, C::Error>) {
109        let (sin, cos) = self.0.sin_cos();
110        (Self::new(sin), Self::new(cos))
111    }
112}
113
114impl<T: Float, C: FloatChecker<T>> Ord for CheckedFloat<T, C> {
115    fn cmp(&self, other: &Self) -> Ordering {
116        match (self.0.is_nan(), other.0.is_nan()) {
117            (true, true) => match (self.0.is_sign_positive(), other.0.is_sign_positive()) {
118                (true, true) | (false, false) => Ordering::Equal,
119                (true, false) => Ordering::Greater,
120                (false, true) => Ordering::Less,
121            }
122            (true, false) => if self.0.is_sign_positive() { Ordering::Greater } else { Ordering::Less },
123            (false, true) => if other.0.is_sign_positive() { Ordering::Less } else { Ordering::Greater },
124            (false, false) => self.0.partial_cmp(&other.0).unwrap(),
125        }
126    }
127}
128impl<T: Float, C: FloatChecker<T>> PartialOrd for CheckedFloat<T, C> {
129    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
130        Some(self.cmp(other))
131    }
132}
133
134impl<T: Float, C: FloatChecker<T>> Eq for CheckedFloat<T, C> { }
135impl<T: Float, C: FloatChecker<T>> PartialEq for CheckedFloat<T, C> {
136    fn eq(&self, other: &Self) -> bool {
137        self.cmp(other) == Ordering::Equal
138    }
139}
140
141impl<T: Float, C: FloatChecker<T>> Copy for CheckedFloat<T, C> { }
142impl<T: Float, C: FloatChecker<T>> Clone for CheckedFloat<T, C> {
143    fn clone(&self) -> Self {
144        Self(self.0, PhantomData)
145    }
146}
147
148impl<T: Float + fmt::Debug, C: FloatChecker<T>> fmt::Debug for CheckedFloat<T, C> {
149    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150        write!(f, "{:?}", self.0)
151    }
152}
153impl<T: Float + fmt::Display, C: FloatChecker<T>> fmt::Display for CheckedFloat<T, C> {
154    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
155        write!(f, "{}", self.0)
156    }
157}