morphir_core/ir/
decimal.rs1use std::fmt;
14use std::hash::{Hash, Hasher};
15use std::str::FromStr;
16
17use bigdecimal::BigDecimal;
18
19#[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 pub fn lexeme(&self) -> &str {
29 &self.lexeme
30 }
31}
32
33pub 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#[derive(Debug, Clone)]
75pub struct DecimalLiteral {
76 value: BigDecimal,
77 lexeme: String,
78}
79
80impl DecimalLiteral {
81 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 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 pub fn from_value(value: BigDecimal) -> Self {
110 let lexeme = value.to_plain_string();
111 Self { value, lexeme }
112 }
113
114 pub fn value(&self) -> &BigDecimal {
116 &self.value
117 }
118
119 pub fn lexeme(&self) -> &str {
121 &self.lexeme
122 }
123}
124
125impl 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}