Skip to main content

use_real/
real.rs

1use core::fmt;
2
3use crate::RealError;
4
5/// A validated finite `f64` value.
6#[derive(Debug, Clone, Copy, Default, PartialEq, PartialOrd)]
7pub struct Real {
8    value: f64,
9}
10
11/// A checked closed interval `[min, max]` over finite real values.
12#[derive(Debug, Clone, Copy, PartialEq)]
13pub struct RealInterval {
14    min: Real,
15    max: Real,
16}
17
18impl Real {
19    /// Creates a real value without validation.
20    #[must_use]
21    pub const fn new(value: f64) -> Self {
22        Self { value }
23    }
24
25    /// Creates a real value from a finite `f64`.
26    ///
27    /// # Errors
28    ///
29    /// Returns [`RealError::NonFiniteValue`] when `value` is `NaN` or
30    /// infinite.
31    ///
32    /// # Examples
33    ///
34    /// ```
35    /// use use_real::{Real, RealError};
36    ///
37    /// let real = Real::try_new(2.5)?;
38    /// assert_eq!(real, Real::new(2.5));
39    ///
40    /// assert!(matches!(
41    ///     Real::try_new(f64::INFINITY),
42    ///     Err(RealError::NonFiniteValue { .. })
43    /// ));
44    /// # Ok::<(), RealError>(())
45    /// ```
46    pub const fn try_new(value: f64) -> Result<Self, RealError> {
47        match RealError::validate_value("value", value) {
48            Ok(value) => Ok(Self::new(value)),
49            Err(error) => Err(error),
50        }
51    }
52
53    /// Validates that an existing real value remains finite.
54    ///
55    /// # Errors
56    ///
57    /// Returns the same error variants as [`Self::try_new`].
58    pub const fn validate(self) -> Result<Self, RealError> {
59        Self::try_new(self.value)
60    }
61
62    /// Returns the stored `f64` value.
63    #[must_use]
64    pub const fn value(&self) -> f64 {
65        self.value
66    }
67
68    /// Returns `0.0` as a validated real value.
69    #[must_use]
70    pub const fn zero() -> Self {
71        Self::new(0.0)
72    }
73
74    /// Returns `1.0` as a validated real value.
75    #[must_use]
76    pub const fn one() -> Self {
77        Self::new(1.0)
78    }
79
80    /// Returns the absolute value.
81    #[must_use]
82    pub const fn abs(self) -> Self {
83        Self::new(self.value.abs())
84    }
85}
86
87impl fmt::Display for Real {
88    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
89        write!(formatter, "{}", self.value)
90    }
91}
92
93impl RealInterval {
94    /// Creates an interval from two validated bounds without rechecking order.
95    #[must_use]
96    pub const fn new(min: Real, max: Real) -> Self {
97        Self { min, max }
98    }
99
100    /// Creates an interval from finite `min` and `max` bounds.
101    ///
102    /// # Errors
103    ///
104    /// Returns [`RealError::NonFiniteValue`] when either bound is not finite,
105    /// and [`RealError::InvalidInterval`] when `min > max`.
106    ///
107    /// # Examples
108    ///
109    /// ```
110    /// use use_real::RealInterval;
111    ///
112    /// let interval = RealInterval::try_new(-2.0, 6.0)?;
113    /// assert!((interval.width() - 8.0).abs() < 1.0e-12);
114    /// # Ok::<(), use_real::RealError>(())
115    /// ```
116    pub fn try_new(min: f64, max: f64) -> Result<Self, RealError> {
117        let min = Real::try_new(min)?;
118        let max = Real::try_new(max)?;
119
120        if min.value() > max.value() {
121            return Err(RealError::InvalidInterval {
122                min: min.value(),
123                max: max.value(),
124            });
125        }
126
127        Ok(Self::new(min, max))
128    }
129
130    /// Validates that an existing interval remains ordered.
131    ///
132    /// # Errors
133    ///
134    /// Returns the same error variants as [`Self::try_new`].
135    pub fn validate(self) -> Result<Self, RealError> {
136        Self::try_new(self.min.value(), self.max.value())
137    }
138
139    /// Returns the lower bound.
140    #[must_use]
141    pub const fn min(&self) -> Real {
142        self.min
143    }
144
145    /// Returns the upper bound.
146    #[must_use]
147    pub const fn max(&self) -> Real {
148        self.max
149    }
150
151    /// Returns the interval width, `max - min`.
152    #[must_use]
153    pub const fn width(&self) -> f64 {
154        self.max.value() - self.min.value()
155    }
156
157    /// Returns the midpoint of the interval.
158    #[must_use]
159    pub const fn midpoint(&self) -> Real {
160        Real::new(f64::midpoint(self.min.value(), self.max.value()))
161    }
162
163    /// Returns `true` when `value` lies inside the closed interval.
164    #[must_use]
165    pub const fn contains(&self, value: Real) -> bool {
166        value.value() >= self.min.value() && value.value() <= self.max.value()
167    }
168
169    /// Clamps `value` into the interval.
170    #[must_use]
171    pub const fn clamp(&self, value: Real) -> Real {
172        Real::new(value.value().clamp(self.min.value(), self.max.value()))
173    }
174
175    /// Returns `true` when the interval has zero width.
176    #[must_use]
177    pub const fn is_degenerate(&self) -> bool {
178        self.min.value().to_bits() == self.max.value().to_bits()
179    }
180}
181
182impl fmt::Display for RealInterval {
183    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
184        write!(formatter, "[{}, {}]", self.min, self.max)
185    }
186}
187
188/// Compares two real values with an explicit non-negative tolerance.
189///
190/// # Errors
191///
192/// Returns [`RealError::NonFiniteTolerance`] when `tolerance` is not finite,
193/// and [`RealError::NegativeTolerance`] when `tolerance < 0.0`.
194///
195/// # Examples
196///
197/// ```
198/// use use_real::{Real, approx_eq};
199///
200/// let left = Real::try_new(1.0)?;
201/// let right = Real::try_new(1.0 + 1.0e-10)?;
202///
203/// assert!(approx_eq(left, right, 1.0e-9)?);
204/// # Ok::<(), use_real::RealError>(())
205/// ```
206pub fn approx_eq(left: Real, right: Real, tolerance: f64) -> Result<bool, RealError> {
207    let tolerance = RealError::validate_tolerance(tolerance)?;
208
209    Ok((left.value() - right.value()).abs() <= tolerance)
210}
211
212#[cfg(test)]
213mod tests {
214    use super::{Real, RealInterval, approx_eq};
215    use crate::RealError;
216
217    fn assert_close(left: f64, right: f64, tolerance: f64) {
218        assert!(
219            (left - right).abs() <= tolerance,
220            "expected {left} to be within {tolerance} of {right}"
221        );
222    }
223
224    #[test]
225    fn validates_real_values() {
226        assert!(matches!(
227            Real::try_new(f64::NAN),
228            Err(RealError::NonFiniteValue { .. })
229        ));
230    }
231
232    #[test]
233    fn exposes_abs_zero_and_one() -> Result<(), RealError> {
234        let value = Real::try_new(-3.5)?;
235
236        assert_eq!(value.abs(), Real::try_new(3.5)?);
237        assert_eq!(Real::zero(), Real::try_new(0.0)?);
238        assert_eq!(Real::one(), Real::try_new(1.0)?);
239
240        Ok(())
241    }
242
243    #[test]
244    fn compares_values_with_explicit_tolerance() -> Result<(), RealError> {
245        let left = Real::try_new(1.0)?;
246        let right = Real::try_new(1.0 + 1.0e-10)?;
247
248        assert!(approx_eq(left, right, 1.0e-9)?);
249        assert!(!approx_eq(left, right, 1.0e-12)?);
250
251        Ok(())
252    }
253
254    #[test]
255    fn validates_interval_bounds() {
256        assert!(matches!(
257            RealInterval::try_new(2.0, -2.0),
258            Err(RealError::InvalidInterval { .. })
259        ));
260    }
261
262    #[test]
263    fn computes_interval_properties() -> Result<(), RealError> {
264        let interval = RealInterval::try_new(-2.0, 6.0)?;
265
266        assert_close(interval.width(), 8.0, 1.0e-12);
267        assert_close(interval.midpoint().value(), 2.0, 1.0e-12);
268        assert!(interval.contains(Real::try_new(1.5)?));
269        assert_eq!(interval.clamp(Real::try_new(8.0)?), Real::try_new(6.0)?);
270        assert!(!interval.is_degenerate());
271
272        Ok(())
273    }
274}