Skip to main content

twine_models/support/constraint/unit_interval/
upper_open.rs

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