Skip to main content

blockworx/
bounded.rs

1//! A clamped `f32` newtype, generic over its bounds. `Bounded<B>` can only
2//! hold values in `[B::MIN, B::MAX]`: the constructor clamps, every arithmetic
3//! op re-clamps, and NaN collapses to `B::MIN` — so the type is totally
4//! ordered and `Eq`/`Ord` are sound.
5
6use std::cmp::Ordering;
7use std::marker::PhantomData;
8
9/// The closed interval a [`Bounded`] value lives in. Implemented by empty
10/// marker types (`ProgressBounds`, `ZoomBounds`, …).
11pub trait Bounds {
12    const MIN: f32;
13    const MAX: f32;
14}
15
16pub struct Bounded<B: Bounds>(f32, PhantomData<B>);
17
18impl<B: Bounds> Bounded<B> {
19    /// Clamp `value` into the bounds; NaN collapses to `B::MIN`.
20    pub const fn new(value: f32) -> Self {
21        let value = if value.is_nan() || value < B::MIN {
22            B::MIN
23        } else if value > B::MAX {
24            B::MAX
25        } else {
26            value
27        };
28        Self(value, PhantomData)
29    }
30
31    pub const fn get(self) -> f32 {
32        self.0
33    }
34
35    pub const fn min_value() -> Self {
36        Self::new(B::MIN)
37    }
38
39    pub const fn max_value() -> Self {
40        Self::new(B::MAX)
41    }
42}
43
44// Manual impls so `B` needs no derives of its own — the marker never appears
45// in a value position.
46impl<B: Bounds> Copy for Bounded<B> {}
47
48impl<B: Bounds> Clone for Bounded<B> {
49    fn clone(&self) -> Self {
50        *self
51    }
52}
53
54impl<B: Bounds> std::fmt::Debug for Bounded<B> {
55    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56        write!(f, "{:?}", self.0)
57    }
58}
59
60impl<B: Bounds> PartialEq for Bounded<B> {
61    fn eq(&self, other: &Self) -> bool {
62        self.0 == other.0
63    }
64}
65
66// Sound: construction excludes NaN, so `==` is reflexive and `total_cmp` a
67// total order.
68impl<B: Bounds> Eq for Bounded<B> {}
69
70impl<B: Bounds> PartialOrd for Bounded<B> {
71    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
72        Some(self.cmp(other))
73    }
74}
75
76impl<B: Bounds> Ord for Bounded<B> {
77    fn cmp(&self, other: &Self) -> Ordering {
78        self.0.total_cmp(&other.0)
79    }
80}
81
82impl<B: Bounds> From<Bounded<B>> for f32 {
83    fn from(value: Bounded<B>) -> f32 {
84        value.0
85    }
86}
87
88impl<B: Bounds> std::ops::Add for Bounded<B> {
89    type Output = Self;
90    fn add(self, rhs: Self) -> Self {
91        Self::new(self.0 + rhs.0)
92    }
93}
94
95impl<B: Bounds> std::ops::Sub for Bounded<B> {
96    type Output = Self;
97    fn sub(self, rhs: Self) -> Self {
98        Self::new(self.0 - rhs.0)
99    }
100}
101
102impl<B: Bounds> std::ops::Mul<f32> for Bounded<B> {
103    type Output = Self;
104    fn mul(self, rhs: f32) -> Self {
105        Self::new(self.0 * rhs)
106    }
107}
108
109impl<B: Bounds> std::ops::Div<f32> for Bounded<B> {
110    type Output = Self;
111    fn div(self, rhs: f32) -> Self {
112        Self::new(self.0 / rhs)
113    }
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119
120    // Deliberately underivable: proves `Bounded`'s manual `Copy`/`Clone`/`Eq`
121    // place no requirements on the marker.
122    struct UnitBounds;
123    impl Bounds for UnitBounds {
124        const MIN: f32 = 0.0;
125        const MAX: f32 = 1.0;
126    }
127    type Unit = Bounded<UnitBounds>;
128
129    struct WideBounds;
130    impl Bounds for WideBounds {
131        const MIN: f32 = -10.0;
132        const MAX: f32 = f32::INFINITY;
133    }
134    type Wide = Bounded<WideBounds>;
135
136    #[test]
137    fn construction_clamps_into_the_bounds() {
138        assert_eq!(Unit::new(0.5).get(), 0.5);
139        assert_eq!(Unit::new(-3.0).get(), 0.0);
140        assert_eq!(Unit::new(7.0).get(), 1.0);
141        assert_eq!(Unit::new(f32::NEG_INFINITY).get(), 0.0);
142        assert_eq!(Unit::new(f32::INFINITY).get(), 1.0);
143    }
144
145    #[test]
146    fn nan_collapses_to_the_minimum() {
147        assert_eq!(Unit::new(f32::NAN).get(), 0.0);
148        assert_eq!(Wide::new(f32::NAN).get(), -10.0);
149    }
150
151    #[test]
152    fn an_unbounded_maximum_passes_values_through() {
153        assert_eq!(Wide::new(1e30).get(), 1e30);
154        assert_eq!(Wide::new(f32::INFINITY).get(), f32::INFINITY);
155    }
156
157    #[test]
158    fn addition_and_subtraction_saturate() {
159        assert_eq!(Unit::new(0.75) + Unit::new(0.75), Unit::new(1.0));
160        assert_eq!(Unit::new(0.25) - Unit::new(0.75), Unit::new(0.0));
161        assert_eq!((Unit::new(0.25) + Unit::new(0.5)).get(), 0.75);
162    }
163
164    #[test]
165    fn scaling_clamps_and_survives_nan_producing_math() {
166        assert_eq!((Unit::new(0.5) * 4.0).get(), 1.0);
167        assert_eq!((Unit::new(0.5) * -1.0).get(), 0.0);
168        assert_eq!((Unit::new(0.5) / 0.5).get(), 1.0);
169        // 0/0 is NaN; the result must still be in-bounds.
170        assert_eq!((Unit::new(0.0) / 0.0).get(), 0.0);
171        assert_eq!((Unit::new(0.5) * f32::NAN).get(), 0.0);
172    }
173
174    #[test]
175    fn the_order_is_total_and_consistent_with_eq() {
176        let mut values = vec![Unit::new(0.9), Unit::new(0.1), Unit::new(0.5)];
177        values.sort();
178        assert_eq!(values, vec![Unit::new(0.1), Unit::new(0.5), Unit::new(0.9)]);
179        assert!(Unit::new(0.1) < Unit::new(0.2));
180        assert_eq!(Unit::new(2.0), Unit::new(1.0), "both clamp to MAX");
181        assert_eq!(Unit::new(0.3).cmp(&Unit::new(0.3)), Ordering::Equal);
182    }
183
184    #[test]
185    fn min_and_max_value_bracket_every_construction() {
186        assert_eq!(Unit::min_value().get(), 0.0);
187        assert_eq!(Unit::max_value().get(), 1.0);
188        assert!(Unit::min_value() <= Unit::new(-100.0));
189        assert!(Unit::max_value() >= Unit::new(100.0));
190    }
191
192    #[test]
193    fn into_f32_returns_the_clamped_value() {
194        let x: f32 = Unit::new(3.0).into();
195        assert_eq!(x, 1.0);
196    }
197
198    #[test]
199    fn copy_works_without_marker_derives() {
200        let a = Unit::new(0.4);
201        let b = a; // move would poison the next use if Copy were missing
202        assert_eq!(a, b);
203    }
204}