1use serde_json::Value;
17
18pub fn python_float_repr(x: f64) -> String {
20 if x.is_nan() {
21 return "NaN".into();
22 }
23 if x.is_infinite() {
24 return if x > 0.0 { "Infinity" } else { "-Infinity" }.into();
25 }
26 if x == 0.0 {
27 return if x.is_sign_negative() { "-0.0" } else { "0.0" }.into();
28 }
29
30 let sci = format!("{x:e}");
33 let (mantissa, exp_str) = sci.split_once('e').expect("LowerExp always emits an e");
34 let exp: i32 = exp_str.parse().expect("valid exponent");
35 let neg = mantissa.starts_with('-');
36 let digits: String = mantissa.chars().filter(char::is_ascii_digit).collect();
37
38 let body = if (-4..16).contains(&exp) {
39 if exp >= 0 {
41 let int_len = (exp + 1) as usize;
42 if digits.len() > int_len {
43 format!("{}.{}", &digits[..int_len], &digits[int_len..])
44 } else {
45 format!("{}{}.0", digits, "0".repeat(int_len - digits.len()))
47 }
48 } else {
49 format!("0.{}{}", "0".repeat((-exp - 1) as usize), digits)
50 }
51 } else {
52 let m = if digits.len() == 1 {
55 digits.clone()
56 } else {
57 format!("{}.{}", &digits[..1], &digits[1..])
58 };
59 let sign = if exp < 0 { '-' } else { '+' };
60 format!("{}e{}{:02}", m, sign, exp.abs())
61 };
62
63 if neg {
64 format!("-{body}")
65 } else {
66 body
67 }
68}
69
70fn escape_string(s: &str, out: &mut String) {
72 out.push('"');
73 for c in s.chars() {
74 match c {
75 '"' => out.push_str("\\\""),
76 '\\' => out.push_str("\\\\"),
77 '\n' => out.push_str("\\n"),
78 '\r' => out.push_str("\\r"),
79 '\t' => out.push_str("\\t"),
80 '\u{8}' => out.push_str("\\b"),
81 '\u{c}' => out.push_str("\\f"),
82 c if (c as u32) < 0x20 => {
83 out.push_str(&format!("\\u{:04x}", c as u32));
84 }
85 c if c.is_ascii() => out.push(c),
86 c => {
87 let code = c as u32;
88 if code <= 0xFFFF {
89 out.push_str(&format!("\\u{code:04x}"));
90 } else {
91 let v = code - 0x10000;
93 out.push_str(&format!(
94 "\\u{:04x}\\u{:04x}",
95 0xD800 + (v >> 10),
96 0xDC00 + (v & 0x3FF)
97 ));
98 }
99 }
100 }
101 }
102 out.push('"');
103}
104
105fn write_value(value: &Value, out: &mut String) {
106 match value {
107 Value::Null => out.push_str("null"),
108 Value::Bool(true) => out.push_str("true"),
109 Value::Bool(false) => out.push_str("false"),
110 Value::Number(n) => {
111 if let Some(i) = n.as_i64() {
112 out.push_str(&i.to_string());
113 } else if let Some(u) = n.as_u64() {
114 out.push_str(&u.to_string());
115 } else {
116 out.push_str(&python_float_repr(n.as_f64().expect("finite float")));
117 }
118 }
119 Value::String(s) => escape_string(s, out),
120 Value::Array(items) => {
121 out.push('[');
122 for (i, item) in items.iter().enumerate() {
123 if i > 0 {
124 out.push(',');
125 }
126 write_value(item, out);
127 }
128 out.push(']');
129 }
130 Value::Object(map) => {
131 out.push('{');
132 for (i, (key, item)) in map.iter().enumerate() {
133 if i > 0 {
134 out.push(',');
135 }
136 escape_string(key, out);
137 out.push(':');
138 write_value(item, out);
139 }
140 out.push('}');
141 }
142 }
143}
144
145pub fn canonical_json(value: &Value) -> String {
148 let mut out = String::new();
149 write_value(value, &mut out);
150 out
151}
152
153fn write_indent(indent: usize, level: usize, out: &mut String) {
155 out.extend(std::iter::repeat_n(' ', indent * level));
156}
157
158fn write_value_pretty(value: &Value, indent: usize, level: usize, out: &mut String) {
159 match value {
160 Value::Object(map) => {
161 if map.is_empty() {
162 out.push_str("{}");
163 return;
164 }
165 out.push_str("{\n");
166 let last = map.len() - 1;
167 for (i, (key, item)) in map.iter().enumerate() {
168 write_indent(indent, level + 1, out);
169 escape_string(key, out);
170 out.push_str(": ");
171 write_value_pretty(item, indent, level + 1, out);
172 if i != last {
173 out.push(',');
174 }
175 out.push('\n');
176 }
177 write_indent(indent, level, out);
178 out.push('}');
179 }
180 Value::Array(items) => {
181 if items.is_empty() {
182 out.push_str("[]");
183 return;
184 }
185 out.push_str("[\n");
186 let last = items.len() - 1;
187 for (i, item) in items.iter().enumerate() {
188 write_indent(indent, level + 1, out);
189 write_value_pretty(item, indent, level + 1, out);
190 if i != last {
191 out.push(',');
192 }
193 out.push('\n');
194 }
195 write_indent(indent, level, out);
196 out.push(']');
197 }
198 scalar => write_value(scalar, out),
199 }
200}
201
202pub fn canonical_json_pretty(value: &Value) -> String {
206 let mut out = String::new();
207 write_value_pretty(value, 2, 0, &mut out);
208 out.push('\n');
209 out
210}
211
212#[cfg(test)]
213mod tests {
214 use super::*;
215 use serde_json::json;
216
217 #[test]
218 fn float_repr_matches_python() {
219 let cases: [(f64, &str); 14] = [
221 (0.000047, "4.7e-05"),
222 (0.0001, "0.0001"),
223 (0.000175, "0.000175"),
224 (7.959543, "7.959543"),
225 (30.0, "30.0"),
226 (1.0, "1.0"),
227 (-0.5, "-0.5"),
228 (0.1, "0.1"),
229 (1e-5, "1e-05"),
230 (1e22, "1e+22"),
231 (1e16, "1e+16"),
232 (9999999999999998.0, "9999999999999998.0"),
233 (0.0, "0.0"),
234 (1.414, "1.414"),
235 ];
236 for (x, expected) in cases {
237 assert_eq!(python_float_repr(x), expected, "for {x}");
238 }
239 }
240
241 #[test]
242 fn canonical_matches_python_layout() {
243 let value = json!({
244 "b": [1, 2.5, null, true],
245 "a": {"nested": "ok", "t": 4.7e-5},
246 });
247 assert_eq!(
248 canonical_json(&value),
249 r#"{"a":{"nested":"ok","t":4.7e-05},"b":[1,2.5,null,true]}"#
250 );
251 }
252
253 #[test]
254 fn pretty_matches_python_indent2_layout() {
255 let value = json!({"b": [1, {"z": 1, "a": 2}], "a": {}});
258 assert_eq!(
259 canonical_json_pretty(&value),
260 "{\n \"a\": {},\n \"b\": [\n 1,\n {\n \"a\": 2,\n \"z\": 1\n }\n ]\n}\n"
261 );
262 }
263
264 #[test]
265 fn pretty_empty_object_and_array() {
266 assert_eq!(canonical_json_pretty(&json!({})), "{}\n");
267 assert_eq!(canonical_json_pretty(&json!([])), "[]\n");
268 }
269
270 #[test]
271 fn non_ascii_escaped_like_python() {
272 let value = json!({"s": "héllo — 🎉"});
275 assert_eq!(
276 canonical_json(&value),
277 "{\"s\":\"h\\u00e9llo \\u2014 \\ud83c\\udf89\"}"
278 );
279 }
280}