Skip to main content

mf2_runtime/number/
decimal.rs

1//! The digit backend of the numeric semantics — the seam of owner decision 1.
2//!
3//! The default is this crate's own buffer ([`own`]): an exact decimal of up
4//! to [`INPUT_DIGITS`] significant digits, stored inline — `Copy`, no
5//! allocation, no panic path. Feature `fixed-decimal` swaps in `fixed_decimal`
6//! behind the same interface; it exists only as the A/B baseline.
7//!
8//! Magnitudes are powers of ten: the digit at magnitude `m` is worth `d ×
9//! 10^m`. A value is canonical: no leading or trailing zero digit is stored,
10//! so `low`/`high` are the least and most significant *nonzero* magnitudes.
11
12#[cfg(not(feature = "fixed-decimal"))]
13mod own;
14#[cfg(not(feature = "fixed-decimal"))]
15pub(crate) use own::Decimal;
16
17#[cfg(feature = "fixed-decimal")]
18mod fixed;
19#[cfg(feature = "fixed-decimal")]
20pub(crate) use fixed::Decimal;
21
22/// The most significant digits an operand may have (an implementation limit,
23/// `number.md`: past it, Unsupported Operation). i64 needs 19, f64 17.
24pub(crate) const INPUT_DIGITS: usize = 40;
25
26/// The largest exponent a `number-literal` may carry (an implementation
27/// limit; it bounds the plain output of a value to about 10,000 digits).
28pub(crate) const MAX_EXPONENT: i32 = 9999;
29
30/// The largest magnitude a value may reach (exponent + digits + slack).
31pub(crate) const MAX_MAGNITUDE: i32 = MAX_EXPONENT + 2 * 64;
32
33/// Why a `number-literal` did not become a [`Decimal`].
34#[derive(Clone, Copy, PartialEq, Eq, Debug)]
35pub(crate) enum ParseError {
36    /// Not `number-literal`: Bad Operand.
37    Syntax,
38    /// Valid, but past [`INPUT_DIGITS`] or [`MAX_EXPONENT`]: Unsupported
39    /// Operation.
40    Limit,
41}
42
43/// `roundingMode` (ECMA-402's names and meanings).
44#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
45#[non_exhaustive]
46pub enum RoundingMode {
47    /// `ceil`: towards +∞.
48    Ceil,
49    /// `floor`: towards −∞.
50    Floor,
51    /// `expand`: away from 0.
52    Expand,
53    /// `trunc`: towards 0.
54    Trunc,
55    /// `halfCeil`: to the nearest, ties towards +∞.
56    HalfCeil,
57    /// `halfFloor`: to the nearest, ties towards −∞.
58    HalfFloor,
59    /// `halfExpand` (the default): to the nearest, ties away from 0.
60    HalfExpand,
61    /// `halfTrunc`: to the nearest, ties towards 0.
62    HalfTrunc,
63    /// `halfEven`: to the nearest, ties to even.
64    HalfEven,
65}
66
67/// The mantissa of a rounding increment: every `roundingIncrement` value is
68/// `1`, `2`, `5` or `25` times a power of ten.
69#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
70pub(crate) enum Increment {
71    One,
72    Two,
73    Five,
74    TwentyFive,
75}
76
77impl Increment {
78    #[cfg_attr(feature = "fixed-decimal", allow(dead_code))]
79    pub(crate) const fn value(self) -> u8 {
80        match self {
81            Increment::One => 1,
82            Increment::Two => 2,
83            Increment::Five => 5,
84            Increment::TwentyFive => 25,
85        }
86    }
87}
88
89/// A `number-literal`, split: `["-"] int ["." frac] [e exp]`.
90pub(crate) struct Literal<'s> {
91    pub(crate) neg: bool,
92    /// The integer digits (ASCII), never empty.
93    pub(crate) int: &'s [u8],
94    /// The fraction digits (ASCII), possibly empty.
95    pub(crate) frac: &'s [u8],
96    pub(crate) exp: i32,
97}
98
99/// Checks `number-literal` (`number.md`, "Numeric Operands") and splits it.
100pub(crate) fn split_literal(b: &[u8]) -> Result<Literal<'_>, ParseError> {
101    let neg = b.first() == Some(&b'-');
102    let rest = if neg { b.get(1..).unwrap_or(&[]) } else { b };
103    let mut i = 0;
104    match rest.first() {
105        Some(b'0') => i = 1,
106        Some(b'1'..=b'9') => {
107            while rest.get(i).is_some_and(u8::is_ascii_digit) {
108                i += 1;
109            }
110        }
111        _ => return Err(ParseError::Syntax),
112    }
113    let int = rest.get(..i).unwrap_or(&[]);
114    let mut frac: &[u8] = &[];
115    if rest.get(i) == Some(&b'.') {
116        i += 1;
117        let start = i;
118        while rest.get(i).is_some_and(u8::is_ascii_digit) {
119            i += 1;
120        }
121        if i == start {
122            return Err(ParseError::Syntax);
123        }
124        frac = rest.get(start..i).unwrap_or(&[]);
125    }
126    let mut exp: i32 = 0;
127    let mut limit = false;
128    if matches!(rest.get(i), Some(b'e' | b'E')) {
129        i += 1;
130        let neg_exp = match rest.get(i) {
131            Some(b'-') => {
132                i += 1;
133                true
134            }
135            Some(b'+') => {
136                i += 1;
137                false
138            }
139            _ => false,
140        };
141        let start = i;
142        while let Some(&d) = rest.get(i).filter(|d| d.is_ascii_digit()) {
143            if !limit {
144                exp = exp * 10 + i32::from(d - b'0');
145                limit = exp > MAX_EXPONENT;
146            }
147            i += 1;
148        }
149        if i == start {
150            return Err(ParseError::Syntax);
151        }
152        if neg_exp {
153            exp = -exp;
154        }
155    }
156    if i != rest.len() {
157        return Err(ParseError::Syntax);
158    }
159    if limit {
160        return Err(ParseError::Limit);
161    }
162    Ok(Literal {
163        neg,
164        int,
165        frac,
166        exp,
167    })
168}
169
170#[cfg(test)]
171#[allow(clippy::panic)]
172mod tests {
173    use super::{Decimal, Increment, ParseError, RoundingMode};
174    use alloc::string::String;
175
176    fn plain(d: &Decimal) -> String {
177        let mut s = String::new();
178        d.write_plain(&mut s);
179        s
180    }
181
182    fn p(s: &str) -> Decimal {
183        match Decimal::parse(s.as_bytes()) {
184            Ok(d) => d,
185            Err(e) => panic!("{s}: {e:?}"),
186        }
187    }
188
189    #[test]
190    fn parse_and_print() {
191        for (src, want) in [
192            ("0", "0"),
193            ("-0", "-0"),
194            ("4.2", "4.2"),
195            ("-4.20", "-4.2"),
196            ("0.42e+1", "4.2"),
197            ("1E3", "1000"),
198            ("1e-2", "0.01"),
199            ("10", "10"),
200            ("0.000", "0"),
201            ("120.0500", "120.05"),
202            ("1e21", "1000000000000000000000"),
203            ("1.5e-7", "0.00000015"),
204        ] {
205            assert_eq!(plain(&p(src)), want, "{src}");
206        }
207        for bad in [
208            "00", "042", "1.", "1e", "1E", "1.e", "1.2e", "1.e3", "1e+", "1e-", "1.0e2.0", "foo",
209            ".1", "+1", "0x1", "", "-", "1 ", " 1",
210        ] {
211            assert_eq!(
212                Decimal::parse(bad.as_bytes()).err(),
213                Some(ParseError::Syntax),
214                "{bad}"
215            );
216        }
217        assert_eq!(Decimal::parse(b"1e10000").err(), Some(ParseError::Limit));
218        assert_eq!(plain(&p("1e0000000009")), "1000000000");
219        let long = "1234567890123456789012345678901234567890";
220        assert_eq!(plain(&p(long)), long);
221        let longer = "12345678901234567890123456789012345678901";
222        assert_eq!(
223            Decimal::parse(longer.as_bytes()).err(),
224            Some(ParseError::Limit)
225        );
226        // Leading and trailing zeros do not count.
227        assert_eq!(
228            plain(&p(
229                "0.000000000000000000001234567890123456789012345678901234567890000"
230            )),
231            "0.00000000000000000000123456789012345678901234567890123456789"
232        );
233    }
234
235    fn rounded(s: &str, pos: i16, mode: RoundingMode, inc: Increment) -> String {
236        let mut d = p(s);
237        d.round(pos, mode, inc);
238        plain(&d)
239    }
240
241    #[test]
242    fn rounding_modes() {
243        use RoundingMode::*;
244        let one = Increment::One;
245        // ECMA-402's table for roundingMode, at 0 fraction digits.
246        let table: &[(&str, [&str; 9])] = &[
247            // value   ceil  floor expand trunc hCeil hFloor hExpand hTrunc hEven
248            (
249                "-1.5",
250                ["-1", "-2", "-2", "-1", "-1", "-2", "-2", "-1", "-2"],
251            ),
252            (
253                "-0.5",
254                ["-0", "-1", "-1", "-0", "-0", "-1", "-1", "-0", "-0"],
255            ),
256            ("0.4", ["1", "0", "1", "0", "0", "0", "0", "0", "0"]),
257            ("0.5", ["1", "0", "1", "0", "1", "0", "1", "0", "0"]),
258            ("0.6", ["1", "0", "1", "0", "1", "1", "1", "1", "1"]),
259            ("1.5", ["2", "1", "2", "1", "2", "1", "2", "1", "2"]),
260            ("2.5", ["3", "2", "3", "2", "3", "2", "3", "2", "2"]),
261        ];
262        let modes = [
263            Ceil, Floor, Expand, Trunc, HalfCeil, HalfFloor, HalfExpand, HalfTrunc, HalfEven,
264        ];
265        for (v, wants) in table {
266            for (m, want) in modes.iter().zip(wants) {
267                assert_eq!(rounded(v, 0, *m, one), *want, "{v} {m:?}");
268            }
269        }
270        assert_eq!(rounded("9.995", -2, HalfExpand, one), "10");
271        assert_eq!(rounded("999.5", 0, HalfExpand, one), "1000");
272        assert_eq!(rounded("0.004", 0, Ceil, one), "1");
273        assert_eq!(rounded("0.004", 0, HalfExpand, one), "0");
274        assert_eq!(rounded("1234", 2, HalfExpand, one), "1200");
275        assert_eq!(rounded("1.25000000000000000001", -1, HalfEven, one), "1.3");
276        assert_eq!(rounded("1.25", -1, HalfEven, one), "1.2");
277        assert_eq!(rounded("1.35", -1, HalfEven, one), "1.4");
278    }
279
280    #[test]
281    fn increments() {
282        use RoundingMode::*;
283        // Multiples of 0.05, 0.25, 2, …
284        assert_eq!(rounded("1.23", -2, HalfExpand, Increment::Five), "1.25");
285        assert_eq!(rounded("1.22", -2, HalfExpand, Increment::Five), "1.2");
286        assert_eq!(rounded("1.225", -2, HalfExpand, Increment::Five), "1.25");
287        assert_eq!(rounded("1.225", -2, HalfEven, Increment::Five), "1.2");
288        assert_eq!(rounded("1.275", -2, HalfEven, Increment::Five), "1.3");
289        assert_eq!(rounded("1.3", -1, HalfExpand, Increment::TwentyFive), "2.5");
290        assert_eq!(rounded("1.1", -1, HalfExpand, Increment::TwentyFive), "0");
291        assert_eq!(rounded("1.125", -2, HalfEven, Increment::TwentyFive), "1");
292        assert_eq!(rounded("1.375", -2, HalfEven, Increment::TwentyFive), "1.5");
293        assert_eq!(rounded("3", 0, HalfEven, Increment::Two), "4");
294        assert_eq!(rounded("5", 0, HalfEven, Increment::Two), "4");
295        assert_eq!(rounded("7", 0, HalfEven, Increment::Two), "8");
296        assert_eq!(rounded("7", 0, Trunc, Increment::Two), "6");
297        assert_eq!(rounded("-7", 0, Floor, Increment::Two), "-8");
298        assert_eq!(rounded("-7", 0, Ceil, Increment::Two), "-6");
299        assert_eq!(rounded("99.9", 1, HalfExpand, Increment::Five), "100");
300        assert_eq!(rounded("0.1", 2, Ceil, Increment::TwentyFive), "2500");
301        assert_eq!(rounded("12", -3, HalfExpand, Increment::Five), "12");
302    }
303
304    #[test]
305    fn addition() {
306        let add = |a: &str, b: &str| {
307            let d = p(a).add(&p(b)).map(|d| plain(&d));
308            d.unwrap_or_default()
309        };
310        assert_eq!(add("41", "1"), "42");
311        assert_eq!(add("52", "-10"), "42");
312        assert_eq!(add("1", "-3"), "-2");
313        assert_eq!(add("-1.5", "2"), "0.5");
314        assert_eq!(add("0.25", "-1"), "-0.75");
315        assert_eq!(add("999", "99"), "1098");
316        assert_eq!(add("-0", "0"), "0");
317        assert_eq!(add("1", "-1"), "0");
318        // The own buffer adds exactly up to 40 digits; `fixed_decimal`'s
319        // baseline (P0.5's `add_small`) only through `i64`.
320        #[cfg(not(feature = "fixed-decimal"))]
321        assert_eq!(add("1e30", "1"), "1000000000000000000000000000001");
322        assert!(p("1e45").add(&p("1")).is_none());
323    }
324}