use serde_json::Value;
pub fn canonical_json(value: &Value) -> String {
let mut out = String::new();
write_value(value, &mut out);
out
}
fn write_value(value: &Value, out: &mut String) {
match value {
Value::Null => out.push_str("null"),
Value::Bool(flag) => out.push_str(if *flag { "true" } else { "false" }),
Value::Number(number) => {
out.push_str(&canonical_number(number.as_f64().unwrap_or(f64::NAN)))
}
Value::String(text) => write_string(text, out),
Value::Array(items) => {
out.push('[');
for (index, item) in items.iter().enumerate() {
if index > 0 {
out.push(',');
}
write_value(item, out);
}
out.push(']');
}
Value::Object(map) => {
let mut keys: Vec<&String> = map.keys().collect();
keys.sort_by(|left, right| left.encode_utf16().cmp(right.encode_utf16()));
out.push('{');
for (index, key) in keys.iter().enumerate() {
if index > 0 {
out.push(',');
}
write_string(key, out);
out.push(':');
write_value(&map[*key], out);
}
out.push('}');
}
}
}
fn write_string(text: &str, out: &mut String) {
out.push('"');
for ch in text.chars() {
match ch {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\u{08}' => out.push_str("\\b"),
'\u{0c}' => out.push_str("\\f"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
control if control < '\u{20}' => {
out.push_str(&format!("\\u{:04x}", control as u32));
}
other => out.push(other),
}
}
out.push('"');
}
pub fn canonical_number(value: f64) -> String {
if !value.is_finite() {
return "null".to_string();
}
if value == 0.0 {
return "0".to_string();
}
if value.is_sign_negative() {
return format!("-{}", canonical_number(-value));
}
let scientific = format!("{value:e}");
let (mantissa, exponent) = scientific
.split_once('e')
.expect("Rust renders a finite f64 as <mantissa>e<exponent>");
let digits: String = mantissa.chars().filter(|ch| *ch != '.').collect();
let significant = digits.len() as i32;
let point = exponent
.parse::<i32>()
.expect("Rust renders the exponent as a decimal integer")
+ 1;
render_digits(&digits, significant, point)
}
fn render_digits(digits: &str, significant: i32, point: i32) -> String {
if significant <= point && point <= 21 {
return format!("{digits}{}", "0".repeat((point - significant) as usize));
}
if 0 < point && point <= 21 {
let split = point as usize;
return format!("{}.{}", &digits[..split], &digits[split..]);
}
if -6 < point && point <= 0 {
return format!("0.{}{digits}", "0".repeat((-point) as usize));
}
let exponent = point - 1;
let sign = if exponent.is_negative() { "" } else { "+" };
if significant == 1 {
return format!("{digits}e{sign}{exponent}");
}
format!("{}.{}e{sign}{exponent}", &digits[..1], &digits[1..])
}
#[cfg(test)]
#[path = "jcs_tests.rs"]
mod tests;