Skip to main content

morphir_core/ir/
decimal.rs

1//! A decimal literal is a genuine decimal.
2//!
3//! The value is a [`BigDecimal`]; the lexeme is the text the literal was written with, which is
4//! what the wire carries (a JSON string) and what a writer emits. `bigdecimal` normalises what it
5//! parses, so the lexeme is the only record of the written scale and spelling: `10.50` and `10.5`
6//! denote the same number and are different literals, which is Java's `BigDecimal.equals` rule
7//! and the rule the kit's byte-wise canonical comparison needs.
8//!
9//! The grammar is the one the v4 schema page states under "Literals"; both Rust models and the
10//! reference apply it before any parser sees the text, so what is refused does not depend on a
11//! library's leniency.
12
13use std::fmt;
14use std::hash::{Hash, Hasher};
15use std::str::FromStr;
16
17use bigdecimal::BigDecimal;
18
19/// A string that is not a decimal lexeme.
20#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
21#[error("{lexeme:?} is not a decimal lexeme")]
22pub struct InvalidDecimalLexeme {
23    lexeme: String,
24}
25
26impl InvalidDecimalLexeme {
27    /// The text that was offered as a decimal literal.
28    pub fn lexeme(&self) -> &str {
29        &self.lexeme
30    }
31}
32
33/// Whether `text` is `[+-]? ( digits ( "." digits? )? | "." digits ) ( [eE] [+-]? digits )?`.
34pub fn is_decimal_lexeme(text: &str) -> bool {
35    let bytes = text.as_bytes();
36    let mut i = 0;
37    if matches!(bytes.first(), Some(b'+' | b'-')) {
38        i += 1;
39    }
40    let integer_start = i;
41    while i < bytes.len() && bytes[i].is_ascii_digit() {
42        i += 1;
43    }
44    let integer_digits = i - integer_start;
45    let mut fraction_digits = 0;
46    if i < bytes.len() && bytes[i] == b'.' {
47        i += 1;
48        let fraction_start = i;
49        while i < bytes.len() && bytes[i].is_ascii_digit() {
50            i += 1;
51        }
52        fraction_digits = i - fraction_start;
53    }
54    if integer_digits == 0 && fraction_digits == 0 {
55        return false;
56    }
57    if i < bytes.len() && matches!(bytes[i], b'e' | b'E') {
58        i += 1;
59        if i < bytes.len() && matches!(bytes[i], b'+' | b'-') {
60            i += 1;
61        }
62        let exponent_start = i;
63        while i < bytes.len() && bytes[i].is_ascii_digit() {
64            i += 1;
65        }
66        if i == exponent_start {
67            return false;
68        }
69    }
70    i == bytes.len()
71}
72
73/// A decimal literal: the number it denotes and the text it was written with.
74#[derive(Debug, Clone)]
75pub struct DecimalLiteral {
76    value: BigDecimal,
77    lexeme: String,
78}
79
80impl DecimalLiteral {
81    /// Read a decimal literal from its lexeme, keeping the lexeme verbatim.
82    pub fn parse(lexeme: &str) -> Result<Self, InvalidDecimalLexeme> {
83        let invalid = || InvalidDecimalLexeme {
84            lexeme: lexeme.to_owned(),
85        };
86        if !is_decimal_lexeme(lexeme) {
87            return Err(invalid());
88        }
89        // The grammar admits `12.` and `.5`; `bigdecimal`'s parser wants a digit on each side of
90        // the point, so the text handed to it is completed with a zero. The lexeme is untouched.
91        let mut completed = lexeme.to_owned();
92        let sign_len = usize::from(completed.starts_with(['+', '-']));
93        if completed[sign_len..].starts_with('.') {
94            completed.insert(sign_len, '0');
95        }
96        if let Some(point) = completed.find('.')
97            && !completed[point + 1..].starts_with(|c: char| c.is_ascii_digit())
98        {
99            completed.insert(point + 1, '0');
100        }
101        let value = BigDecimal::from_str(&completed).map_err(|_| invalid())?;
102        Ok(Self {
103            value,
104            lexeme: lexeme.to_owned(),
105        })
106    }
107
108    /// Build a literal from a value, spelling it in plain positional notation.
109    pub fn from_value(value: BigDecimal) -> Self {
110        let lexeme = value.to_plain_string();
111        Self { value, lexeme }
112    }
113
114    /// The number this literal denotes.
115    pub fn value(&self) -> &BigDecimal {
116        &self.value
117    }
118
119    /// The text this literal was written with, verbatim.
120    pub fn lexeme(&self) -> &str {
121        &self.lexeme
122    }
123}
124
125/// Two decimal literals are the same literal when they were written the same way.
126impl PartialEq for DecimalLiteral {
127    fn eq(&self, other: &Self) -> bool {
128        self.lexeme == other.lexeme
129    }
130}
131
132impl Eq for DecimalLiteral {}
133
134impl Hash for DecimalLiteral {
135    fn hash<H: Hasher>(&self, state: &mut H) {
136        self.lexeme.hash(state);
137    }
138}
139
140impl fmt::Display for DecimalLiteral {
141    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
142        f.write_str(&self.lexeme)
143    }
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149    use std::str::FromStr;
150
151    #[test]
152    fn the_grammar_accepts_what_the_schema_page_names() {
153        for lexeme in [
154            "0", "10.50", "-0.00", "+12.", ".5", "1e-7", "1E+3", "-.5e2", "007",
155        ] {
156            assert!(is_decimal_lexeme(lexeme), "{lexeme}");
157        }
158    }
159
160    #[test]
161    fn the_grammar_refuses_everything_else() {
162        for lexeme in [
163            "",
164            "ten",
165            ".",
166            "+",
167            "1_000",
168            "0x10",
169            "NaN",
170            "Infinity",
171            "-Infinity",
172            "1e",
173            "1e+",
174            " 1",
175            "1 ",
176            "1.5.2",
177            "1,5",
178        ] {
179            assert!(!is_decimal_lexeme(lexeme), "{lexeme}");
180        }
181    }
182
183    #[test]
184    fn a_literal_keeps_its_lexeme_and_carries_the_value() {
185        let literal = DecimalLiteral::parse("10.50").unwrap();
186        assert_eq!(literal.lexeme(), "10.50");
187        assert_eq!(
188            literal.value(),
189            &bigdecimal::BigDecimal::from_str("10.5").unwrap()
190        );
191        for lexeme in ["+12.", ".5", "-.5e2", "1e-7"] {
192            let parsed = DecimalLiteral::parse(lexeme).expect(lexeme);
193            assert_eq!(parsed.lexeme(), lexeme);
194        }
195        assert_eq!(DecimalLiteral::parse("ten").unwrap_err().lexeme(), "ten");
196    }
197
198    #[test]
199    fn identity_is_the_lexeme_not_the_number() {
200        let scaled = DecimalLiteral::parse("10.50").unwrap();
201        let plain = DecimalLiteral::parse("10.5").unwrap();
202        assert_ne!(scaled, plain);
203        assert_eq!(scaled.value(), plain.value());
204        assert_eq!(scaled, DecimalLiteral::parse("10.50").unwrap());
205    }
206
207    #[test]
208    fn from_value_spells_the_plain_form() {
209        let value = bigdecimal::BigDecimal::from_str("1.50").unwrap();
210        assert_eq!(DecimalLiteral::from_value(value).lexeme(), "1.50");
211    }
212}