Skip to main content

twine_models/support/constraint/unit_interval/
open.rs

1use std::cmp::Ordering;
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 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 [`UnitIntervalOpen::new`]
14/// associated function.
15///
16/// # Examples
17///
18/// Using with `f64`:
19///
20/// ```
21/// use twine_models::support::constraint::{Constrained, UnitIntervalOpen};
22///
23/// // Generic constructor:
24/// let a = Constrained::<_, UnitIntervalOpen>::new(0.25).unwrap();
25/// assert_eq!(a.into_inner(), 0.25);
26///
27/// // Associated constructor:
28/// let b = UnitIntervalOpen::new(0.9).unwrap();
29/// assert_eq!(b.as_ref(), &0.9);
30///
31/// // Error cases:
32/// assert!(UnitIntervalOpen::new(0.0).is_err());
33/// assert!(UnitIntervalOpen::new(1.0).is_err());
34/// assert!(UnitIntervalOpen::new(f64::NAN).is_err());
35/// ```
36///
37/// Using with `uom::si::f64::Ratio`:
38///
39/// ```
40/// use twine_models::support::constraint::{Constrained, UnitIntervalOpen};
41/// use uom::si::{f64::Ratio, ratio::{ratio, percent}};
42///
43/// let r = Constrained::<Ratio, UnitIntervalOpen>::new(Ratio::new::<ratio>(0.42)).unwrap();
44/// assert_eq!(r.as_ref().get::<percent>(), 42.0);
45/// ```
46#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
47pub struct UnitIntervalOpen;
48
49impl UnitIntervalOpen {
50    /// Constructs `Constrained<T, UnitIntervalOpen>` if 0 < value < 1.
51    ///
52    /// # Errors
53    ///
54    /// Fails if the value is outside the open unit interval:
55    ///
56    /// - [`ConstraintError::BelowMinimum`] if less than or equal to zero.
57    /// - [`ConstraintError::AboveMaximum`] if greater than or equal to one.
58    /// - [`ConstraintError::NotANumber`] if comparison is undefined (e.g., NaN).
59    pub fn new<T: UnitBounds>(
60        value: T,
61    ) -> Result<Constrained<T, UnitIntervalOpen>, ConstraintError> {
62        Constrained::<T, UnitIntervalOpen>::new(value)
63    }
64}
65
66impl<T: UnitBounds> Constraint<T> for UnitIntervalOpen {
67    fn check(value: &T) -> Result<(), ConstraintError> {
68        match (value.partial_cmp(&T::zero()), value.partial_cmp(&T::one())) {
69            (None, _) | (_, None) => Err(ConstraintError::NotANumber),
70            (Some(Ordering::Less | Ordering::Equal), _) => Err(ConstraintError::BelowMinimum),
71            (_, Some(Ordering::Greater | Ordering::Equal)) => Err(ConstraintError::AboveMaximum),
72            _ => Ok(()),
73        }
74    }
75}
76
77#[cfg(test)]
78mod tests {
79    use crate::support::constraint::*;
80
81    use uom::si::{f64::Ratio, ratio::ratio};
82
83    #[test]
84    #[allow(clippy::float_cmp)]
85    fn floats_valid() {
86        assert!(Constrained::<f64, UnitIntervalOpen>::new(0.1).is_ok());
87        assert!(Constrained::<f64, UnitIntervalOpen>::new(0.9).is_ok());
88        assert!(UnitIntervalOpen::new(0.5).is_ok());
89    }
90
91    #[test]
92    fn floats_out_of_range() {
93        assert!(matches!(
94            UnitIntervalOpen::new(0.0),
95            Err(ConstraintError::BelowMinimum)
96        ));
97        assert!(matches!(
98            UnitIntervalOpen::new(-1.0),
99            Err(ConstraintError::BelowMinimum)
100        ));
101        assert!(matches!(
102            UnitIntervalOpen::new(1.0),
103            Err(ConstraintError::AboveMaximum)
104        ));
105        assert!(matches!(
106            UnitIntervalOpen::new(2.0),
107            Err(ConstraintError::AboveMaximum)
108        ));
109    }
110
111    #[test]
112    fn floats_nan_is_not_a_number() {
113        assert!(matches!(
114            UnitIntervalOpen::new(f64::NAN),
115            Err(ConstraintError::NotANumber)
116        ));
117    }
118
119    #[test]
120    #[allow(clippy::float_cmp)]
121    fn uom_ratio_valid() {
122        assert!(Constrained::<Ratio, UnitIntervalOpen>::new(Ratio::new::<ratio>(0.01)).is_ok());
123        assert!(Constrained::<Ratio, UnitIntervalOpen>::new(Ratio::new::<ratio>(0.99)).is_ok());
124        assert!(UnitIntervalOpen::new(Ratio::new::<ratio>(0.5)).is_ok());
125    }
126
127    #[test]
128    fn uom_ratio_out_of_range() {
129        assert!(matches!(
130            UnitIntervalOpen::new(Ratio::new::<ratio>(0.0)),
131            Err(ConstraintError::BelowMinimum)
132        ));
133        assert!(matches!(
134            UnitIntervalOpen::new(Ratio::new::<ratio>(1.0)),
135            Err(ConstraintError::AboveMaximum)
136        ));
137    }
138}