textiler_core/utils/
bounded_float.rs

1use serde::{Deserialize, Serialize};
2use std::cmp::Ordering;
3use std::fmt::{Debug, Display, Formatter};
4use std::hash::{Hash, Hasher};
5use std::ops::{Add, Deref, Sub};
6
7#[derive(Copy, Clone, PartialEq, PartialOrd, Serialize, Deserialize)]
8#[serde(transparent)]
9pub struct BoundedFloat<const LOW: i16, const HIGH: i16> {
10    num: f32,
11}
12
13impl<const LOW: i16, const HIGH: i16> Debug for BoundedFloat<LOW, HIGH> {
14    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
15        write!(f, "{:.?}", self.num)
16    }
17}
18
19impl<const LOW: i16, const HIGH: i16> Display for BoundedFloat<LOW, HIGH> {
20    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
21        write!(f, "{}", self.num)
22    }
23}
24
25impl<const LOW: i16, const HIGH: i16> Sub for BoundedFloat<LOW, HIGH> {
26    type Output = f32;
27
28    fn sub(self, rhs: Self) -> Self::Output {
29        *self - *rhs
30    }
31}
32
33impl<const LOW: i16, const HIGH: i16> Add for BoundedFloat<LOW, HIGH> {
34    type Output = f32;
35
36    fn add(self, rhs: Self) -> Self::Output {
37        *self + *rhs
38    }
39}
40
41impl<const LOW: i16, const HIGH: i16> Eq for BoundedFloat<LOW, HIGH> {}
42
43impl<const LOW: i16, const HIGH: i16> Ord for BoundedFloat<LOW, HIGH> {
44    fn cmp(&self, other: &Self) -> Ordering {
45        self.partial_cmp(other)
46            .expect("should always be successful")
47    }
48}
49
50impl<const LOW: i16, const HIGH: i16> Hash for BoundedFloat<LOW, HIGH> {
51    fn hash<H: Hasher>(&self, state: &mut H) {
52        let raw_bytes = self.to_be_bytes();
53        u32::from_be_bytes(raw_bytes).hash(state);
54    }
55}
56
57impl<const LOW: i16, const HIGH: i16> Deref for BoundedFloat<LOW, HIGH> {
58    type Target = f32;
59
60    fn deref(&self) -> &Self::Target {
61        &self.num
62    }
63}
64
65impl<const LOW: i16, const HIGH: i16> BoundedFloat<LOW, HIGH> {
66    pub const MIN: BoundedFloat<LOW, HIGH> = BoundedFloat { num: LOW as f32 };
67
68    pub const MAX: BoundedFloat<LOW, HIGH> = BoundedFloat { num: HIGH as f32 };
69
70    /// Creates a bounded float value, if the given val is within the range given
71    pub fn new(val: f32) -> Option<Self> {
72        if val >= LOW as f32 && val <= HIGH as f32 {
73            Some(Self { num: val })
74        } else {
75            None
76        }
77    }
78}
79
80#[cfg(test)]
81mod tests {
82    use crate::utils::bounded_float::BoundedFloat;
83    use std::collections::BTreeSet;
84
85    #[test]
86    fn bounded_floats() {
87        BoundedFloat::<0, 1>::new(0.1).expect("is valid");
88        assert!(BoundedFloat::<0, 1>::new(1.1).is_none());
89    }
90
91    #[test]
92    fn bounded_floats_inclusive() {
93        BoundedFloat::<0, 1>::new(0.).expect("is valid");
94        BoundedFloat::<0, 1>::new(1.).expect("is valid");
95    }
96
97    #[test]
98    fn bounded_float_rejects_nan() {
99        assert!(BoundedFloat::<0, 1>::new(f32::NAN).is_none());
100    }
101
102    #[test]
103    fn bounded_float_btree() {
104        let mut map = BTreeSet::<BoundedFloat<0, 1>>::new();
105        map.insert(BoundedFloat::new(0.7).unwrap());
106        map.insert(BoundedFloat::new(0.03).unwrap());
107        map.insert(BoundedFloat::new(0.9).unwrap());
108        map.insert(BoundedFloat::new(0.8).unwrap());
109        map.insert(BoundedFloat::new(0.2).unwrap());
110
111        map.iter()
112            .inspect(|f| println!("{f:#?}"))
113            .try_fold(
114                f32::MIN,
115                |acc, &next| {
116                    if *next > acc {
117                        Ok(*next)
118                    } else {
119                        Err(())
120                    }
121                },
122            )
123            .expect("should be okay");
124    }
125}