Skip to main content

deser_core/ext/
decimal.rs

1use alloc::format;
2use alloc::string::String;
3use alloc::string::ToString;
4use core::fmt;
5use core::str::FromStr;
6
7use crate::error::Error;
8use crate::event::Atom;
9use crate::ext::known::{WellKnown, impl_well_known, invalid};
10use crate::ext::{BigInt, Extension, Number};
11
12/// An exact decimal number of arbitrary precision.
13///
14/// This is a well-known extension type (see [`ext`](crate::ext)) which
15/// holds the decimal as validated text in the syntax of JSON numbers
16/// (`-12.50`, `1e-7`).  The text is kept as is, so trailing zeros (the
17/// scale) are retained.  The fallback is the text as string.
18///
19/// When deserializing, decimals, strings, integers and floats are accepted.
20/// [`Number`] extension values (which text formats like JSON emit for
21/// floats) are converted exactly.
22///
23/// ```
24/// use deser::ext::Decimal;
25///
26/// let value: Decimal = "-12.50".parse().unwrap();
27/// assert_eq!(value.as_str(), "-12.50");
28/// let (mantissa, exponent) = value.to_parts();
29/// assert_eq!((mantissa.to_string(), exponent), ("-1250".to_string(), -2));
30/// assert_eq!(Decimal::from_parts(&mantissa, exponent), value);
31/// ```
32///
33/// With the `rust_decimal` and `bigdecimal` features the decimal types of
34/// these crates serialize as [`Decimal`].
35#[derive(Clone, PartialEq, Eq, Hash)]
36pub struct Decimal(String);
37
38impl Decimal {
39    /// Creates a decimal from its text representation.
40    ///
41    /// The text has to follow the syntax of JSON numbers.
42    pub fn new<S: Into<String>>(value: S) -> Result<Decimal, Error> {
43        let value = value.into();
44        if is_valid_decimal(&value) {
45            Ok(Decimal(value))
46        } else {
47            Err(invalid("invalid decimal"))
48        }
49    }
50
51    /// Creates a decimal from a mantissa and a base 10 exponent.
52    ///
53    /// The value is `mantissa * 10^exponent`.
54    pub fn from_parts(mantissa: &BigInt, exponent: i64) -> Decimal {
55        let mut digits = mantissa.to_string();
56        let negative = digits.starts_with('-');
57        if negative {
58            digits.remove(0);
59        }
60        let mut rv = String::new();
61        if negative {
62            rv.push('-');
63        }
64        let scale = exponent.unsigned_abs();
65        if exponent < 0 && scale <= digits.len() as u64 + 20 {
66            let scale = scale as usize;
67            if digits.len() > scale {
68                rv.push_str(&digits[..digits.len() - scale]);
69                rv.push('.');
70                rv.push_str(&digits[digits.len() - scale..]);
71            } else {
72                rv.push_str("0.");
73                rv.extend(core::iter::repeat_n('0', scale - digits.len()));
74                rv.push_str(&digits);
75            }
76        } else {
77            rv.push_str(&digits);
78            if exponent != 0 {
79                rv.push('e');
80                rv.push_str(&exponent.to_string());
81            }
82        }
83        Decimal(rv)
84    }
85
86    /// Returns the mantissa and the base 10 exponent.
87    ///
88    /// The value is `mantissa * 10^exponent`.  Exponents that do not fit
89    /// into `i64` saturate.
90    pub fn to_parts(&self) -> (BigInt, i64) {
91        let s = self.0.as_str();
92        let (mantissa, exponent) = match s.find(['e', 'E']) {
93            Some(idx) => (&s[..idx], &s[idx + 1..]),
94            None => (s, "0"),
95        };
96        let exponent: i64 =
97            exponent
98                .trim_start_matches('+')
99                .parse()
100                .unwrap_or(if exponent.starts_with('-') {
101                    i64::MIN
102                } else {
103                    i64::MAX
104                });
105        let (digits, scale) = match mantissa.split_once('.') {
106            Some((int, frac)) => (format!("{}{}", int, frac), frac.len() as i64),
107            None => (mantissa.to_string(), 0),
108        };
109        // the text was validated
110        let mantissa: BigInt = digits.parse().unwrap();
111        (mantissa, exponent.saturating_sub(scale))
112    }
113
114    /// Returns the text of the decimal.
115    pub fn as_str(&self) -> &str {
116        &self.0
117    }
118
119    /// Converts the decimal into its text.
120    pub fn into_string(self) -> String {
121        self.0
122    }
123}
124
125/// Checks the syntax of JSON numbers.
126fn is_valid_decimal(s: &str) -> bool {
127    crate::ext::number::is_json_number(s)
128}
129
130impl fmt::Display for Decimal {
131    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
132        f.write_str(&self.0)
133    }
134}
135
136impl fmt::Debug for Decimal {
137    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
138        write!(f, "Decimal({})", self.0)
139    }
140}
141
142impl FromStr for Decimal {
143    type Err = Error;
144
145    fn from_str(s: &str) -> Result<Decimal, Error> {
146        Decimal::new(s)
147    }
148}
149
150impl From<BigInt> for Decimal {
151    fn from(value: BigInt) -> Decimal {
152        Decimal(value.to_string())
153    }
154}
155
156impl From<i64> for Decimal {
157    fn from(value: i64) -> Decimal {
158        Decimal(value.to_string())
159    }
160}
161
162impl From<u64> for Decimal {
163    fn from(value: u64) -> Decimal {
164        Decimal(value.to_string())
165    }
166}
167
168impl TryFrom<f64> for Decimal {
169    type Error = Error;
170
171    /// Converts a float with the shortest representation that roundtrips.
172    fn try_from(value: f64) -> Result<Decimal, Error> {
173        if value.is_finite() {
174            Decimal::new(format!("{:?}", value))
175        } else {
176            Err(invalid("decimals cannot be infinite or NaN"))
177        }
178    }
179}
180
181impl TryFrom<f32> for Decimal {
182    type Error = Error;
183
184    /// Converts a float with the shortest representation that roundtrips
185    /// through `f32`.
186    fn try_from(value: f32) -> Result<Decimal, Error> {
187        if value.is_finite() {
188            Decimal::new(format!("{:?}", value))
189        } else {
190            Err(invalid("decimals cannot be infinite or NaN"))
191        }
192    }
193}
194
195impl Extension for Decimal {
196    fn name(&self) -> &str {
197        "decimal"
198    }
199
200    fn fallback(&self) -> Atom<'_> {
201        Atom::Str(self.0.as_str().into())
202    }
203}
204
205impl WellKnown for Decimal {
206    const EXPECTING: &'static str = "decimal";
207
208    /// Accepts decimals, strings, integers and floats.
209    fn from_atom(atom: &Atom) -> Result<Option<Decimal>, Error> {
210        Ok(Some(match *atom {
211            Atom::Ext(ref ext) => {
212                if let Some(value) = ext.downcast_ref::<Decimal>() {
213                    value.clone()
214                } else if let Some(value) = ext.downcast_value_ref::<Number>() {
215                    // numbers use the same syntax as decimals
216                    Decimal(value.as_str().to_string())
217                } else if let Some(value) = BigInt::from_atom(atom)? {
218                    Decimal::from(value)
219                } else {
220                    return Ok(None);
221                }
222            }
223            Atom::Str(ref value) => value.parse()?,
224            Atom::U64(value) => Decimal::from(value),
225            Atom::I64(value) => Decimal::from(value),
226            Atom::F64(value) => Decimal::try_from(value)?,
227            Atom::F32(value) => Decimal::try_from(value)?,
228            _ => return Ok(None),
229        }))
230    }
231}
232
233impl_well_known!(Decimal);
234
235#[test]
236fn test_decimal() {
237    for valid in ["0", "-0", "1.5", "-12.50", "1e5", "1E+5", "1.5e-7", "0.001"] {
238        assert_eq!(valid.parse::<Decimal>().unwrap().as_str(), valid);
239    }
240    for invalid in [
241        "", "-", "01", "1.", ".5", "+1", "1e", "1e+", "NaN", "1.5.5", " 1",
242    ] {
243        assert!(invalid.parse::<Decimal>().is_err(), "{}", invalid);
244    }
245    let parts = |s: &str| {
246        let (mantissa, exponent) = s.parse::<Decimal>().unwrap().to_parts();
247        (mantissa.to_string(), exponent)
248    };
249    assert_eq!(parts("12.50"), ("1250".into(), -2));
250    assert_eq!(parts("-0.001"), ("-1".into(), -3));
251    assert_eq!(parts("1.5e10"), ("15".into(), 9));
252    assert_eq!(parts("7"), ("7".into(), 0));
253    let from_parts = |m: i64, e: i64| Decimal::from_parts(&BigInt::from(m), e).into_string();
254    assert_eq!(from_parts(1250, -2), "12.50");
255    assert_eq!(from_parts(-1, -3), "-0.001");
256    assert_eq!(from_parts(15, 9), "15e9");
257    assert_eq!(from_parts(7, 0), "7");
258    assert_eq!(from_parts(1, -40), "1e-40");
259    assert_eq!(Decimal::try_from(0.1).unwrap().as_str(), "0.1");
260    assert_eq!(Decimal::try_from(1e100).unwrap().as_str(), "1e100");
261    assert!(Decimal::try_from(f64::NAN).is_err());
262}