blockworx_geom/
bounded.rs1use std::cmp::Ordering;
7use std::marker::PhantomData;
8
9pub 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 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
44impl<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
66impl<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
88#[derive(Clone, Copy, Debug, PartialEq)]
92pub struct OutOfBounds {
93 pub value: f32,
94 pub min: f32,
95 pub max: f32,
96}
97
98impl std::fmt::Display for OutOfBounds {
99 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
100 write!(f, "{} is outside [{}, {}]", self.value, self.min, self.max)
101 }
102}
103
104impl std::error::Error for OutOfBounds {}
105
106impl<B: Bounds> TryFrom<f32> for Bounded<B> {
107 type Error = OutOfBounds;
108 fn try_from(value: f32) -> Result<Self, Self::Error> {
109 if (B::MIN..=B::MAX).contains(&value) {
110 Ok(Self(value, PhantomData))
111 } else {
112 Err(OutOfBounds {
113 value,
114 min: B::MIN,
115 max: B::MAX,
116 })
117 }
118 }
119}
120
121impl<B: Bounds> serde::Serialize for Bounded<B> {
124 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
125 serializer.serialize_f32(self.0)
126 }
127}
128
129impl<'de, B: Bounds> serde::Deserialize<'de> for Bounded<B> {
130 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
131 let value = f32::deserialize(deserializer)?;
132 Self::try_from(value).map_err(serde::de::Error::custom)
133 }
134}
135
136impl<B: Bounds> std::ops::Add for Bounded<B> {
137 type Output = Self;
138 fn add(self, rhs: Self) -> Self {
139 Self::new(self.0 + rhs.0)
140 }
141}
142
143impl<B: Bounds> std::ops::Sub for Bounded<B> {
144 type Output = Self;
145 fn sub(self, rhs: Self) -> Self {
146 Self::new(self.0 - rhs.0)
147 }
148}
149
150impl<B: Bounds> std::ops::Mul<f32> for Bounded<B> {
151 type Output = Self;
152 fn mul(self, rhs: f32) -> Self {
153 Self::new(self.0 * rhs)
154 }
155}
156
157impl<B: Bounds> std::ops::Div<f32> for Bounded<B> {
158 type Output = Self;
159 fn div(self, rhs: f32) -> Self {
160 Self::new(self.0 / rhs)
161 }
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167
168 struct UnitBounds;
171 impl Bounds for UnitBounds {
172 const MIN: f32 = 0.0;
173 const MAX: f32 = 1.0;
174 }
175 type Unit = Bounded<UnitBounds>;
176
177 struct WideBounds;
178 impl Bounds for WideBounds {
179 const MIN: f32 = -10.0;
180 const MAX: f32 = f32::INFINITY;
181 }
182 type Wide = Bounded<WideBounds>;
183
184 #[test]
185 fn a_value_read_outside_the_bounds_is_refused_not_clamped() {
186 assert_eq!(Unit::try_from(0.5), Ok(Unit::new(0.5)));
187 for refused in [-0.25, 1.5, f32::NAN] {
188 assert!(Unit::try_from(refused).is_err(), "{refused} was accepted");
189 }
190 assert_eq!(Wide::try_from(f32::INFINITY), Ok(Wide::new(f32::INFINITY)));
191 }
192
193 #[test]
194 fn construction_clamps_into_the_bounds() {
195 assert_eq!(Unit::new(0.5).get(), 0.5);
196 assert_eq!(Unit::new(-3.0).get(), 0.0);
197 assert_eq!(Unit::new(7.0).get(), 1.0);
198 assert_eq!(Unit::new(f32::NEG_INFINITY).get(), 0.0);
199 assert_eq!(Unit::new(f32::INFINITY).get(), 1.0);
200 }
201
202 #[test]
203 fn nan_collapses_to_the_minimum() {
204 assert_eq!(Unit::new(f32::NAN).get(), 0.0);
205 assert_eq!(Wide::new(f32::NAN).get(), -10.0);
206 }
207
208 #[test]
209 fn an_unbounded_maximum_passes_values_through() {
210 assert_eq!(Wide::new(1e30).get(), 1e30);
211 assert_eq!(Wide::new(f32::INFINITY).get(), f32::INFINITY);
212 }
213
214 #[test]
215 fn addition_and_subtraction_saturate() {
216 assert_eq!(Unit::new(0.75) + Unit::new(0.75), Unit::new(1.0));
217 assert_eq!(Unit::new(0.25) - Unit::new(0.75), Unit::new(0.0));
218 assert_eq!((Unit::new(0.25) + Unit::new(0.5)).get(), 0.75);
219 }
220
221 #[test]
222 fn scaling_clamps_and_survives_nan_producing_math() {
223 assert_eq!((Unit::new(0.5) * 4.0).get(), 1.0);
224 assert_eq!((Unit::new(0.5) * -1.0).get(), 0.0);
225 assert_eq!((Unit::new(0.5) / 0.5).get(), 1.0);
226 assert_eq!((Unit::new(0.0) / 0.0).get(), 0.0);
228 assert_eq!((Unit::new(0.5) * f32::NAN).get(), 0.0);
229 }
230
231 #[test]
232 fn the_order_is_total_and_consistent_with_eq() {
233 let mut values = vec![Unit::new(0.9), Unit::new(0.1), Unit::new(0.5)];
234 values.sort();
235 assert_eq!(values, vec![Unit::new(0.1), Unit::new(0.5), Unit::new(0.9)]);
236 assert!(Unit::new(0.1) < Unit::new(0.2));
237 assert_eq!(Unit::new(2.0), Unit::new(1.0), "both clamp to MAX");
238 assert_eq!(Unit::new(0.3).cmp(&Unit::new(0.3)), Ordering::Equal);
239 }
240
241 #[test]
242 fn min_and_max_value_bracket_every_construction() {
243 assert_eq!(Unit::min_value().get(), 0.0);
244 assert_eq!(Unit::max_value().get(), 1.0);
245 assert!(Unit::min_value() <= Unit::new(-100.0));
246 assert!(Unit::max_value() >= Unit::new(100.0));
247 }
248
249 #[test]
250 fn into_f32_returns_the_clamped_value() {
251 let x: f32 = Unit::new(3.0).into();
252 assert_eq!(x, 1.0);
253 }
254
255 #[test]
256 fn copy_works_without_marker_derives() {
257 let a = Unit::new(0.4);
258 let b = a; assert_eq!(a, b);
260 }
261}