twine_core/constraint/
non_negative.rs

1use std::{cmp::Ordering, marker::PhantomData, ops::Add};
2
3use num_traits::Zero;
4
5use super::{Constrained, Constraint, ConstraintError};
6
7/// Marker type enforcing that a value is non-negative (zero or greater).
8///
9/// Use this type with [`Constrained<T, NonNegative>`] to encode non-negativity
10/// at the type level.
11///
12/// You can construct a value constrained to be non-negative using either the
13/// generic [`Constrained::new`] method or the convenient [`NonNegative::new`]
14/// associated function.
15///
16/// # Examples
17///
18/// ```
19/// use twine_core::constraint::{Constrained, NonNegative};
20///
21/// // Generic constructor:
22/// let x = Constrained::<_, NonNegative>::new(5).unwrap();
23/// assert_eq!(x.into_inner(), 5);
24///
25/// // Associated constructor:
26/// let y = NonNegative::new(0.0).unwrap();
27/// assert_eq!(y.into_inner(), 0.0);
28///
29/// // Error cases:
30/// assert!(NonNegative::new(-7).is_err());
31/// assert!(NonNegative::new(f64::NAN).is_err());
32/// ```
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub struct NonNegative;
35
36impl NonNegative {
37    /// Constructs a [`Constrained<T, NonNegative>`] if the value is non-negative.
38    ///
39    /// # Errors
40    ///
41    /// Returns an error if the value is negative or not a number (`NaN`).
42    pub fn new<T: PartialOrd + Zero>(
43        value: T,
44    ) -> Result<Constrained<T, NonNegative>, ConstraintError> {
45        Constrained::<T, NonNegative>::new(value)
46    }
47
48    /// Returns the additive identity (zero) as a non-negative constrained value.
49    ///
50    /// This method is equivalent to [`Constrained::<T, NonNegative>::zero()`].
51    #[must_use]
52    pub fn zero<T: PartialOrd + Zero>() -> Constrained<T, NonNegative> {
53        Constrained::<T, NonNegative>::zero()
54    }
55}
56
57impl<T: PartialOrd + Zero> Constraint<T> for NonNegative {
58    fn check(value: &T) -> Result<(), ConstraintError> {
59        match value.partial_cmp(&T::zero()) {
60            Some(Ordering::Greater | Ordering::Equal) => Ok(()),
61            Some(Ordering::Less) => Err(ConstraintError::Negative),
62            None => Err(ConstraintError::NotANumber),
63        }
64    }
65}
66
67/// Adds two `Constrained<T, NonNegative>` values.
68///
69/// Assumes that summing two non-negative values yields a non-negative result.
70/// This holds for most numeric types (`i32`, `f64`, `uom::Quantity`, etc.),
71/// but may not for all possible `T`.
72/// The invariant is checked in debug builds.
73///
74/// # Panics
75///
76/// Panics in debug builds if the sum is unexpectedly negative.
77impl<T> Add for Constrained<T, NonNegative>
78where
79    T: Add<Output = T> + PartialOrd + Zero,
80{
81    type Output = Self;
82
83    fn add(self, rhs: Self) -> Self {
84        let value = self.value + rhs.value;
85        debug_assert!(
86            value >= T::zero(),
87            "Addition produced a negative value, violating NonNegative bound invariant"
88        );
89        Self {
90            value,
91            _marker: PhantomData,
92        }
93    }
94}
95
96impl<T> Zero for Constrained<T, NonNegative>
97where
98    T: PartialOrd + Zero,
99{
100    fn zero() -> Self {
101        Self {
102            value: T::zero(),
103            _marker: PhantomData,
104        }
105    }
106
107    fn is_zero(&self) -> bool {
108        self.value == T::zero()
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115
116    use uom::si::{f64::MassRate, mass_rate::kilogram_per_second};
117
118    #[test]
119    fn integers() {
120        let one = Constrained::<i32, NonNegative>::new(1).unwrap();
121        assert_eq!(one.into_inner(), 1);
122
123        let two = NonNegative::new(2).unwrap();
124        assert_eq!(two.as_ref(), &2);
125
126        let zero = NonNegative::zero();
127        assert_eq!(zero.into_inner(), 0);
128
129        let sum = one + two + zero;
130        assert_eq!(sum.into_inner(), 3);
131
132        assert!(NonNegative::new(-1).is_err());
133    }
134
135    #[test]
136    fn floats() {
137        assert!(Constrained::<f64, NonNegative>::new(2.0).is_ok());
138        assert!(NonNegative::new(0.0).is_ok());
139        assert!(NonNegative::new(-2.0).is_err());
140        assert!(NonNegative::new(f64::NAN).is_err());
141    }
142
143    #[test]
144    fn mass_rates() {
145        let mass_rate = MassRate::new::<kilogram_per_second>(5.0);
146        assert!(NonNegative::new(mass_rate).is_ok());
147
148        let mass_rate = MassRate::new::<kilogram_per_second>(0.0);
149        assert!(NonNegative::new(mass_rate).is_ok());
150
151        let mass_rate = MassRate::new::<kilogram_per_second>(-2.0);
152        assert!(NonNegative::new(mass_rate).is_err());
153    }
154}