textiler_core/utils/
bounded_float.rs1use 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 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}