twine_core/constraint/
non_positive.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-positive (zero or less).
8///
9/// Use this type with [`Constrained<T, NonPositive>`] to encode non-positivity
10/// at the type level.
11///
12/// You can construct a value constrained to be non-positive using either the
13/// generic [`Constrained::new`] method or the convenient [`NonPositive::new`]
14/// associated function.
15///
16/// # Examples
17///
18/// ```
19/// use twine_core::constraint::{Constrained, NonPositive};
20///
21/// // Generic constructor:
22/// let x = Constrained::<_, NonPositive>::new(0).unwrap();
23/// assert_eq!(x.into_inner(), 0);
24///
25/// // Associated constructor:
26/// let y = NonPositive::new(-5).unwrap();
27/// assert_eq!(y.into_inner(), -5);
28///
29/// // Error cases:
30/// assert!(NonPositive::new(3).is_err());
31/// assert!(NonPositive::new(f64::NAN).is_err());
32/// ```
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub struct NonPositive;
35
36impl NonPositive {
37    /// Constructs a [`Constrained<T, NonPositive>`] if the value is non-positive.
38    ///
39    /// # Errors
40    ///
41    /// Returns an error if the value is positive or not a number (`NaN`).
42    pub fn new<T: PartialOrd + Zero>(
43        value: T,
44    ) -> Result<Constrained<T, NonPositive>, ConstraintError> {
45        Constrained::<T, NonPositive>::new(value)
46    }
47
48    /// Returns the additive identity (zero) as a non-positive constrained value.
49    ///
50    /// This method is equivalent to [`Constrained::<T, NonPositive>::zero()`].
51    #[must_use]
52    pub fn zero<T: PartialOrd + Zero>() -> Constrained<T, NonPositive> {
53        Constrained::<T, NonPositive>::zero()
54    }
55}
56
57impl<T: PartialOrd + Zero> Constraint<T> for NonPositive {
58    fn check(value: &T) -> Result<(), ConstraintError> {
59        match value.partial_cmp(&T::zero()) {
60            Some(Ordering::Less | Ordering::Equal) => Ok(()),
61            Some(Ordering::Greater) => Err(ConstraintError::Positive),
62            None => Err(ConstraintError::NotANumber),
63        }
64    }
65}
66
67/// Adds two `Constrained<T, NonPositive>` values.
68///
69/// Assumes that summing two non-positive values yields a non-positive 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 positive.
77impl<T> Add for Constrained<T, NonPositive>
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 positive value, violating NonPositive bound invariant"
88        );
89        Self {
90            value,
91            _marker: PhantomData,
92        }
93    }
94}
95
96impl<T> Zero for Constrained<T, NonPositive>
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    use uom::si::{f64::Power, power::watt};
116
117    #[test]
118    fn integers() {
119        let neg_one = Constrained::<i32, NonPositive>::new(-1).unwrap();
120        assert_eq!(neg_one.into_inner(), -1);
121
122        let neg_two = NonPositive::new(-2).unwrap();
123        assert_eq!(neg_two.as_ref(), &-2);
124
125        let zero = NonPositive::zero();
126        assert_eq!(zero.into_inner(), 0);
127
128        let sum = neg_one + neg_two + zero;
129        assert_eq!(sum.into_inner(), -3);
130
131        assert!(NonPositive::new(2).is_err());
132    }
133
134    #[test]
135    fn floats() {
136        assert!(Constrained::<f64, NonPositive>::new(-2.0).is_ok());
137        assert!(NonPositive::new(0.0).is_ok());
138        assert!(NonPositive::new(2.0).is_err());
139        assert!(NonPositive::new(f64::NAN).is_err());
140    }
141
142    #[test]
143    fn powers() {
144        let neg_mass_rate = Power::new::<watt>(-5.0);
145        assert!(NonPositive::new(neg_mass_rate).is_ok());
146
147        let zero_mass_rate = Power::new::<watt>(0.0);
148        assert!(NonPositive::new(zero_mass_rate).is_ok());
149
150        let pos_mass_rate = Power::new::<watt>(2.0);
151        assert!(NonPositive::new(pos_mass_rate).is_err());
152    }
153}