Skip to main content

use_real/
real.rs

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