1use std::fmt::Write as _;
20
21use tabnas_alchemy::shared::{Code, Fail};
22
23pub fn is_json_number(text: &str) -> bool {
27 let b = text.as_bytes();
28 let mut i = 0;
29 if b.first() == Some(&b'-') {
30 i += 1;
31 }
32 match b.get(i) {
33 Some(b'0') => i += 1,
34 Some(b'1'..=b'9') => {
35 i += 1;
36 while matches!(b.get(i), Some(b'0'..=b'9')) {
37 i += 1;
38 }
39 }
40 _ => return false,
41 }
42 if b.get(i) == Some(&b'.') {
43 i += 1;
44 let start = i;
45 while matches!(b.get(i), Some(b'0'..=b'9')) {
46 i += 1;
47 }
48 if i == start {
49 return false;
50 }
51 }
52 if matches!(b.get(i), Some(b'e' | b'E')) {
53 i += 1;
54 if matches!(b.get(i), Some(b'+' | b'-')) {
55 i += 1;
56 }
57 let start = i;
58 while matches!(b.get(i), Some(b'0'..=b'9')) {
59 i += 1;
60 }
61 if i == start {
62 return false;
63 }
64 }
65 i == b.len()
66}
67
68pub(crate) fn check_number(value: f64, lexeme: Option<&str>) -> Result<(), Fail> {
77 if let Some(l) = lexeme {
78 if !is_json_number(l) {
79 return Err(Fail::new(
80 Code::InvalidNumber,
81 format!("{l:?} is not a JSON number"),
82 ));
83 }
84 }
85 if !value.is_finite() {
86 let message = match lexeme {
87 Some(l) => format!("{l:?} is {value} as a number, which has no representation"),
88 None => format!("{value} has no representation as a number"),
89 };
90 return Err(Fail::new(Code::TargetValueUnrepresentable, message));
91 }
92 Ok(())
93}
94
95const POSITIONAL_MIN: f64 = 1e-6;
101const POSITIONAL_MAX: f64 = 1e21;
102
103pub(crate) fn write_value(value: f64, out: &mut String) -> &str {
115 out.clear();
116 let magnitude = value.abs();
117 let _ = if magnitude == 0.0 || (POSITIONAL_MIN..POSITIONAL_MAX).contains(&magnitude) {
119 write!(out, "{value}")
120 } else {
121 write!(out, "{value:e}")
122 };
123 out
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129
130 #[test]
131 fn the_json_number_grammar_is_exact() {
132 for ok in [
133 "0",
134 "-0",
135 "1",
136 "-1",
137 "10",
138 "1.5",
139 "0.0",
140 "1e5",
141 "1E5",
142 "1e+5",
143 "1e-5",
144 "1.5e10",
145 "123456789012345678901234567890",
146 "-0.000001",
147 ] {
148 assert!(is_json_number(ok), "{ok:?} should be a number");
149 }
150 for bad in [
151 "",
152 "-",
153 "+1",
154 "01",
155 "1.",
156 ".5",
157 "1e",
158 "1e+",
159 "1.e5",
160 "0x10",
161 "NaN",
162 "Infinity",
163 "-Infinity",
164 " 1",
165 "1 ",
166 "1_000",
167 "1,5",
168 "١",
169 ] {
170 assert!(!is_json_number(bad), "{bad:?} should not be a number");
171 }
172 }
173
174 #[test]
175 fn a_json_number_lexeme_beside_a_finite_value_passes() {
176 assert_eq!(check_number(1.0, Some("1.00")), Ok(()));
177 assert_eq!(check_number(1.0, None), Ok(()));
178 assert_eq!(
179 check_number(1e30, Some("123456789012345678901234567890")),
180 Ok(())
181 );
182 }
183
184 #[test]
185 fn a_lexeme_that_is_not_a_json_number_is_invalid_number() {
186 assert_eq!(
187 check_number(1.0, Some("1.")).unwrap_err().code,
188 Code::InvalidNumber
189 );
190 assert_eq!(
193 check_number(f64::NAN, Some("NaN")).unwrap_err().code,
194 Code::InvalidNumber
195 );
196 }
197
198 #[test]
199 fn a_non_finite_value_is_unrepresentable_with_or_without_a_lexeme() {
200 for v in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
201 assert_eq!(
202 check_number(v, None).unwrap_err().code,
203 Code::TargetValueUnrepresentable
204 );
205 }
206 let err = check_number(f64::INFINITY, Some("1e999")).unwrap_err();
207 assert_eq!(err.code, Code::TargetValueUnrepresentable);
208 assert!(err.message.contains("\"1e999\""), "{}", err.message);
209 assert_eq!(
210 check_number(f64::NEG_INFINITY, Some("-1e999"))
211 .unwrap_err()
212 .code,
213 Code::TargetValueUnrepresentable
214 );
215 }
216
217 fn text(value: f64) -> String {
218 let mut scratch = String::new();
219 write_value(value, &mut scratch).to_owned()
220 }
221
222 #[test]
223 fn a_value_takes_the_shortest_form_positional_within_the_javascript_range() {
224 assert_eq!(text(1.0), "1");
225 assert_eq!(text(0.1), "0.1");
226 assert_eq!(text(-0.0), "-0");
227 assert_eq!(text(0.0), "0");
228 assert_eq!(text(50.25), "50.25");
229 assert_eq!(text(123456.789), "123456.789");
230 assert_eq!(text(1e20), "100000000000000000000");
231 assert_eq!(text(1.5e17), "150000000000000000");
232 assert_eq!(text(1.23456789e18), "1234567890000000000");
233 assert_eq!(text(0.000001), "0.000001");
234 assert_eq!(text(-0.000025), "-0.000025");
235 }
236
237 #[test]
238 fn a_value_outside_the_positional_range_takes_the_exponent_form() {
239 assert_eq!(text(1e21), "1e21");
240 assert_eq!(text(1e300), "1e300");
241 assert_eq!(text(1e-300), "1e-300");
242 assert_eq!(text(1e-7), "1e-7");
243 assert_eq!(text(-2.5e-8), "-2.5e-8");
244 assert_eq!(text(5e-324), "5e-324");
245 assert_eq!(text(f64::MAX), "1.7976931348623157e308");
246 assert_eq!(text(-1.5e300), "-1.5e300");
247 }
248
249 #[test]
250 fn every_form_is_a_json_number_that_reads_back_as_the_same_value() {
251 let values = [
252 0.0,
253 -0.0,
254 1.0,
255 0.1,
256 1e20,
257 1e21,
258 1e300,
259 1e-300,
260 1e-6,
261 1e-7,
262 5e-324,
263 f64::MAX,
264 f64::MIN,
265 f64::MIN_POSITIVE,
266 123456.789,
267 2f64.powi(53),
268 9.999999999999999e20,
269 ];
270 let mut scratch = String::new();
271 for v in values {
272 let t = write_value(v, &mut scratch);
273 assert!(is_json_number(t), "{v:e} wrote {t:?}");
274 let back: f64 = t.parse().unwrap_or(f64::NAN);
275 assert_eq!(back.to_bits(), v.to_bits(), "{v:e} wrote {t:?}");
276 }
277 }
278
279 #[test]
280 fn the_reviewers_probe_is_bytes_not_hundreds_of_digits() {
281 let total: usize = [1e300, 1e-300, 1.5e17, 1.23456789e18]
282 .iter()
283 .map(|&v| text(v).len())
284 .sum();
285 assert_eq!(total, 5 + 6 + 18 + 19);
286 }
287}