use serde_json::{Number, Value};
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct UnsupportedNumber(pub(crate) String);
pub(crate) fn to_string(value: &Value) -> Result<String, UnsupportedNumber> {
let mut out = String::new();
write_value(&mut out, value)?;
Ok(out)
}
fn write_value(out: &mut String, value: &Value) -> Result<(), UnsupportedNumber> {
match value {
Value::Null => out.push_str("null"),
Value::Bool(true) => out.push_str("true"),
Value::Bool(false) => out.push_str("false"),
Value::Number(number) => write_number(out, finite(number)?),
Value::String(string) => write_string(out, string),
Value::Array(items) => {
out.push('[');
for (index, item) in items.iter().enumerate() {
if index > 0 {
out.push(',');
}
write_value(out, item)?;
}
out.push(']');
}
Value::Object(members) => {
let mut members: Vec<_> = members.iter().collect();
members.sort_by(|(a, _), (b, _)| a.encode_utf16().cmp(b.encode_utf16()));
out.push('{');
for (index, (name, member)) in members.into_iter().enumerate() {
if index > 0 {
out.push(',');
}
write_string(out, name);
out.push(':');
write_value(out, member)?;
}
out.push('}');
}
}
Ok(())
}
fn finite(number: &Number) -> Result<f64, UnsupportedNumber> {
number
.as_f64()
.filter(|value| value.is_finite())
.ok_or_else(|| UnsupportedNumber(number.to_string()))
}
fn write_string(out: &mut String, string: &str) {
out.push_str(&serde_json::to_string(string).expect("a string serializes"));
}
fn write_number(out: &mut String, value: f64) {
if value == 0.0 {
out.push('0');
return;
}
if value.is_sign_negative() {
out.push('-');
}
let magnitude = value.abs();
let shortest = format!("{magnitude:e}");
let k = shortest.split_once('e').map_or(0, |(mantissa, _)| {
mantissa.bytes().filter(u8::is_ascii_digit).count()
});
let rounded = format!("{magnitude:.*e}", k.saturating_sub(1));
let scientific = if rounded.parse::<f64>() == Ok(magnitude) {
rounded
} else {
shortest
};
let (mantissa, exponent) = scientific
.split_once('e')
.expect("`{:e}` writes an exponent");
let exponent: i32 = exponent.parse().expect("the exponent is an integer");
let digits: String = mantissa.chars().filter(|c| *c != '.').collect();
let digits = digits.trim_end_matches('0');
let k = i32::try_from(digits.len()).expect("at most 17 digits");
let n = exponent + 1;
let zeros = |count: i32| "0".repeat(usize::try_from(count).unwrap_or_default());
if k <= n && n <= 21 {
out.push_str(digits);
out.push_str(&zeros(n - k));
} else if 0 < n && n <= 21 {
let (integer, fraction) = digits.split_at(usize::try_from(n).expect("n is positive"));
out.push_str(integer);
out.push('.');
out.push_str(fraction);
} else if -6 < n && n <= 0 {
out.push_str("0.");
out.push_str(&zeros(-n));
out.push_str(digits);
} else {
let (first, rest) = digits.split_at(1);
out.push_str(first);
if !rest.is_empty() {
out.push('.');
out.push_str(rest);
}
out.push('e');
out.push(if n - 1 < 0 { '-' } else { '+' });
out.push_str(&(n - 1).unsigned_abs().to_string());
}
}
#[cfg(test)]
mod tests {
use serde_json::{Value, json};
use super::{to_string, write_number};
fn number(value: f64) -> String {
let mut out = String::new();
write_number(&mut out, value);
out
}
#[test]
fn number_serialization_samples() {
for (bits, expected) in [
(0x0000_0000_0000_0000_u64, "0"),
(0x8000_0000_0000_0000, "0"),
(0x0000_0000_0000_0001, "5e-324"),
(0x8000_0000_0000_0001, "-5e-324"),
(0x7fef_ffff_ffff_ffff, "1.7976931348623157e+308"),
(0xffef_ffff_ffff_ffff, "-1.7976931348623157e+308"),
(0x4340_0000_0000_0000, "9007199254740992"),
(0xc340_0000_0000_0000, "-9007199254740992"),
(0x4430_0000_0000_0000, "295147905179352830000"),
(0x44b5_2d02_c7e1_4af5, "9.999999999999997e+22"),
(0x44b5_2d02_c7e1_4af6, "1e+23"),
(0x44b5_2d02_c7e1_4af7, "1.0000000000000001e+23"),
(0x444b_1ae4_d6e2_ef4e, "999999999999999700000"),
(0x444b_1ae4_d6e2_ef4f, "999999999999999900000"),
(0x444b_1ae4_d6e2_ef50, "1e+21"),
(0x3eb0_c6f7_a0b5_ed8c, "9.999999999999997e-7"),
(0x3eb0_c6f7_a0b5_ed8d, "0.000001"),
(0x41b3_de43_5555_5553, "333333333.3333332"),
(0x41b3_de43_5555_5554, "333333333.33333325"),
(0x41b3_de43_5555_5555, "333333333.3333333"),
(0x41b3_de43_5555_5556, "333333333.3333334"),
(0x41b3_de43_5555_5557, "333333333.33333343"),
(0xbecb_f647_612f_3696, "-0.0000033333333333333333"),
] {
assert_eq!(number(f64::from_bits(bits)), expected, "{bits:016x}");
}
assert_eq!(
number(f64::from_bits(0x4340_0000_0000_0001)),
"9007199254740994"
);
assert_eq!(
number(f64::from_bits(0x4340_0000_0000_0002)),
"9007199254740996"
);
assert_eq!(
number(f64::from_bits(0xc313_a13c_9c26_9a59)),
"-1381326561846934.2"
);
for (value, expected) in [
(1.0, "1"),
(-56.0, "-56"),
(4.5, "4.5"),
(0.5, "0.5"),
(0.125, "0.125"),
(0.002, "0.002"),
(1e-7, "1e-7"),
(1.5e-7, "1.5e-7"),
(1e30, "1e+30"),
(123_456_789_012_345_680_000.0, "123456789012345680000"),
] {
assert_eq!(number(value), expected);
}
}
#[test]
fn rfc_sample() {
let input: Value = serde_json::from_str(
r#"{
"numbers": [333333333.33333329, 1E30, 4.50, 2e-3, 0.000000000000000000000000001],
"string": "\u20ac$\u000F\u000aA'\u0042\u0022\u005c\\\"\/",
"literals": [null, true, false]
}"#,
)
.unwrap();
let bytes = [
"7b 22 6c 69 74 65 72 61 6c 73 22 3a 5b 6e 75 6c 6c 2c 74 72",
"75 65 2c 66 61 6c 73 65 5d 2c 22 6e 75 6d 62 65 72 73 22 3a",
"5b 33 33 33 33 33 33 33 33 33 2e 33 33 33 33 33 33 33 2c 31",
"65 2b 33 30 2c 34 2e 35 2c 30 2e 30 30 32 2c 31 65 2d 32 37",
"5d 2c 22 73 74 72 69 6e 67 22 3a 22 e2 82 ac 24 5c 75 30 30",
"30 66 5c 6e 41 27 42 5c 22 5c 5c 5c 5c 5c 22 2f 22 7d",
]
.join(" ")
.split(' ')
.map(|byte| u8::from_str_radix(byte, 16).unwrap())
.collect::<Vec<u8>>();
assert_eq!(to_string(&input).unwrap().as_bytes(), bytes);
}
#[test]
fn reference_test_data() {
for (input, output) in [
(
r#"[56, {"d": true, "10": null, "1": [ ]}]"#,
r#"[56,{"1":[],"10":null,"d":true}]"#,
),
(
r#"{"peach": "This sorting order", "péché": "is wrong according to French", "pêche": "but canonicalization MUST", "sin": "ignore locale"}"#,
r#"{"peach":"This sorting order","péché":"is wrong according to French","pêche":"but canonicalization MUST","sin":"ignore locale"}"#,
),
(
r#"{"1": {"f": {"f": "hi","F": 5} ,"\n": 56.0}, "10": { }, "": "empty", "a": { }, "111": [ {"e": "yes","E": "no" } ], "A": { }}"#,
r#"{"":"empty","1":{"\n":56,"f":{"F":5,"f":"hi"}},"10":{},"111":[{"E":"no","e":"yes"}],"A":{},"a":{}}"#,
),
(
r#"{"Unnormalized Unicode":"A\u030a"}"#,
"{\"Unnormalized Unicode\":\"A\u{30a}\"}",
),
(
r#"{"\u20ac": "Euro Sign", "\r": "Carriage Return", "\u000a": "Newline", "1": "One", "\u0080": "Control\u007f", "\ud83d\ude02": "Smiley", "\u00f6": "Latin Small Letter O With Diaeresis", "\ufb33": "Hebrew Letter Dalet With Dagesh", "</script>": "Browser Challenge"}"#,
concat!(
r#"{"\n":"Newline","\r":"Carriage Return","1":"One","</script>":"Browser Challenge","#,
"\"\u{80}\":\"Control\u{7f}\",",
r#""ö":"Latin Small Letter O With Diaeresis","€":"Euro Sign","😂":"Smiley","#,
"\"\u{fb33}\":\"Hebrew Letter Dalet With Dagesh\"}",
),
),
] {
let value: Value = serde_json::from_str(input).unwrap();
assert_eq!(to_string(&value).unwrap(), output, "{input}");
let again: Value = serde_json::from_str(output).unwrap();
assert_eq!(to_string(&again).unwrap(), output, "{input}");
}
}
#[test]
fn sorts_by_utf16_code_units_not_code_points() {
let value = json!({"\u{fb33}": 1, "\u{1f602}": 2});
assert_eq!(
to_string(&value).unwrap(),
"{\"\u{1f602}\":2,\"\u{fb33}\":1}"
);
}
#[test]
fn integers_are_numbers() {
assert_eq!(
to_string(&json!([0, -0, 9_007_199_254_740_993_u64, -3])).unwrap(),
"[0,0,9007199254740992,-3]"
);
}
}