Skip to main content

const_decimal/
display.rs

1use std::cmp::Ordering;
2use std::fmt::Display;
3use std::num::ParseIntError;
4use std::str::FromStr;
5
6use thiserror::Error;
7
8use crate::{Decimal, ScaledInteger};
9
10impl<I, const D: u8> Display for Decimal<I, D>
11where
12    I: ScaledInteger<D>,
13{
14    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
15        // `SCALING_FACTOR` cannot be zero.
16        #[allow(clippy::arithmetic_side_effects)]
17        let integer = self.0 / I::SCALING_FACTOR;
18        // `SCALING_FACTOR` cannot be zero.
19        #[allow(clippy::arithmetic_side_effects)]
20        let fractional = self.0 % I::SCALING_FACTOR;
21
22        let is_negative = self.0 < I::ZERO;
23        // The quotient carries the sign unless it is zero.
24        let sign = match is_negative && integer == I::ZERO {
25            true => "-",
26            false => "",
27        };
28        // Only negate the remainder, whose magnitude is less than the positive scaling
29        // factor and therefore always representable.
30        let fractional = match is_negative {
31            true => (!fractional).wrapping_add(&I::ONE),
32            false => fractional,
33        };
34
35        write!(f, "{sign}{integer}.{fractional:0>decimals$}", decimals = D as usize)
36    }
37}
38
39impl<I, const D: u8> FromStr for Decimal<I, D>
40where
41    I: ScaledInteger<D>,
42{
43    type Err = ParseDecimalError<I>;
44
45    fn from_str(s: &str) -> Result<Self, Self::Err> {
46        if s.is_empty() {
47            return Err(ParseDecimalError::EmptyString);
48        }
49
50        // Strip the sign (-0 would parse to 0 and break our output).
51        let unsigned_s = s.strip_prefix('-').unwrap_or(s);
52
53        // Parse the unsigned representation.
54        let Some((integer_s, fractional_s)) = unsigned_s.split_once('.') else {
55            // The number does not contain a decimal point, but we can still try and parse
56            // it as an integer.
57            let integer = I::from_str(s)?;
58
59            return Decimal::try_from_scaled(integer, 0)
60                .ok_or(ParseDecimalError::Overflow(integer, I::ZERO));
61        };
62        let integer = I::from_str(integer_s)?;
63        let fractional = I::from_str(fractional_s)?;
64
65        let scaled_integer = integer
66            .checked_mul(&I::SCALING_FACTOR)
67            .ok_or(ParseDecimalError::Overflow(integer, fractional))?;
68
69        let fractional_s_len = fractional_s.len();
70        let fractional = match fractional_s_len.cmp(&(D as usize)) {
71            Ordering::Equal => fractional,
72            Ordering::Less => {
73                // `fractional_s_len` guaranteed to be less than D.
74                #[allow(clippy::arithmetic_side_effects)]
75                let shortfall = D as usize - fractional_s_len;
76
77                // TODO: Remove the `checked_mul` in favor of ensuring `D` cannot overflow.
78                fractional
79                    .checked_mul(&I::pow(I::TEN, shortfall.try_into().unwrap()))
80                    .unwrap()
81            }
82            Ordering::Greater => return Err(ParseDecimalError::PrecisionLoss(fractional_s.len())),
83        };
84        let unsigned = scaled_integer
85            .checked_add(&fractional)
86            .ok_or(ParseDecimalError::Overflow(integer, fractional))?;
87
88        // Use two's complement to convert to the signed representation.
89        Ok(match unsigned_s.len() == s.len() {
90            true => Decimal(unsigned),
91            false => {
92                debug_assert_eq!(unsigned_s.len().checked_add(1).unwrap(), s.len());
93
94                Decimal((!unsigned).wrapping_add(&I::ONE))
95            }
96        })
97    }
98}
99
100#[derive(Debug, PartialEq, Eq, Error)]
101pub enum ParseDecimalError<I>
102where
103    I: Display,
104{
105    #[error("Empty string provided")]
106    EmptyString,
107    #[error("Missing decimal point")]
108    MissingDecimalPoint,
109    #[error("Resultant decimal overflowed; integer={0}; fractional={1}")]
110    Overflow(I, I),
111    #[error("Failed to parse integer; err={0}")]
112    ParseInt(#[from] ParseIntError),
113    #[error("Could not parse without precision loss; decimals={0}")]
114    PrecisionLoss(usize),
115    #[error("The radix must be 10 in `from_str_radix`")]
116    RadixMustBe10,
117}
118
119#[cfg(test)]
120mod tests {
121    use expect_test::expect;
122    use proptest::prelude::Arbitrary;
123    use proptest::proptest;
124    use proptest::test_runner::TestRunner;
125
126    use super::*;
127    use crate::{Int64_9, Uint64_9};
128
129    #[test]
130    fn uint64_9_to_string() {
131        assert_eq!(Uint64_9::ONE.to_string(), "1.000000000");
132        assert_eq!(Uint64_9::try_from_scaled(123, 9).unwrap().to_string(), "0.000000123");
133        assert_eq!(
134            (Uint64_9::ONE + Uint64_9::try_from_scaled(123, 9).unwrap()).to_string(),
135            "1.000000123"
136        );
137    }
138
139    #[test]
140    fn uint64_9_from_str() {
141        assert_eq!("".parse::<Uint64_9>(), Err(ParseDecimalError::EmptyString));
142        expect![[r#"
143            Err(
144                ParseInt(
145                    ParseIntError {
146                        kind: Empty,
147                    },
148                ),
149            )
150        "#]]
151        .assert_debug_eq(&"1.".parse::<Uint64_9>());
152        assert_eq!("1.0".parse::<Uint64_9>(), Ok(Uint64_9::ONE));
153        assert_eq!("10.0".parse::<Uint64_9>(), Ok(Uint64_9::try_from_scaled(10, 0).unwrap()));
154        assert_eq!("10".parse::<Uint64_9>(), Ok(Uint64_9::try_from_scaled(10, 0).unwrap()));
155        assert_eq!("0.1".parse::<Uint64_9>(), Ok(Decimal(10u64.pow(8))));
156        assert_eq!("0.123456789".parse::<Uint64_9>(), Ok(Decimal(123_456_789)));
157        assert_eq!("0.012345678".parse::<Uint64_9>(), Ok(Decimal(12_345_678)));
158        assert_eq!("0.000000001".parse::<Uint64_9>(), Ok(Decimal(1)));
159
160        assert_eq!("0.0000000001".parse::<Uint64_9>(), Err(ParseDecimalError::PrecisionLoss(10)));
161        assert_eq!(
162            format!("{}.0", u64::MAX).parse::<Uint64_9>(),
163            Err(ParseDecimalError::Overflow(u64::MAX, 0))
164        );
165        assert_eq!(
166            format!("{}.0", u64::MAX / Uint64_9::SCALING_FACTOR).parse::<Uint64_9>(),
167            Ok(Decimal(u64::MAX / Uint64_9::SCALING_FACTOR * Uint64_9::SCALING_FACTOR))
168        );
169        assert_eq!("18446744073.709551615".to_string().parse::<Uint64_9>(), Ok(Decimal::MAX));
170        assert_eq!(
171            "18446744073.709551616".to_string().parse::<Uint64_9>(),
172            Err(ParseDecimalError::Overflow(18_446_744_073, 709_551_616)),
173        );
174    }
175
176    #[test]
177    fn int64_9_to_string() {
178        assert_eq!(Int64_9::ZERO.to_string(), "0.000000000");
179        assert_eq!(Int64_9::ONE.to_string(), "1.000000000");
180        assert_eq!(Int64_9::try_from_scaled(123, 9).unwrap().to_string(), "0.000000123");
181        assert_eq!(
182            (Int64_9::ONE + Int64_9::try_from_scaled(123, 9).unwrap()).to_string(),
183            "1.000000123"
184        );
185        assert_eq!((-Int64_9::ONE).to_string(), "-1.000000000");
186        assert_eq!((-Int64_9::try_from_scaled(123, 9).unwrap()).to_string(), "-0.000000123");
187        assert_eq!(
188            (-Int64_9::ONE + -Int64_9::try_from_scaled(123, 9).unwrap()).to_string(),
189            "-1.000000123"
190        );
191    }
192
193    #[test]
194    fn signed_min_to_string() {
195        assert_eq!(Decimal::<i8, 1>::MIN.to_string(), "-12.8");
196    }
197
198    #[test]
199    fn int64_9_from_str() {
200        assert_eq!("".parse::<Int64_9>(), Err(ParseDecimalError::EmptyString));
201        expect![[r#"
202            Err(
203                ParseInt(
204                    ParseIntError {
205                        kind: Empty,
206                    },
207                ),
208            )
209        "#]]
210        .assert_debug_eq(&"1.".parse::<Int64_9>());
211        assert_eq!("1.0".parse::<Int64_9>(), Ok(Int64_9::ONE));
212        assert_eq!("0.1".parse::<Int64_9>(), Ok(Decimal(10i64.pow(8))));
213        assert_eq!("0.123456789".parse::<Int64_9>(), Ok(Decimal(123_456_789)));
214        assert_eq!("0.012345678".parse::<Int64_9>(), Ok(Decimal(12_345_678)));
215        assert_eq!("0.000000001".parse::<Int64_9>(), Ok(Decimal(1)));
216        assert_eq!("0.0000000001".parse::<Int64_9>(), Err(ParseDecimalError::PrecisionLoss(10)));
217        assert_eq!("-1.0".parse::<Int64_9>(), Ok(-Int64_9::ONE));
218        assert_eq!("-10.0".parse::<Int64_9>(), Ok(Int64_9::try_from_scaled(-10, 0).unwrap()));
219        assert_eq!("-10".parse::<Int64_9>(), Ok(Int64_9::try_from_scaled(-10, 0).unwrap()));
220        assert_eq!("-0.1".parse::<Int64_9>(), Ok(-Decimal(10i64.pow(8))));
221        assert_eq!("-0.123456789".parse::<Int64_9>(), Ok(-Decimal(123_456_789)));
222        assert_eq!("-0.012345678".parse::<Int64_9>(), Ok(-Decimal(12_345_678)));
223        assert_eq!("-0.000000001".parse::<Int64_9>(), Ok(-Decimal(1)));
224        assert_eq!("-0.0000000001".parse::<Int64_9>(), Err(ParseDecimalError::PrecisionLoss(10)));
225    }
226
227    // TODO: Round trip fuzz test does not cover strings with precision greater/less
228    // than target precision.
229
230    #[test]
231    fn uint64_9_round_trip() {
232        decimal_round_trip::<9, u64>();
233    }
234
235    #[test]
236    fn int64_9_round_trip() {
237        decimal_round_trip::<9, i64>();
238    }
239
240    #[test]
241    fn uint128_18_round_trip() {
242        decimal_round_trip::<9, u64>();
243    }
244
245    #[test]
246    fn int128_18_round_trip() {
247        decimal_round_trip::<9, i64>();
248    }
249
250    fn decimal_round_trip<const D: u8, I>()
251    where
252        I: ScaledInteger<D> + Arbitrary,
253    {
254        let mut runner = TestRunner::default();
255        let input = Decimal::arbitrary();
256
257        runner
258            .run(&input, |decimal: Decimal<I, D>| {
259                let round_trip = decimal.to_string().parse().unwrap();
260
261                assert_eq!(decimal, round_trip);
262
263                Ok(())
264            })
265            .unwrap();
266    }
267
268    #[test]
269    fn uint64_9_parse_no_panic() {
270        decimal_parse_no_panic::<9, u64>();
271    }
272
273    #[test]
274    fn int64_9_parse_no_panic() {
275        decimal_parse_no_panic::<9, i64>();
276    }
277
278    #[test]
279    fn uint128_18_parse_no_panic() {
280        decimal_parse_no_panic::<9, u64>();
281    }
282
283    #[test]
284    fn int128_18_parse_no_panic() {
285        decimal_parse_no_panic::<9, i64>();
286    }
287
288    fn decimal_parse_no_panic<const D: u8, I>()
289    where
290        I: ScaledInteger<D>,
291    {
292        proptest!(|(decimal_s: String)| {
293            let _ = decimal_s.parse::<Decimal<I, D>>();
294        });
295    }
296
297    #[test]
298    fn uint64_9_parse_numeric_no_panic() {
299        decimal_parse_numeric_no_panic::<9, u64>();
300    }
301
302    #[test]
303    fn int64_9_parse_numeric_no_panic() {
304        decimal_parse_numeric_no_panic::<9, i64>();
305    }
306
307    #[test]
308    fn uint128_18_parse_numeric_no_panic() {
309        decimal_parse_numeric_no_panic::<9, u64>();
310    }
311
312    #[test]
313    fn int128_18_parse_numeric_no_panic() {
314        decimal_parse_numeric_no_panic::<9, i64>();
315    }
316
317    fn decimal_parse_numeric_no_panic<const D: u8, I>()
318    where
319        I: ScaledInteger<D>,
320    {
321        proptest!(|(decimal_s in "[0-9]{0,24}\\.[0-9]{0,24}")| {
322            let _ = decimal_s.parse::<Decimal<I, D>>();
323        });
324    }
325}