Skip to main content

cassandra_protocol/types/
decimal.rs

1use derive_more::Constructor;
2use float_eq::*;
3use num_bigint::BigInt;
4use std::io::Cursor;
5
6use crate::frame::{Serialize, Version};
7
8/// Cassandra Decimal type
9#[derive(Debug, Clone, PartialEq, Constructor, Ord, PartialOrd, Eq, Hash)]
10pub struct Decimal {
11    pub unscaled: BigInt,
12    pub scale: i32,
13}
14
15impl Decimal {
16    /// Method that returns plain `BigInt` value.
17    ///
18    /// Negative scale is handled by multiplying instead of dividing - that
19    /// avoids the previous `scale as u32` cast which made a negative scale
20    /// wrap to a huge value and panic in `10i64.pow`.
21    pub fn as_plain(&self) -> BigInt {
22        if self.scale >= 0 {
23            // dividing by 10^scale; use checked_pow on a u32 exponent so an
24            // out-of-range value yields a clean zero rather than panicking.
25            let exponent = self.scale as u32;
26            match 10i64.checked_pow(exponent) {
27                Some(divisor) => self.unscaled.clone() / divisor,
28                None => BigInt::from(0),
29            }
30        } else {
31            // negative scale means the unscaled value should be multiplied
32            // by 10^|scale| to recover the represented integer
33            let exponent = self.scale.unsigned_abs();
34            self.unscaled.clone() * BigInt::from(10).pow(exponent)
35        }
36    }
37}
38
39impl Serialize for Decimal {
40    fn serialize(&self, cursor: &mut Cursor<&mut Vec<u8>>, version: Version) {
41        self.scale.serialize(cursor, version);
42        self.unscaled
43            .to_signed_bytes_be()
44            .serialize(cursor, version);
45    }
46}
47
48macro_rules! impl_from_for_decimal {
49    ($t:ty) => {
50        impl From<$t> for Decimal {
51            fn from(i: $t) -> Self {
52                Decimal {
53                    unscaled: i.into(),
54                    scale: 0,
55                }
56            }
57        }
58    };
59}
60
61impl_from_for_decimal!(i8);
62impl_from_for_decimal!(i16);
63impl_from_for_decimal!(i32);
64impl_from_for_decimal!(i64);
65impl_from_for_decimal!(u8);
66impl_from_for_decimal!(u16);
67
68impl From<f32> for Decimal {
69    fn from(f: f32) -> Decimal {
70        // Cap the loop just below the point where 10i64.pow(scale) overflows
71        // (10^19 > i64::MAX). Without this guard a hostile input could keep
72        // the loop spinning until the pow call panics. In practice f32
73        // precision causes the equality check to succeed long before this
74        // cap, so existing well-formed inputs are unaffected.
75        const MAX_SCALE: u32 = 18;
76        let mut scale: u32 = 0;
77
78        loop {
79            let unscaled = f * (10i64.pow(scale) as f32);
80
81            if float_eq!(unscaled, unscaled.trunc(), abs <= f32::EPSILON) {
82                return Decimal::new((unscaled as i64).into(), scale as i32);
83            }
84
85            if scale >= MAX_SCALE {
86                // best-effort termination: snap to the truncated value at the
87                // current scale rather than looping forever / panicking
88                return Decimal::new((unscaled.trunc() as i64).into(), scale as i32);
89            }
90
91            scale += 1;
92        }
93    }
94}
95
96impl From<f64> for Decimal {
97    fn from(f: f64) -> Decimal {
98        // Same termination guard as the f32 conversion - bounded just below
99        // i64 overflow on 10i64.pow.
100        const MAX_SCALE: u32 = 18;
101        let mut scale: u32 = 0;
102
103        loop {
104            let unscaled = f * (10i64.pow(scale) as f64);
105
106            if float_eq!(unscaled, unscaled.trunc(), abs <= f64::EPSILON) {
107                return Decimal::new((unscaled as i64).into(), scale as i32);
108            }
109
110            if scale >= MAX_SCALE {
111                return Decimal::new((unscaled.trunc() as i64).into(), scale as i32);
112            }
113
114            scale += 1;
115        }
116    }
117}
118
119impl From<Decimal> for BigInt {
120    fn from(value: Decimal) -> Self {
121        value.as_plain()
122    }
123}
124
125#[cfg(test)]
126mod test {
127    use super::*;
128
129    #[test]
130    fn serialize_test() {
131        assert_eq!(
132            Decimal::new(129.into(), 0).serialize_to_vec(Version::V4),
133            vec![0, 0, 0, 0, 0x00, 0x81]
134        );
135
136        assert_eq!(
137            Decimal::new(BigInt::from(-129), 0).serialize_to_vec(Version::V4),
138            vec![0, 0, 0, 0, 0xFF, 0x7F]
139        );
140
141        let expected: Vec<u8> = vec![0, 0, 0, 1, 0x00, 0x81];
142        assert_eq!(
143            Decimal::new(129.into(), 1).serialize_to_vec(Version::V4),
144            expected
145        );
146
147        let expected: Vec<u8> = vec![0, 0, 0, 1, 0xFF, 0x7F];
148        assert_eq!(
149            Decimal::new(BigInt::from(-129), 1).serialize_to_vec(Version::V4),
150            expected
151        );
152    }
153
154    #[test]
155    fn from_f32() {
156        assert_eq!(
157            Decimal::from(12300001_f32),
158            Decimal::new(12300001.into(), 0)
159        );
160        assert_eq!(
161            Decimal::from(1230000.1_f32),
162            Decimal::new(12300001.into(), 1)
163        );
164        assert_eq!(
165            Decimal::from(0.12300001_f32),
166            Decimal::new(12300001.into(), 8)
167        );
168    }
169
170    #[test]
171    fn from_f64() {
172        assert_eq!(
173            Decimal::from(1230000000000001_f64),
174            Decimal::new(1230000000000001i64.into(), 0)
175        );
176        assert_eq!(
177            Decimal::from(123000000000000.1f64),
178            Decimal::new(1230000000000001i64.into(), 1)
179        );
180        assert_eq!(
181            Decimal::from(0.1230000000000001f64),
182            Decimal::new(1230000000000001i64.into(), 16)
183        );
184    }
185
186    // 0.1 is not exactly representable in IEEE-754 float, so the previous
187    // implementation kept doubling `scale` looking for an exact match and
188    // eventually panicked on `10i64.pow(scale)` overflow when scale exceeded
189    // the number of significant digits. The conversion must terminate without
190    // panicking and produce a sensible Decimal.
191    #[test]
192    fn from_f32_tolerates_inexact_floats() {
193        let _decimal = Decimal::from(0.1f32);
194        let _decimal = Decimal::from(0.2f32);
195        let _decimal = Decimal::from(1.0f32 / 3.0f32);
196    }
197
198    #[test]
199    fn from_f64_tolerates_inexact_floats() {
200        let _decimal = Decimal::from(0.1f64);
201        let _decimal = Decimal::from(0.2f64);
202        let _decimal = Decimal::from(1.0f64 / 3.0f64);
203    }
204
205    // as_plain divides by 10^scale; if scale is negative the previous
206    // `scale as u32` cast wrapped to a huge value and `10i64.pow` panicked.
207    // The function should either reject negative scales or handle them.
208    #[test]
209    fn as_plain_does_not_panic_on_negative_scale() {
210        let decimal = Decimal::new(5.into(), -3);
211        // Just verify it does not panic; we don't assert a specific value
212        // here because the semantics of negative scale aren't part of this
213        // bug fix - we only need to be safe.
214        let _ = decimal.as_plain();
215    }
216}