twine_core/constraint/
strictly_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 strictly negative (less than zero).
8///
9/// Use this type with [`Constrained<T, StrictlyNegative>`] to encode strict
10/// negativity at the type level.
11///
12/// You can construct a value constrained to be strictly negative using
13/// either the generic [`Constrained::new`] method or the convenient
14/// [`StrictlyNegative::new`] associated function.
15///
16/// # Examples
17///
18/// ```
19/// use twine_core::constraint::{Constrained, StrictlyNegative};
20///
21/// // Generic constructor:
22/// let x = Constrained::<_, StrictlyNegative>::new(-1).unwrap();
23/// assert_eq!(x.into_inner(), -1);
24///
25/// // Associated constructor:
26/// let y = StrictlyNegative::new(-2.5).unwrap();
27/// assert_eq!(y.into_inner(), -2.5);
28///
29/// // Error cases:
30/// assert!(StrictlyNegative::new(0).is_err());
31/// assert!(StrictlyNegative::new(3).is_err());
32/// assert!(StrictlyNegative::new(f64::NAN).is_err());
33/// ```
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub struct StrictlyNegative;
36
37impl StrictlyNegative {
38    /// Constructs a [`Constrained<T, StrictlyNegative>`] if the value is strictly negative.
39    ///
40    /// # Errors
41    ///
42    /// Returns an error if the value is zero, positive, or not a number (`NaN`).
43    pub fn new<T: PartialOrd + Zero>(
44        value: T,
45    ) -> Result<Constrained<T, StrictlyNegative>, ConstraintError> {
46        Constrained::<T, StrictlyNegative>::new(value)
47    }
48}
49
50impl<T: PartialOrd + Zero> Constraint<T> for StrictlyNegative {
51    fn check(value: &T) -> Result<(), ConstraintError> {
52        match value.partial_cmp(&T::zero()) {
53            Some(Ordering::Less) => Ok(()),
54            Some(Ordering::Equal) => Err(ConstraintError::Zero),
55            Some(Ordering::Greater) => Err(ConstraintError::Negative),
56            None => Err(ConstraintError::NotANumber),
57        }
58    }
59}
60
61/// Adds two `Constrained<T, StrictlyNegative>` values.
62///
63/// Assumes that summing two negative values yields a negative 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-negative.
71impl<T> Add for Constrained<T, StrictlyNegative>
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-negative value, violating StrictlyNegative bound invariant"
82        );
83        Self {
84            value,
85            _marker: PhantomData,
86        }
87    }
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93
94    #[test]
95    fn integers() {
96        let x = Constrained::<i32, StrictlyNegative>::new(-1).unwrap();
97        assert_eq!(x.into_inner(), -1);
98
99        let y = StrictlyNegative::new(-42).unwrap();
100        assert_eq!(y.as_ref(), &-42);
101
102        assert!(StrictlyNegative::new(0).is_err());
103        assert!(StrictlyNegative::new(2).is_err());
104    }
105
106    #[test]
107    fn floats() {
108        assert!(Constrained::<f64, StrictlyNegative>::new(-1.0).is_ok());
109        assert!(StrictlyNegative::new(-0.1).is_ok());
110        assert!(StrictlyNegative::new(0.0).is_err());
111        assert!(StrictlyNegative::new(5.0).is_err());
112        assert!(StrictlyNegative::new(f64::NAN).is_err());
113    }
114}