parse_rust_core/js_number.rs
1//! ECMAScript `Number::toString(x, 10)`, which is what `JSON.stringify` uses for numbers.
2//!
3//! This exists because `serde_json` formats `f64` with ryu, producing the shortest
4//! round-tripping decimal, and that is **not** the same string ECMAScript produces. Parse
5//! Server is a Node server, so its number output is ECMAScript's, and wire compatibility
6//! means matching it byte for byte.
7//!
8//! Measured divergences between ryu and `JSON.stringify`, all reproduced in the tests below:
9//!
10//! | Value | `JSON.stringify` | `serde_json` |
11//! |----------|------------------|--------------|
12//! | `100.0` | `100` | `100.0` |
13//! | `1e20` | `100000000000000000000` | `1e20` |
14//! | `1e-6` | `0.000001` | `1e-6` |
15//! | `-0.0` | `0` | `-0.0` |
16//!
17//! Not a divergence, despite looking like one: both emit `+` in a positive exponent, so
18//! `1.5e+300` already matches. Do not "fix" that.
19//!
20//! Spec: ECMA-262 §6.1.6.1.20, `Number::toString`.
21//!
22//! Note the distinction this module does *not* cover: number **representation**, meaning which
23//! BSON type a value is stored as, is a separate problem handled at the storage boundary in
24//! `parse-rust-mongo`. Conflating the two is a mistake this project made once.
25
26use std::fmt::Write as _;
27
28/// Format an `f64` exactly as ECMAScript's `String(x)` / `JSON.stringify(x)` would.
29///
30/// Note `NaN` and the infinities: this returns their ECMAScript *string* forms, which is
31/// correct for `String(x)` but is **not** valid JSON. `JSON.stringify` emits `null` for all
32/// three. Callers serializing to JSON must handle that before calling here; the encoder in
33/// `crate::value` does.
34pub fn to_ecma_string(x: f64) -> String {
35 if x.is_nan() {
36 return "NaN".to_string();
37 }
38 // Step 2: both zeros render as "0". The sign of -0.0 is deliberately dropped.
39 if x == 0.0 {
40 return "0".to_string();
41 }
42 if x < 0.0 {
43 return format!("-{}", to_ecma_string(-x));
44 }
45 if x.is_infinite() {
46 return "Infinity".to_string();
47 }
48
49 let (digits, n) = shortest_digits(x);
50 let k = digits.len() as i32;
51 render(&digits, k, n)
52}
53
54/// Decompose a positive, finite `f64` into its shortest round-tripping decimal digits and the
55/// position of the decimal point.
56///
57/// Returns `(digits, n)` with no trailing zeros, such that `0.<digits> * 10^n == x`. This is
58/// `s` and `n` from the spec, with `k = digits.len()`.
59///
60/// **Uses ryu, not `format!("{:e}")`, and the difference is not cosmetic.** Spec step 5 does not
61/// merely ask for a shortest representation, it asks for a specific one: among candidates of
62/// minimal length, the one closest in value to `x`, breaking a remaining tie toward the even
63/// digit. Rust's `Display`/`LowerExp` guarantee only that the result round-trips, which is a
64/// weaker property, and the two disagree in practice.
65///
66/// Found by the differential test in `tests/js_number_differential.rs`, not by reading:
67/// `f64::from_bits(4829166033435530498)` renders as `726354065216160.3` via `{:e}` and
68/// `726354065216160.2` via ryu and Node. Both strings parse back to the identical bit pattern,
69/// so both are "shortest round-tripping"; only ryu's is the closest to the true value.
70fn shortest_digits(x: f64) -> (String, i32) {
71 let mut buf = ryu::Buffer::new();
72 let s = buf.format_finite(x); // "100.0", "0.1", "1e20", "1.5e300", "726354065216160.2"
73
74 let (mantissa, exp10) = match s.split_once('e') {
75 // Scientific: mantissa is d[.ddd], exponent has no explicit '+'.
76 Some((m, e)) => (m, e.parse::<i32>().unwrap_or(0)),
77 None => (s, 0),
78 };
79
80 let (int_part, frac_part) = mantissa.split_once('.').unwrap_or((mantissa, ""));
81
82 // n counts digits before the decimal point. For a pure fraction ryu emits "0.000ddd", and
83 // each leading zero in the fraction pushes the point one place further right.
84 let (digits, n) = if int_part == "0" {
85 let lead_zeros = frac_part.len() - frac_part.trim_start_matches('0').len();
86 (frac_part[lead_zeros..].to_string(), -(lead_zeros as i32))
87 } else {
88 let mut d = String::with_capacity(int_part.len() + frac_part.len());
89 d.push_str(int_part);
90 d.push_str(frac_part);
91 (d, int_part.len() as i32)
92 };
93
94 // ryu writes "100.0" for an integer, so the combined digits can carry trailing zeros that
95 // inflate k and would push a value into the wrong rendering branch.
96 let trimmed = digits.trim_end_matches('0');
97 let digits = if trimmed.is_empty() {
98 "0".to_string()
99 } else {
100 trimmed.to_string()
101 };
102
103 (digits, n + exp10)
104}
105
106/// Steps 6 through 10 of ECMA-262 §6.1.6.1.20, given the digits, their count `k`, and the
107/// decimal point position `n`.
108fn render(digits: &str, k: i32, n: i32) -> String {
109 // Step 6: k <= n <= 21. Integer, padded with n-k trailing zeros, no fractional part.
110 if k <= n && n <= 21 {
111 let mut s = String::with_capacity(n as usize);
112 s.push_str(digits);
113 for _ in 0..(n - k) {
114 s.push('0');
115 }
116 return s;
117 }
118 // Step 7: 0 < n <= 21. Decimal point falls inside the digits.
119 if 0 < n && n <= 21 {
120 let (int_part, frac_part) = digits.split_at(n as usize);
121 return format!("{int_part}.{frac_part}");
122 }
123 // Step 8: -6 < n <= 0. Leading "0." then -n zeros then the digits.
124 if -6 < n && n <= 0 {
125 let mut s = String::with_capacity((2 - n) as usize + digits.len());
126 s.push_str("0.");
127 for _ in 0..(-n) {
128 s.push('0');
129 }
130 s.push_str(digits);
131 return s;
132 }
133 // Steps 9 and 10: exponential. The exponent is n-1, and its sign is always explicit.
134 let e = n - 1;
135 let sign = if e >= 0 { '+' } else { '-' };
136 let mut s = String::new();
137 if k == 1 {
138 // Step 9: single digit, no decimal point.
139 let _ = write!(s, "{digits}e{sign}{}", e.abs());
140 } else {
141 // Step 10: first digit, point, remainder.
142 let (first, rest) = digits.split_at(1);
143 let _ = write!(s, "{first}.{rest}e{sign}{}", e.abs());
144 }
145 s
146}
147
148/// `String(value)`, the coercion a JavaScript template literal or `+` performs.
149///
150/// Upstream builds several client-visible error messages by concatenating a value into a string,
151/// so the rendering is part of the wire contract rather than a debugging convenience:
152/// `You cannot use ${value} as a query parameter.` (`MongoTransform.js:352`) and
153/// `'This is not a valid ' + obj.__type` (`SchemaController.js`) both reach a client.
154///
155/// The two cases worth naming: an array joins its elements on commas **after** coercing each one,
156/// so nesting flattens and `null` renders as the empty string; and every other object is the
157/// literal `[object Object]`, which is why a malformed operand's message says nothing about it.
158pub fn to_ecma_display(value: &crate::value::ParseValue) -> String {
159 use crate::value::ParseValue;
160 match value {
161 ParseValue::String(s) => s.clone(),
162 ParseValue::Number(n) => to_ecma_string(*n),
163 ParseValue::Bool(b) => b.to_string(),
164 ParseValue::Null => "null".to_string(),
165 ParseValue::Array(items) => items
166 .iter()
167 .map(|item| match item {
168 // `Array.prototype.join` renders null and undefined as empty, which is not what
169 // `String(null)` does. The difference is only visible inside an array.
170 ParseValue::Null => String::new(),
171 other => to_ecma_display(other),
172 })
173 .collect::<Vec<_>>()
174 .join(","),
175 _ => "[object Object]".to_string(),
176 }
177}
178
179/// JavaScript truthiness.
180///
181/// Upstream guards several schema decisions with a bare `if (obj.key)`, which is **not** a presence
182/// test: `""`, `0`, `false` and `NaN` are all present and all falsy, and an empty array or object
183/// is falsy in neither JavaScript nor here. Reading those guards as "is the key set" accepts
184/// metadata upstream refuses, and reading them as "is the key a string" refuses metadata upstream
185/// accepts.
186pub fn is_truthy(value: &crate::value::ParseValue) -> bool {
187 use crate::value::ParseValue;
188 match value {
189 ParseValue::Null => false,
190 ParseValue::Bool(b) => *b,
191 ParseValue::Number(n) => *n != 0.0 && !n.is_nan(),
192 ParseValue::String(s) => !s.is_empty(),
193 // Every object is truthy in JavaScript, an empty array and an empty object included.
194 _ => true,
195 }
196}
197
198#[cfg(test)]
199mod tests {
200 use super::*;
201
202 /// Every expectation here was produced by running the value through Node and pasting the
203 /// output, not by reasoning about the spec. `tests/js_number_differential.rs`, driving `tools/js-number-oracle.js`, re-derives
204 /// them at scale against a live Node.
205 #[test]
206 fn matches_node_on_the_known_divergences() {
207 // The four rules where ryu and ECMAScript disagree.
208 assert_eq!(to_ecma_string(100.0), "100"); // ryu: 100.0
209 assert_eq!(to_ecma_string(3_000_000_000.0), "3000000000"); // ryu: 3000000000.0
210 assert_eq!(to_ecma_string(1e20), "100000000000000000000"); // ryu: 1e20
211 assert_eq!(to_ecma_string(1e-6), "0.000001"); // ryu: 1e-6
212 assert_eq!(to_ecma_string(-0.0), "0"); // ryu: -0.0
213 }
214
215 #[test]
216 fn agrees_with_ryu_where_it_already_agreed() {
217 // Guards against "fixing" the exponent sign, which ryu already gets right.
218 assert_eq!(to_ecma_string(1.5e300), "1.5e+300");
219 assert_eq!(to_ecma_string(1e21), "1e+21");
220 assert_eq!(to_ecma_string(1e-7), "1e-7");
221 assert_eq!(to_ecma_string(0.1), "0.1");
222 assert_eq!(to_ecma_string(5e-324), "5e-324");
223 assert_eq!(to_ecma_string(f64::MAX), "1.7976931348623157e+308");
224 }
225
226 #[test]
227 fn boundaries_are_exact() {
228 // Upper switch to exponential is at 1e21, not before.
229 assert_eq!(to_ecma_string(1e20), "100000000000000000000");
230 assert_eq!(to_ecma_string(1e21), "1e+21");
231 // Lower switch is at 1e-7, not 1e-6.
232 assert_eq!(to_ecma_string(1e-6), "0.000001");
233 assert_eq!(to_ecma_string(1e-7), "1e-7");
234 // 21 significant digits still renders as an integer below 1e21.
235 assert_eq!(
236 to_ecma_string(123456789012345678901.0),
237 "123456789012345680000"
238 );
239 }
240
241 #[test]
242 fn integers_and_fractions() {
243 assert_eq!(to_ecma_string(0.0), "0");
244 assert_eq!(to_ecma_string(1.0), "1");
245 assert_eq!(to_ecma_string(-1.0), "-1");
246 assert_eq!(to_ecma_string(1.5), "1.5");
247 assert_eq!(to_ecma_string(-1.5), "-1.5");
248 assert_eq!(to_ecma_string(9007199254740992.0), "9007199254740992"); // 2^53
249 assert_eq!(to_ecma_string(-1e21), "-1e+21");
250 }
251
252 #[test]
253 fn non_finite_are_ecmascript_strings_not_json() {
254 // Documented above: these are String(x), not JSON. The value encoder maps them to null.
255 assert_eq!(to_ecma_string(f64::NAN), "NaN");
256 assert_eq!(to_ecma_string(f64::INFINITY), "Infinity");
257 assert_eq!(to_ecma_string(f64::NEG_INFINITY), "-Infinity");
258 }
259
260 #[test]
261 fn round_trips_through_parse() {
262 // Whatever we emit must parse back to the same bits, since the digits are shortest
263 // round-tripping by construction. Catches an error in the rendering steps.
264 let vals = [
265 1.0,
266 100.0,
267 0.1,
268 1e20,
269 1e21,
270 1e-6,
271 1e-7,
272 1.5e300,
273 5e-324,
274 f64::MAX,
275 9007199254740992.0,
276 123456789012345678901.0,
277 -42.75,
278 2.2250738585072014e-308,
279 ];
280 for v in vals {
281 let s = to_ecma_string(v);
282 let back: f64 = s
283 .parse()
284 .unwrap_or_else(|e| panic!("{s} did not reparse: {e}"));
285 assert_eq!(back.to_bits(), v.to_bits(), "round trip failed for {s}");
286 }
287 }
288}