icydb_base/sanitizer/
num.rs1use crate::{
2 core::traits::{NumCast, Sanitizer},
3 prelude::*,
4};
5
6#[sanitizer]
11pub struct Clamp {
12 min: Decimal,
13 max: Decimal,
14}
15
16impl Clamp {
17 pub fn new<N: NumCast>(min: N, max: N) -> Self {
18 let min = <Decimal as NumCast>::from(min).unwrap();
19 let max = <Decimal as NumCast>::from(max).unwrap();
20 assert!(min <= max, "clamp requires min <= max");
21
22 Self { min, max }
23 }
24}
25
26impl<T: NumCast + Clone> Sanitizer<T> for Clamp {
27 fn sanitize(&self, value: T) -> T {
28 let v = <Decimal as NumCast>::from(value).unwrap();
29
30 let clamped = if v < self.min {
31 self.min
32 } else if v > self.max {
33 self.max
34 } else {
35 v
36 };
37
38 <T as NumCast>::from(clamped).expect("clamped value must fit into target type")
40 }
41}
42
43#[sanitizer]
52pub struct RoundDecimalPlaces {
53 scale: u32,
54}
55
56impl RoundDecimalPlaces {
57 #[must_use]
58 pub const fn new(scale: u32) -> Self {
59 Self { scale }
60 }
61}
62
63impl Sanitizer<Decimal> for RoundDecimalPlaces {
64 fn sanitize(&self, value: Decimal) -> Decimal {
65 value.round_dp(self.scale)
66 }
67}
68
69#[cfg(test)]
74mod tests {
75 use super::*;
76 use std::str::FromStr;
77
78 #[test]
79 fn clamps_integers() {
80 let clamp = Clamp::new(10, 20);
81
82 assert_eq!(clamp.sanitize(5), 10, "below min should clamp to min");
83 assert_eq!(clamp.sanitize(25), 20, "above max should clamp to max");
84 assert_eq!(clamp.sanitize(15), 15, "within range should stay the same");
85 assert_eq!(clamp.sanitize(10), 10, "exact min should stay the same");
86 assert_eq!(clamp.sanitize(20), 20, "exact max should stay the same");
87 }
88
89 #[test]
90 fn handles_edge_cases() {
91 let clamp = Clamp::new(-10, -5);
92
93 assert_eq!(clamp.sanitize(-20), -10, "below min clamps to min");
94 assert_eq!(clamp.sanitize(-7), -7, "within range is untouched");
95 assert_eq!(clamp.sanitize(-5), -5, "exact max stays");
96 assert_eq!(clamp.sanitize(0), -5, "above max clamps to max");
97 }
98
99 #[test]
100 fn rounds_decimal_places_midpoint_away_from_zero() {
101 let round = RoundDecimalPlaces::new(2);
102
103 assert_eq!(
104 round.sanitize(Decimal::from_str("1.234").unwrap()),
105 Decimal::from_str("1.23").unwrap(),
106 "should round down when below midpoint"
107 );
108 assert_eq!(
109 round.sanitize(Decimal::from_str("1.235").unwrap()),
110 Decimal::from_str("1.24").unwrap(),
111 "should round up at midpoint away from zero"
112 );
113 assert_eq!(
114 round.sanitize(Decimal::from_str("-1.235").unwrap()),
115 Decimal::from_str("-1.24").unwrap(),
116 "negative midpoint should round away from zero"
117 );
118 }
119}