Skip to main content

icydb_model/base/normalizer/
num.rs

1//! Module: base::normalizer::num
2//!
3//! Responsibility: base normalizer definitions.
4//! Does not own: validation policy, persistence, or schema mutation semantics.
5//! Boundary: mutates schema field values through facade normalizer traits.
6
7use crate::{
8    base::helper::try_cast_decimal, prelude::*, schema::NumericValue, visitor::Normalizer,
9};
10use std::any::type_name;
11
12///
13/// Clamp
14///
15
16#[normalizer]
17pub struct Clamp {
18    min: Decimal,
19    max: Decimal,
20    invalid_configuration: bool,
21}
22
23impl Clamp {
24    /// Configure bounds; unrepresentable bounds make normalization fail without mutation.
25    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        // Keep conversion failure distinct from a real zero bound. The derived
30        // Default remains the valid zero interval used by existing callers.
31        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///
78/// RoundDecimalPlaces
79///
80
81#[normalizer]
82pub struct RoundDecimalPlaces {
83    scale: u32,
84    invalid_configuration: bool,
85}
86
87impl RoundDecimalPlaces {
88    /// Configure a scale; failed conversion makes normalization fail without mutation.
89    #[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///
111/// TESTS
112///
113
114#[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}