Skip to main content

twine_models/support/constraint/unit_interval/
closed.rs

1use std::{cmp::Ordering, marker::PhantomData};
2
3use crate::support::constraint::{Constrained, Constraint, ConstraintError, UnitBounds};
4
5/// Marker type enforcing that a value lies in the closed unit interval: `0 ≤ x ≤ 1`.
6///
7/// Requires `T: UnitBounds`.
8/// We provide [`UnitBounds`] implementations for `f32`, `f64`, and `uom::si::f64::Ratio`.
9///
10/// You can construct a value constrained to `[0, 1]` using either the generic
11/// [`Constrained::new`] method or the convenient [`UnitInterval::new`]
12/// associated function.
13/// Convenience constructors [`UnitInterval::zero`] and [`UnitInterval::one`]
14/// are also provided for the endpoints.
15///
16/// # Examples
17///
18/// Using with `f64`:
19///
20/// ```
21/// use twine_models::support::constraint::{Constrained, UnitInterval};
22///
23/// // Generic constructor:
24/// let a = Constrained::<_, UnitInterval>::new(0.25).unwrap();
25/// assert_eq!(a.into_inner(), 0.25);
26///
27/// // Associated constructor:
28/// let b = UnitInterval::new(1.0).unwrap();
29/// assert_eq!(b.as_ref(), &1.0);
30///
31/// // Endpoints:
32/// let z = UnitInterval::zero::<f64>();
33/// let o = UnitInterval::one::<f64>();
34/// assert_eq!((z.into_inner(), o.into_inner()), (0.0, 1.0));
35///
36/// // Error cases:
37/// assert!(UnitInterval::new(-0.0001).is_err());
38/// assert!(UnitInterval::new(1.0001).is_err());
39/// assert!(UnitInterval::new(f64::NAN).is_err());
40/// ```
41///
42/// Using with `uom::si::f64::Ratio`:
43///
44/// ```
45/// use twine_models::support::constraint::{Constrained, UnitInterval};
46/// use uom::si::{f64::Ratio, ratio::{ratio, percent}};
47///
48/// let r = Constrained::<Ratio, UnitInterval>::new(Ratio::new::<ratio>(0.42)).unwrap();
49/// assert_eq!(r.as_ref().get::<percent>(), 42.0);
50///
51/// let z = UnitInterval::zero::<Ratio>();
52/// let o = UnitInterval::one::<Ratio>();
53/// assert_eq!(z.into_inner().get::<ratio>(), 0.0);
54/// assert_eq!(o.into_inner().get::<ratio>(), 1.0);
55/// ```
56#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
57pub struct UnitInterval;
58
59impl UnitInterval {
60    /// Constructs `Constrained<T, UnitInterval>` if 0 ≤ value ≤ 1.
61    ///
62    /// # Errors
63    ///
64    /// Fails if the value is outside the closed unit interval:
65    ///
66    /// - [`ConstraintError::BelowMinimum`] if less than zero.
67    /// - [`ConstraintError::AboveMaximum`] if greater than one.
68    /// - [`ConstraintError::NotANumber`] if comparison is undefined (e.g., NaN).
69    pub fn new<T: UnitBounds>(value: T) -> Result<Constrained<T, UnitInterval>, ConstraintError> {
70        Constrained::<T, UnitInterval>::new(value)
71    }
72
73    /// Returns the lower bound (zero) as a constrained value.
74    #[must_use]
75    pub fn zero<T: UnitBounds>() -> Constrained<T, UnitInterval> {
76        Constrained::<T, UnitInterval> {
77            value: T::zero(),
78            _marker: PhantomData,
79        }
80    }
81
82    /// Returns the upper bound (one) as a constrained value.
83    #[must_use]
84    pub fn one<T: UnitBounds>() -> Constrained<T, UnitInterval> {
85        Constrained::<T, UnitInterval> {
86            value: T::one(),
87            _marker: PhantomData,
88        }
89    }
90}
91
92impl<T: UnitBounds> Constraint<T> for UnitInterval {
93    fn check(value: &T) -> Result<(), ConstraintError> {
94        match (value.partial_cmp(&T::zero()), value.partial_cmp(&T::one())) {
95            (None, _) | (_, None) => Err(ConstraintError::NotANumber),
96            (Some(Ordering::Less), _) => Err(ConstraintError::BelowMinimum),
97            (_, Some(Ordering::Greater)) => Err(ConstraintError::AboveMaximum),
98            _ => Ok(()),
99        }
100    }
101}
102
103#[cfg(test)]
104mod tests {
105    use crate::support::constraint::*;
106
107    use uom::si::{
108        f64::Ratio,
109        ratio::{percent, ratio},
110    };
111
112    #[test]
113    #[allow(clippy::float_cmp)]
114    fn floats_valid() {
115        assert!(Constrained::<f64, UnitInterval>::new(0.0).is_ok());
116        assert!(Constrained::<f64, UnitInterval>::new(1.0).is_ok());
117        assert!(UnitInterval::new(0.5).is_ok());
118
119        let z = UnitInterval::zero::<f64>();
120        let o = UnitInterval::one::<f64>();
121        assert_eq!(z.into_inner(), 0.0);
122        assert_eq!(o.into_inner(), 1.0);
123    }
124
125    #[test]
126    fn floats_out_of_range() {
127        assert!(matches!(
128            UnitInterval::new(-1.0),
129            Err(ConstraintError::BelowMinimum)
130        ));
131        assert!(matches!(
132            UnitInterval::new(2.0),
133            Err(ConstraintError::AboveMaximum)
134        ));
135        assert!(matches!(
136            UnitInterval::new(-1e-15),
137            Err(ConstraintError::BelowMinimum),
138        ));
139        assert!(matches!(
140            UnitInterval::new(1.0 + 1e-15),
141            Err(ConstraintError::AboveMaximum)
142        ));
143        assert!(matches!(
144            UnitInterval::new(f64::INFINITY),
145            Err(ConstraintError::AboveMaximum)
146        ));
147        assert!(matches!(
148            UnitInterval::new(f64::NEG_INFINITY),
149            Err(ConstraintError::BelowMinimum)
150        ));
151    }
152
153    #[test]
154    fn floats_nan_is_not_a_number() {
155        assert!(matches!(
156            UnitInterval::new(f64::NAN),
157            Err(ConstraintError::NotANumber)
158        ));
159    }
160
161    #[test]
162    #[allow(clippy::float_cmp)]
163    fn uom_ratio_valid() {
164        assert!(Constrained::<Ratio, UnitInterval>::new(Ratio::new::<ratio>(0.0)).is_ok());
165        assert!(Constrained::<Ratio, UnitInterval>::new(Ratio::new::<ratio>(1.0)).is_ok());
166        assert!(UnitInterval::new(Ratio::new::<ratio>(0.5)).is_ok());
167
168        let z = UnitInterval::zero::<Ratio>();
169        let o = UnitInterval::one::<Ratio>();
170
171        assert_eq!(z.into_inner().get::<ratio>(), 0.0);
172        assert_eq!(z.into_inner().get::<percent>(), 0.0);
173
174        assert_eq!(o.into_inner().get::<ratio>(), 1.0);
175        assert_eq!(o.into_inner().get::<percent>(), 100.0);
176    }
177
178    #[test]
179    fn uom_ratio_out_of_range() {
180        assert!(matches!(
181            UnitInterval::new(Ratio::new::<ratio>(-0.1)),
182            Err(ConstraintError::BelowMinimum)
183        ));
184        assert!(matches!(
185            UnitInterval::new(Ratio::new::<ratio>(1.1)),
186            Err(ConstraintError::AboveMaximum)
187        ));
188    }
189}