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}
21
22impl Clamp {
23    pub fn new<N: NumericValue>(min: N, max: N) -> Self {
24        let min = try_cast_decimal(&min).unwrap_or_default();
25        let max = try_cast_decimal(&max).unwrap_or_default();
26
27        Self { min, max }
28    }
29}
30
31impl<T: NumericValue> Normalizer<T> for Clamp {
32    fn normalize(&self, value: &mut T) -> Result<(), String> {
33        if self.min > self.max {
34            return Err(format!(
35                "Clamp requires min <= max (got {}..={})",
36                self.min, self.max
37            ));
38        }
39
40        let v = try_cast_decimal(value).ok_or_else(|| {
41            format!(
42                "value of type {} cannot be represented as Decimal",
43                type_name::<T>()
44            )
45        })?;
46
47        let clamped = if v < self.min {
48            self.min
49        } else if v > self.max {
50            self.max
51        } else {
52            v
53        };
54
55        *value = T::try_from_decimal(clamped).ok_or_else(|| {
56            format!(
57                "clamped value cannot be represented as {}",
58                type_name::<T>()
59            )
60        })?;
61
62        Ok(())
63    }
64}
65
66///
67/// RoundDecimalPlaces
68///
69
70#[normalizer]
71pub struct RoundDecimalPlaces {
72    scale: u32,
73}
74
75impl RoundDecimalPlaces {
76    #[must_use]
77    pub fn new(scale: impl TryInto<u32>) -> Self {
78        Self {
79            scale: scale.try_into().unwrap_or_default(),
80        }
81    }
82}
83
84impl Normalizer<Decimal> for RoundDecimalPlaces {
85    fn normalize(&self, value: &mut Decimal) -> Result<(), String> {
86        *value = value.round_dp(self.scale);
87
88        Ok(())
89    }
90}
91
92///
93/// TESTS
94///
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99    use core::str::FromStr;
100
101    fn dec(v: &str) -> Decimal {
102        Decimal::from_str(v).unwrap()
103    }
104
105    #[test]
106    fn clamps_integers() {
107        let clamp = Clamp::new(10, 20);
108
109        let mut v = 5;
110        clamp.normalize(&mut v).unwrap();
111        assert_eq!(v, 10);
112
113        let mut v = 25;
114        clamp.normalize(&mut v).unwrap();
115        assert_eq!(v, 20);
116
117        let mut v = 15;
118        clamp.normalize(&mut v).unwrap();
119        assert_eq!(v, 15);
120    }
121
122    #[test]
123    fn clamp_invalid_config() {
124        let clamp = Clamp::new(20, 10);
125
126        let mut v = 15;
127        assert!(clamp.normalize(&mut v).is_err());
128    }
129
130    #[test]
131    fn rounds_decimal_places_midpoint_away_from_zero() {
132        let round = RoundDecimalPlaces::new(2);
133
134        let mut v = dec("1.234");
135        round.normalize(&mut v).unwrap();
136        assert_eq!(v, dec("1.23"));
137
138        let mut v = dec("1.235");
139        round.normalize(&mut v).unwrap();
140        assert_eq!(v, dec("1.24"));
141
142        let mut v = dec("-1.235");
143        round.normalize(&mut v).unwrap();
144        assert_eq!(v, dec("-1.24"));
145    }
146}