1use serde_json::Value;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub struct Dumps {
17 pub ensure_ascii: bool,
19}
20
21impl Dumps {
22 #[must_use]
24 pub fn to_string(self, value: &Value) -> String {
25 let mut out = String::new();
26 self.write(value, &mut out);
27 out
28 }
29
30 pub fn write(self, value: &Value, out: &mut String) {
32 match value {
33 Value::Null => out.push_str("null"),
34 Value::Bool(true) => out.push_str("true"),
35 Value::Bool(false) => out.push_str("false"),
36 Value::Number(n) => {
37 if let Some(i) = n.as_i64() {
38 out.push_str(&i.to_string());
39 } else if let Some(u) = n.as_u64() {
40 out.push_str(&u.to_string());
41 } else {
42 float_repr(n.as_f64().unwrap_or(f64::NAN), out);
43 }
44 }
45 Value::String(s) => self.string(s, out),
46 Value::Array(items) => {
47 out.push('[');
48 for (i, item) in items.iter().enumerate() {
49 if i > 0 {
50 out.push_str(", ");
51 }
52 self.write(item, out);
53 }
54 out.push(']');
55 }
56 Value::Object(map) => {
57 out.push('{');
58 for (i, (k, v)) in map.iter().enumerate() {
59 if i > 0 {
60 out.push_str(", ");
61 }
62 self.string(k, out);
63 out.push_str(": ");
64 self.write(v, out);
65 }
66 out.push('}');
67 }
68 }
69 }
70
71 fn string(self, 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 => push_u(out, c as u32),
83 c if self.ensure_ascii && !(' '..='~').contains(&c) => {
86 let mut buf = [0u16; 2];
87 for unit in c.encode_utf16(&mut buf) {
88 push_u(out, u32::from(*unit));
89 }
90 }
91 c => out.push(c),
92 }
93 }
94 out.push('"');
95 }
96}
97
98fn push_u(out: &mut String, unit: u32) {
99 use std::fmt::Write;
100 let _ = write!(out, "\\u{unit:04x}");
101}
102
103pub fn float_repr(f: f64, out: &mut String) {
111 if f.is_nan() {
112 out.push_str("NaN");
113 return;
114 }
115 if f.is_infinite() {
116 out.push_str(if f > 0.0 { "Infinity" } else { "-Infinity" });
117 return;
118 }
119 let sci = format!("{f:e}");
121 let (mantissa, exp) = sci.split_once('e').expect("{:e} always has an exponent");
122 let exp: i32 = exp.parse().expect("{:e} writes a decimal exponent");
123 let (neg, mantissa) = match mantissa.strip_prefix('-') {
124 Some(m) => (true, m),
125 None => (false, mantissa),
126 };
127 let digits: String = mantissa.chars().filter(char::is_ascii_digit).collect();
128 if neg {
129 out.push('-');
130 }
131 if (-4..16).contains(&exp) {
132 if exp < 0 {
133 out.push_str("0.");
134 for _ in 0..(-exp - 1) {
135 out.push('0');
136 }
137 out.push_str(&digits);
138 } else {
139 let point = exp as usize + 1;
140 if digits.len() <= point {
141 out.push_str(&digits);
142 for _ in digits.len()..point {
143 out.push('0');
144 }
145 out.push_str(".0");
146 } else {
147 out.push_str(&digits[..point]);
148 out.push('.');
149 out.push_str(&digits[point..]);
150 }
151 }
152 } else {
153 out.push_str(&digits[..1]);
154 if digits.len() > 1 {
155 out.push('.');
156 out.push_str(&digits[1..]);
157 }
158 out.push('e');
159 out.push(if exp < 0 { '-' } else { '+' });
160 out.push_str(&format!("{:02}", exp.abs()));
161 }
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167 use serde_json::json;
168
169 fn repr(f: f64) -> String {
170 let mut s = String::new();
171 float_repr(f, &mut s);
172 s
173 }
174
175 #[test]
176 fn floats_print_like_python() {
177 let cases = [
179 (0.0, "0.0"),
180 (-0.0, "-0.0"),
181 (1.0, "1.0"),
182 (1.5, "1.5"),
183 (0.1, "0.1"),
184 (100.0, "100.0"),
185 (1e-5, "1e-05"),
186 (0.0001, "0.0001"),
187 (0.00012, "0.00012"),
188 (1.5e-7, "1.5e-07"),
189 (1e15, "1000000000000000.0"),
190 (1e16, "1e+16"),
191 (1.2345e16, "1.2345e+16"),
192 (123456789.125, "123456789.125"),
193 (1e100, "1e+100"),
194 (-2.5e-300, "-2.5e-300"),
195 (f64::MAX, "1.7976931348623157e+308"),
196 (5e-324, "5e-324"),
197 ];
198 for (f, want) in cases {
199 assert_eq!(repr(f), want, "{f:e}");
200 }
201 }
202
203 #[test]
204 fn dumps_matches_python() {
205 let v = json!({"b": [1, 2.0, true, null], "é": "a\"b\\c\n\u{1}\u{7f}ü😀", "n": -3});
206 assert_eq!(
207 Dumps { ensure_ascii: false }.to_string(&v),
208 "{\"b\": [1, 2.0, true, null], \"é\": \"a\\\"b\\\\c\\n\\u0001\u{7f}ü😀\", \"n\": -3}"
209 );
210 assert_eq!(
211 Dumps { ensure_ascii: true }.to_string(&v),
212 "{\"b\": [1, 2.0, true, null], \"\\u00e9\": \"a\\\"b\\\\c\\n\\u0001\\u007f\\u00fc\\ud83d\\ude00\", \"n\": -3}"
213 );
214 }
215}