Skip to main content

lexe_common/
decimal.rs

1use rust_decimal::Decimal;
2
3/// A simpler, const-friendly, proc-macro-free `rust_decimal_macros::dec`.
4///
5/// Only handles decimals in the range `-2**63 <= x <= 2**63` to keep the
6/// implementation simpler.
7#[macro_export]
8macro_rules! dec {
9    ($amount:expr) => {
10        const { $crate::decimal::decimal_from_str_const(stringify!($amount)) }
11    };
12}
13
14/// A `const` version of [`Decimal::from_str_radix`] (with radix=10).
15///
16/// Only handles decimals in the range `-2**63 <= x <= 2**63` to keep the
17/// implementation simpler.
18pub const fn decimal_from_str_const(s: &str) -> Decimal {
19    let mut bs = s.as_bytes();
20
21    // check sign
22    let mut negative = false;
23    match bs.split_first() {
24        Some((b, rest)) => match b {
25            b'-' => {
26                negative = true;
27                bs = rest;
28            }
29            b'+' => bs = rest,
30            _ => {}
31        },
32        None => panic!("empty"),
33    }
34
35    // parse the actual number, keeping track of the decimal position and
36    // skipping any "_" characters.
37    let mut num_digits: u8 = 0;
38    let mut idx_point: Option<u8> = None;
39    let mut accum: u64 = 0;
40    while let [b, rest @ ..] = bs {
41        bs = rest;
42        match *b {
43            b'0'..=b'9' => {
44                let d = (*b - b'0') as u64;
45                accum = accum.checked_mul(10).expect("overflow");
46                accum = accum.checked_add(d).expect("overflow");
47                num_digits += 1;
48            }
49            b'.' => {
50                if idx_point.is_some() {
51                    panic!("duplicate decimal point");
52                }
53                idx_point = Some(num_digits);
54            }
55            b'_' => continue,
56            _ => panic!("not a valid decimal"),
57        }
58    }
59
60    // probably an error
61    if num_digits == 0 {
62        panic!("no digits");
63    }
64
65    let lo = (accum & 0xffff_ffff) as u32;
66    let mid = ((accum >> 32) & 0xffff_ffff) as u32;
67    let hi = 0;
68    let idx_point = match idx_point {
69        Some(x) => x,
70        None => num_digits,
71    };
72    let scale = (num_digits - idx_point) as u32;
73
74    Decimal::from_parts(lo, mid, hi, negative, scale)
75}
76
77#[cfg(test)]
78mod test {
79    use proptest::proptest;
80
81    use super::*;
82
83    const MYCONST: Decimal = dec!(132.456);
84
85    #[test]
86    fn test_decimal_from_str_const() {
87        #[track_caller]
88        fn ok(s: &str) {
89            let actual = decimal_from_str_const(s);
90            let expected = Decimal::from_str_radix(s, 10).unwrap();
91            assert_eq!(actual, expected);
92        }
93
94        ok("1");
95        ok("1.");
96        ok(".1");
97        ok("-1");
98        ok("0.1");
99        ok("-0.1");
100        ok("2.0");
101        ok("-0");
102
103        ok("9223372036854775808");
104        ok("-9223372036854775808");
105
106        ok("9.223372036854775808");
107        ok("922337203685477580.8");
108        ok("9223372036854775808.");
109
110        assert_eq!(MYCONST, Decimal::from_parts(132456, 0, 0, false, 3));
111
112        proptest!(|(x: u64, negative: bool, scale in 0u32..20)| {
113            // sample only x < 2**63
114            let x = x & 0x7fff_ffff_ffff_ffff;
115            let lo = (x & 0xffff_ffff) as u32;
116            let mid = ((x >> 32) & 0xffff_ffff) as u32;
117            let hi = 0;
118            let dec = Decimal::from_parts(lo, mid, hi, negative, scale);
119            ok(&dec.to_string());
120        })
121    }
122}