icydb_model/base/normalizer/
num.rs1use crate::{
8 base::helper::try_cast_decimal, prelude::*, schema::NumericValue, visitor::Normalizer,
9};
10use std::any::type_name;
11
12#[normalizer]
17pub struct Clamp {
18 min: Decimal,
19 max: Decimal,
20 invalid_configuration: bool,
21}
22
23impl Clamp {
24 pub fn new<N: NumericValue>(min: N, max: N) -> Self {
26 let min = try_cast_decimal(&min);
27 let max = try_cast_decimal(&max);
28
29 Self {
32 min: min.unwrap_or_default(),
33 max: max.unwrap_or_default(),
34 invalid_configuration: min.is_none() || max.is_none(),
35 }
36 }
37}
38
39impl<T: NumericValue> Normalizer<T> for Clamp {
40 fn normalize(&self, value: &mut T) -> Result<(), String> {
41 if self.invalid_configuration {
42 return Err("Clamp bounds cannot be represented as Decimal".to_string());
43 }
44 if self.min > self.max {
45 return Err(format!(
46 "Clamp requires min <= max (got {}..={})",
47 self.min, self.max
48 ));
49 }
50
51 let v = try_cast_decimal(value).ok_or_else(|| {
52 format!(
53 "value of type {} cannot be represented as Decimal",
54 type_name::<T>()
55 )
56 })?;
57
58 let clamped = if v < self.min {
59 self.min
60 } else if v > self.max {
61 self.max
62 } else {
63 v
64 };
65
66 *value = T::try_from_decimal(clamped).ok_or_else(|| {
67 format!(
68 "clamped value cannot be represented as {}",
69 type_name::<T>()
70 )
71 })?;
72
73 Ok(())
74 }
75}
76
77#[normalizer]
82pub struct RoundDecimalPlaces {
83 scale: u32,
84 invalid_configuration: bool,
85}
86
87impl RoundDecimalPlaces {
88 #[must_use]
90 pub fn new(scale: impl TryInto<u32>) -> Self {
91 let scale = scale.try_into().ok();
92 Self {
93 scale: scale.unwrap_or_default(),
94 invalid_configuration: scale.is_none(),
95 }
96 }
97}
98
99impl Normalizer<Decimal> for RoundDecimalPlaces {
100 fn normalize(&self, value: &mut Decimal) -> Result<(), String> {
101 if self.invalid_configuration {
102 return Err("decimal rounding scale cannot be represented as u32".to_string());
103 }
104 *value = value.round_dp(self.scale);
105
106 Ok(())
107 }
108}
109
110#[cfg(test)]
115mod tests {
116 use super::*;
117 use core::str::FromStr;
118
119 fn dec(v: &str) -> Decimal {
120 Decimal::from_str(v).unwrap()
121 }
122
123 #[test]
124 fn clamps_integers() {
125 let clamp = Clamp::new(10, 20);
126
127 let mut v = 5;
128 clamp.normalize(&mut v).unwrap();
129 assert_eq!(v, 10);
130
131 let mut v = 25;
132 clamp.normalize(&mut v).unwrap();
133 assert_eq!(v, 20);
134
135 let mut v = 15;
136 clamp.normalize(&mut v).unwrap();
137 assert_eq!(v, 15);
138 }
139
140 #[test]
141 fn clamp_invalid_config() {
142 let clamp = Clamp::new(20, 10);
143
144 let mut v = 15;
145 assert!(clamp.normalize(&mut v).is_err());
146 }
147
148 #[test]
149 fn invalid_numeric_clamp_configuration_preserves_values() {
150 let clamps = [
151 Clamp::new(0_u128, u128::MAX),
152 Clamp::new(u128::MAX, 0),
153 Clamp::new(u128::MAX, u128::MAX),
154 Clamp::new(0.0_f64, f64::NAN),
155 Clamp::new(f64::INFINITY, 1.0),
156 Clamp::new(0.0, f64::NEG_INFINITY),
157 Clamp::new(0.0, 1e40),
158 Clamp::new(-1e40, 0.0),
159 Clamp::new(0.0_f32, f32::MAX),
160 ];
161 for clamp in clamps {
162 for original in [0_u128, 7, u128::MAX] {
163 let mut value = original;
164 for _ in 0..2 {
165 assert!(clamp.normalize(&mut value).is_err());
166 assert_eq!(value, original);
167 }
168 }
169 }
170 }
171
172 #[test]
173 fn invalid_numeric_rounding_configuration_preserves_values() {
174 for round in [
175 RoundDecimalPlaces::new(-1),
176 RoundDecimalPlaces::new(u64::MAX),
177 ] {
178 for original in [dec("1.235"), dec("-1.235"), Decimal::ZERO] {
179 let mut value = original;
180 for _ in 0..2 {
181 assert!(round.normalize(&mut value).is_err());
182 assert_eq!(value, original);
183 }
184 }
185 }
186 }
187
188 #[test]
189 fn numeric_normalizer_boundaries_and_defaults_remain_valid() {
190 let mut value = i128::MIN;
191 Clamp::new(i128::MIN, i128::MAX)
192 .normalize(&mut value)
193 .unwrap();
194 assert_eq!(value, i128::MIN);
195 let mut value = i128::MAX as u128;
196 Clamp::new(0_u128, i128::MAX as u128)
197 .normalize(&mut value)
198 .unwrap();
199 assert_eq!(value, i128::MAX as u128);
200 let mut value = dec("1.235");
201 Clamp::new(dec("1.2"), dec("1.3"))
202 .normalize(&mut value)
203 .unwrap();
204 assert_eq!(value, dec("1.235"));
205 let mut value = 7;
206 Clamp::default().normalize(&mut value).unwrap();
207 assert_eq!(value, 0);
208 for (round, expected) in [
209 (RoundDecimalPlaces::default(), dec("1")),
210 (RoundDecimalPlaces::new(2), dec("1.24")),
211 (RoundDecimalPlaces::new(u32::MAX), dec("1.235")),
212 ] {
213 let mut value = dec("1.235");
214 round.normalize(&mut value).unwrap();
215 assert_eq!(value, expected);
216 }
217 }
218
219 #[test]
220 fn rounds_decimal_places_midpoint_away_from_zero() {
221 let round = RoundDecimalPlaces::new(2);
222
223 let mut v = dec("1.234");
224 round.normalize(&mut v).unwrap();
225 assert_eq!(v, dec("1.23"));
226
227 let mut v = dec("1.235");
228 round.normalize(&mut v).unwrap();
229 assert_eq!(v, dec("1.24"));
230
231 let mut v = dec("-1.235");
232 round.normalize(&mut v).unwrap();
233 assert_eq!(v, dec("-1.24"));
234 }
235}