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