clamped_values/
lib.rs

1//! Provides a generic `ClampedValue` struct that stores a value and ensures that it is
2//! always within the specified minimum and maximum values.
3
4use num_traits::{SaturatingAdd, SaturatingMul, SaturatingSub};
5use std::ops::{AddAssign, Div, DivAssign, MulAssign, Sub, SubAssign};
6
7/// A value that is clamped between a minimum and maximum value.
8#[derive(Debug)]
9pub struct ClampedValue<T: PartialOrd + Clone> {
10    value: T,
11    min: T,
12    max: T,
13}
14
15impl<T: PartialOrd + Clone> ClampedValue<T> {
16    /// Creates a new `ClampedValue<T>`.
17    ///
18    /// # Panics
19    ///
20    /// Panics if either:
21    /// - `min` is larger than `max`
22    /// - `value` is not within `min` and `max`
23    pub fn new(min: T, value: T, max: T) -> Self {
24        if min > max {
25            panic!("Cannot create a clamped value where the minimum is larger than the maximum");
26        } else if value < min || value > max {
27            panic!("Cannot create a clamped value where the value is not within the minimum and maximum");
28        }
29
30        Self { value, min, max }
31    }
32
33    pub fn value(&self) -> &T {
34        &self.value
35    }
36
37    pub fn min(&self) -> &T {
38        &self.min
39    }
40
41    pub fn max(&self) -> &T {
42        &self.max
43    }
44
45    /// Sets the minimum to `new_min`.
46    ///
47    /// # Panics
48    ///
49    /// Panics if either:
50    /// - `new_min` is larger than the maximum
51    /// - `new_min` is larger than the current value
52    pub fn set_min(&mut self, new_min: T) {
53        if new_min > self.max {
54            panic!("Cannot set the minimum to a value that is larger than the maximum");
55        } else if new_min > self.value {
56            panic!("Cannot set the minimum to a value that is larger than the current value");
57        }
58
59        self.min = new_min;
60    }
61
62    /// Sets the maximum to `new_max`.
63    ///
64    /// # Panics
65    ///
66    /// Panics if either:
67    /// - `new_max` is smaller than the minimum
68    /// - `new_max` is smaller than the current value
69    pub fn set_max(&mut self, new_max: T) {
70        if new_max < self.min {
71            panic!("Cannot set the maximum to a value that is smaller than the minimum");
72        } else if new_max < self.value {
73            panic!("Cannot set the maximum to a value that is smaller than the current value")
74        }
75
76        self.max = new_max;
77    }
78
79    /// Sets the value to `new_value`, saturating at min or max if `new_value` is outside those bounds.
80    pub fn set(&mut self, new_value: T) {
81        self.value = new_value;
82        self.clamp();
83    }
84
85    // clamps self.value in between self.min and self.max
86    fn clamp(&mut self) {
87        if self.value < self.min {
88            self.value = self.min.clone();
89        } else if self.value > self.max {
90            self.value = self.max.clone();
91        }
92    }
93}
94
95impl<T> ClampedValue<T>
96where
97    T: Into<f32> + Sub<Output = T> + PartialOrd + Clone,
98{
99    /// Returns an f32 ranging from 0.0 to 1.0, representing the current value
100    /// in relation to the minimum and maximum, where 0.0 is the minimum and
101    /// 1.0 is the maximum
102    /// 
103    /// # Examples
104    /// 
105    /// ```
106    /// use clamped_values::ClampedValue;
107    /// 
108    /// let clamped_value = ClampedValue::<u8>::new(50, 75, 100);
109    /// 
110    /// assert_eq!(clamped_value.percent_f32(), 0.5);
111    /// ```
112    pub fn percent_f32(&self) -> f32 {
113        self.percent::<f32>()
114    }
115}
116
117impl<T> ClampedValue<T>
118where
119    T: Into<f64> + Sub<Output = T> + PartialOrd + Clone,
120{
121    /// Returns an f64 ranging from 0.0 to 1.0, representing the current value
122    /// in relation to the minimum and maximum, where 0.0 is the minimum and
123    /// 1.0 is the maximum
124    /// 
125    /// # Examples
126    /// 
127    /// ```
128    /// use clamped_values::ClampedValue;
129    /// 
130    /// let clamped_value = ClampedValue::<u8>::new(50, 75, 100);
131    /// 
132    /// assert_eq!(clamped_value.percent_f64(), 0.5);
133    /// ``` 
134    pub fn percent_f64(&self) -> f64 {
135        self.percent::<f64>()
136    }
137}
138
139// generic version of the percent code so that we can use the same logic for f32 and f64
140impl<T: Sub<Output = T> + PartialOrd + Clone> ClampedValue<T> {
141    fn percent<U>(&self) -> U
142    where
143        U: Div<Output = U>,
144        T: Into<U>,
145    {
146        // we can sub these values by self.min without worrying about overflow due to the fact that
147        // self.min is ALWAYS smaller than or equal to self.value and self.max
148        (self.value.clone() - self.min.clone()).into()
149            / (self.max.clone() - self.min.clone()).into()
150    }
151}
152
153// For the following three impl blocks, the "Saturating" version of the operation is implemented as opposed to
154// the regular operation due to the fact that the regular operations allow the possibility of
155// overflowing (in debug) or wrapping (in release), which is unexpected behaviour.
156
157impl<T: SaturatingAdd + PartialOrd + Clone> AddAssign<T> for ClampedValue<T> {
158    /// Adds `rhs` to the current value, saturating at the minimum or maximum.
159    ///
160    /// # Examples
161    ///
162    /// ```
163    /// use clamped_values::ClampedValue;
164    ///
165    /// let mut clamped_value = ClampedValue::new(0, 5, 10);
166    ///
167    /// clamped_value += 3;
168    ///
169    /// assert_eq!(*clamped_value.value(), 8);
170    /// ```
171    fn add_assign(&mut self, rhs: T) {
172        self.value = self.value.saturating_add(&rhs);
173        self.clamp();
174    }
175}
176
177impl<T: SaturatingSub + PartialOrd + Clone> SubAssign<T> for ClampedValue<T> {
178    /// Subtracts `rhs` from the value, saturating at the minimum or maximum.
179    /// 
180    /// # Examples
181    /// 
182    /// ```
183    /// use clamped_values::ClampedValue;
184    /// 
185    /// let mut clamped_value = ClampedValue::new(0, 5, 10);
186    /// 
187    /// clamped_value -= 4;
188    /// 
189    /// assert_eq!(*clamped_value.value(), 1);
190    /// ```
191    fn sub_assign(&mut self, rhs: T) {
192        self.value = self.value.saturating_sub(&rhs);
193        self.clamp();
194    }
195}
196
197impl<T: SaturatingMul + PartialOrd + Clone> MulAssign<T> for ClampedValue<T> {
198    /// Multiplies the value by `rhs`, saturating at the minimum or maximum.
199    /// 
200    /// # Examples
201    /// 
202    /// ```
203    /// use clamped_values::ClampedValue;
204    /// 
205    /// let mut clamped_value = ClampedValue::new(0, 2, 10);
206    /// 
207    /// clamped_value *= 3;
208    /// 
209    /// assert_eq!(*clamped_value.value(), 6);
210    /// ``` 
211    fn mul_assign(&mut self, rhs: T) {
212        self.value = self.value.saturating_mul(&rhs);
213        self.clamp();
214    }
215}
216
217impl<T: Div<Output = T> + PartialOrd + Clone> DivAssign<T> for ClampedValue<T> {
218    /// Divides the value by `rhs`, saturating at the minimum or maximum.
219    /// 
220    /// # Examples
221    /// 
222    /// ```
223    /// use clamped_values::ClampedValue;
224    /// 
225    /// let mut clamped_value = ClampedValue::new(0, 8, 10);
226    /// 
227    /// clamped_value /= 2;
228    /// 
229    /// assert_eq!(*clamped_value.value(), 4);
230    /// ``` 
231    fn div_assign(&mut self, rhs: T) {
232        self.value = self.value.clone() / rhs;
233        self.clamp();
234    }
235}
236
237#[cfg(test)]
238mod tests {
239    use crate::ClampedValue;
240
241    #[test]
242    fn new() {
243        ClampedValue::new(10, 20, 30);
244    }
245
246    #[test]
247    #[should_panic]
248    fn new_min_larger_than_max() {
249        ClampedValue::new(30, 10, 20);
250    }
251
252    #[test]
253    #[should_panic]
254    fn new_value_outside_min_max() {
255        ClampedValue::new(10, 40, 30);
256    }
257
258    #[test]
259    fn set() {
260        let mut clamped_value = ClampedValue::new(10, 50, 110);
261
262        clamped_value.set_min(22);
263        assert_eq!(*clamped_value.min(), 22);
264
265        clamped_value.set_max(99);
266        assert_eq!(*clamped_value.max(), 99);
267
268        clamped_value.set(55);
269        assert_eq!(*clamped_value.value(), 55);
270
271        clamped_value.set(1000);
272        assert_eq!(*clamped_value.value(), *clamped_value.max());
273
274        clamped_value.set(-1000);
275        assert_eq!(*clamped_value.value(), *clamped_value.min());
276    }
277
278    #[test]
279    #[should_panic]
280    fn set_min_larger_than_max() {
281        ClampedValue::new(10, 20, 30).set_min(40);
282    }
283
284    #[test]
285    #[should_panic]
286    fn set_min_larger_than_value() {
287        ClampedValue::new(10, 20, 30).set_min(25);
288    }
289
290    #[test]
291    #[should_panic]
292    fn set_max_smaller_than_min() {
293        ClampedValue::new(10, 20, 30).set_max(0);
294    }
295
296    #[test]
297    #[should_panic]
298    fn set_max_smaller_than_value() {
299        ClampedValue::new(10, 20, 30).set_max(15);
300    }
301
302    #[test]
303    fn percent() {
304        // works with all positive numbers
305        let c = ClampedValue::<u8>::new(75, 100, 125);
306        assert_eq!(c.percent_f32(), 0.5);
307        assert_eq!(c.percent_f64(), 0.5);
308
309        // works with all negative numbers
310        let c = ClampedValue::<i8>::new(-100, -40, -20);
311        assert_eq!(c.percent_f32(), 0.75);
312        assert_eq!(c.percent_f64(), 0.75);
313
314        // works with mix of negative and positive numbers
315        let c = ClampedValue::<i8>::new(-40, -10, 40);
316        assert_eq!(c.percent_f32(), 0.375);
317        assert_eq!(c.percent_f64(), 0.375);
318    }
319
320    #[test]
321    fn operations() {
322        let mut clamped_value = ClampedValue::new(20, 20, 40);
323
324        clamped_value += 100;
325        assert_eq!(*clamped_value.value(), *clamped_value.max());
326
327        clamped_value -= 100;
328        assert_eq!(*clamped_value.value(), *clamped_value.min(),);
329
330        clamped_value *= 100;
331        assert_eq!(*clamped_value.value(), *clamped_value.max(),);
332
333        clamped_value /= 10;
334        assert_eq!(*clamped_value.value(), *clamped_value.min());
335    }
336}