Skip to main content

deser_core/ext/
number.rs

1use alloc::borrow::Cow;
2use alloc::format;
3use alloc::string::ToString;
4use core::fmt;
5
6use crate::State;
7use crate::de::{Deserialize, Sink, SinkHandle};
8use crate::error::Error;
9use crate::event::Atom;
10use crate::ext::known::invalid;
11use crate::ext::{BorrowedExtension, ExtValue};
12use crate::ser::{Chunk, Serialize};
13
14/// A number literal from a text format.
15///
16/// This is a well-known borrowing extension (see [`ext`](crate::ext)) for
17/// numbers that text formats parsed as floats (or integers too large for
18/// the data model).  It holds the text of the literal in the syntax of JSON
19/// numbers together with its value as `f64`.  The fallback is the `f64`, so
20/// consumers that do not know about exact numbers are not affected.
21/// Consumers that need the exact value (like [`Decimal`](crate::ext::Decimal)
22/// and [`BigInt`](crate::ext::BigInt) and the types of `rust_decimal`,
23/// `bigdecimal` and `num-bigint`) use the text.
24///
25/// ```
26/// use deser::ext::{Decimal, Number};
27///
28/// let number = Number::parse("0.10").unwrap();
29/// assert_eq!(number.as_str(), "0.10");
30/// assert_eq!(number.value(), 0.1);
31/// ```
32///
33/// The text borrows from the input for formats that support it, extension
34/// values of it are created with
35/// [`ExtValue::borrowed_value::<Number>`](ExtValue::borrowed_value) and
36/// looked up with
37/// [`ExtValue::downcast_value_ref::<Number>`](ExtValue::downcast_value_ref).
38#[derive(Clone, PartialEq)]
39pub struct Number<'a> {
40    text: Cow<'a, str>,
41    value: f64,
42}
43
44impl<'a> Number<'a> {
45    /// Creates a number from its text and value.
46    ///
47    /// The text has to follow the syntax of JSON numbers and the value has
48    /// to be the (possibly approximated) value of the text.  This is not
49    /// checked in release builds.  Use [`parse`](Self::parse) to validate the
50    /// text and compute the value.
51    pub fn new<T: Into<Cow<'a, str>>>(text: T, value: f64) -> Number<'a> {
52        let text = text.into();
53        debug_assert!(is_json_number(&text), "invalid number text {:?}", text);
54        Number { text, value }
55    }
56
57    /// Parses a number from its text.
58    ///
59    /// The text has to follow the syntax of JSON numbers.
60    pub fn parse<T: Into<Cow<'a, str>>>(text: T) -> Result<Number<'a>, Error> {
61        let text = text.into();
62        if !is_json_number(&text) {
63            return Err(invalid("invalid number"));
64        }
65        let value = text.parse().map_err(|_| invalid("invalid number"))?;
66        Ok(Number { text, value })
67    }
68
69    /// Returns the text of the number.
70    pub fn as_str(&self) -> &str {
71        &self.text
72    }
73
74    /// Returns the value of the number as `f64`.
75    ///
76    /// This might be an approximation of the text.
77    pub fn value(&self) -> f64 {
78        self.value
79    }
80
81    /// Returns `true` if the text is an integer (no fraction or exponent).
82    pub fn is_integer(&self) -> bool {
83        !self.text.contains(['.', 'e', 'E'])
84    }
85
86    /// Detaches the number from the data it borrows.
87    pub fn into_static(self) -> Number<'static> {
88        Number {
89            text: Cow::Owned(self.text.into_owned()),
90            value: self.value,
91        }
92    }
93}
94
95/// Checks the syntax of JSON numbers.
96pub(crate) fn is_json_number(s: &str) -> bool {
97    let bytes = s.as_bytes();
98    let mut pos = 0;
99    let digits = |pos: &mut usize| {
100        let start = *pos;
101        while bytes.get(*pos).is_some_and(u8::is_ascii_digit) {
102            *pos += 1;
103        }
104        *pos - start
105    };
106    if bytes.first() == Some(&b'-') {
107        pos += 1;
108    }
109    let int_start = pos;
110    let int_len = digits(&mut pos);
111    if int_len == 0 || (int_len > 1 && bytes[int_start] == b'0') {
112        return false;
113    }
114    if bytes.get(pos) == Some(&b'.') {
115        pos += 1;
116        if digits(&mut pos) == 0 {
117            return false;
118        }
119    }
120    if let Some(b'e' | b'E') = bytes.get(pos) {
121        pos += 1;
122        if let Some(b'+' | b'-') = bytes.get(pos) {
123            pos += 1;
124        }
125        if digits(&mut pos) == 0 {
126            return false;
127        }
128    }
129    pos == bytes.len()
130}
131
132impl<'a> fmt::Display for Number<'a> {
133    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
134        f.write_str(&self.text)
135    }
136}
137
138impl<'a> fmt::Debug for Number<'a> {
139    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
140        write!(f, "Number({})", self.text)
141    }
142}
143
144impl BorrowedExtension for Number<'static> {
145    type Value<'a> = Number<'a>;
146
147    fn name<'v>(_value: &'v Number<'_>) -> &'v str {
148        "number"
149    }
150
151    fn fallback<'v>(value: &'v Number<'_>) -> Atom<'v> {
152        Atom::F64(value.value)
153    }
154
155    fn to_static(value: &Number<'_>) -> Number<'static> {
156        value.clone().into_static()
157    }
158
159    fn shorten<'s, 'l: 's>(value: &'s Number<'l>) -> &'s Number<'s> {
160        value
161    }
162}
163
164impl<'a> Serialize for Number<'a> {
165    fn serialize(&self, _state: &mut State) -> Result<Chunk<'_>, Error> {
166        Ok(Chunk::Atom(Atom::Ext(ExtValue::borrowed_value::<Number>(
167            self,
168        ))))
169    }
170}
171
172/// Numbers are deserialized from numbers (and number extension values) and
173/// strings with the syntax of JSON numbers.  The text is always owned.
174impl<'de, 'a> Deserialize<'de> for Number<'a> {
175    fn deserialize_into<'out>(
176        out: &'out mut Option<Self>,
177        state: &mut State,
178    ) -> SinkHandle<'out, 'de> {
179        SinkHandle::arena(NumberSink(out), state)
180    }
181}
182
183struct NumberSink<'a, 'n>(&'a mut Option<Number<'n>>);
184
185impl<'a, 'n, 'de> Sink<'de> for NumberSink<'a, 'n> {
186    fn expecting(&self) -> Cow<'_, str> {
187        "number".into()
188    }
189
190    fn atom(&mut self, atom: Atom, state: &mut State) -> Result<(), Error> {
191        let number = match atom {
192            Atom::Ext(ref ext) => match ext.downcast_value_ref::<Number>() {
193                Some(value) => value.clone().into_static(),
194                None => return self.unexpected_atom(atom, state),
195            },
196            Atom::U64(value) => Number::new(value.to_string(), value as f64),
197            Atom::I64(value) => Number::new(value.to_string(), value as f64),
198            Atom::F64(value) if value.is_finite() => Number::new(format!("{:?}", value), value),
199            // the text of an `f32` is shorter, the value is the one of the text
200            Atom::F32(value) if value.is_finite() => Number::parse(format!("{:?}", value))?,
201            Atom::Str(ref value) => Number::parse(value.to_string())?,
202            other => return self.unexpected_atom(other, state),
203        };
204        *self.0 = Some(number);
205        Ok(())
206    }
207}
208
209#[test]
210fn test_number() {
211    let number = Number::parse("-12.50e3").unwrap();
212    assert_eq!(number.as_str(), "-12.50e3");
213    assert_eq!(number.value(), -12500.0);
214    assert!(!number.is_integer());
215    assert!(
216        Number::parse("12345678901234567890123456789")
217            .unwrap()
218            .is_integer()
219    );
220    for invalid in ["", "1.", ".1", "+1", "01", "1e", "NaN", "inf", "0x10"] {
221        assert!(Number::parse(invalid).is_err(), "{}", invalid);
222    }
223
224    let ext = ExtValue::borrowed_value::<Number>(&number);
225    assert_eq!(ext.fallback(), Atom::F64(-12500.0));
226    assert_eq!(ext.name(), "number");
227    assert_eq!(
228        ext.downcast_value_ref::<Number>().unwrap().as_str(),
229        "-12.50e3"
230    );
231}