use std::collections::HashSet;
use std::fmt::Write as _;
use crate::Value;
pub(crate) fn write(value: &Value, out: &mut String) {
match value {
Value::Null => out.push_str("Null"),
Value::Bool(true) => out.push_str("bool:true"),
Value::Bool(false) => out.push_str("bool:false"),
Value::Number(number) => write_number(*number, out),
Value::String(text) => write_string(text, out),
Value::Array(items) => write_array(items, out),
Value::Set(items) => {
out.push_str("set:");
write_set(items, out);
}
Value::Map(entries) => {
out.push_str("map:");
write_map(entries, out);
}
}
}
fn write_number(number: f64, out: &mut String) {
out.push_str("number:");
out.push_str(&number_to_string(number));
}
fn number_to_string(number: f64) -> String {
if number.is_nan() {
return "NaN".to_string();
}
if number == f64::INFINITY {
return "Infinity".to_string();
}
if number == f64::NEG_INFINITY {
return "-Infinity".to_string();
}
if number == 0.0 {
return "0".to_string();
}
let sign = if number < 0.0 { "-" } else { "" };
let scientific = format!("{:e}", number.abs());
let (mantissa, exponent) = scientific.split_once('e').expect("scientific form");
let exponent: i32 = exponent.parse().expect("exponent");
let mut digits: String = mantissa.chars().filter(|c| *c != '.').collect();
while digits.len() > 1 && digits.ends_with('0') {
digits.pop();
}
let digits = digits.as_str();
let count = digits.len() as i32;
let point = exponent + 1;
if count <= point && point <= 21 {
let mut text = String::from(digits);
text.extend(std::iter::repeat_n('0', (point - count) as usize));
format!("{sign}{text}")
} else if 0 < point && point <= 21 {
let split = point as usize;
format!("{sign}{}.{}", &digits[..split], &digits[split..])
} else if -6 < point && point <= 0 {
let mut text = String::from("0.");
text.extend(std::iter::repeat_n('0', (-point) as usize));
text.push_str(digits);
format!("{sign}{text}")
} else if count == 1 {
format!(
"{sign}{digits}e{}{}",
exponent_sign(point - 1),
(point - 1).abs()
)
} else {
format!(
"{sign}{}.{}e{}{}",
&digits[..1],
&digits[1..],
exponent_sign(point - 1),
(point - 1).abs()
)
}
}
fn exponent_sign(exponent: i32) -> char {
if exponent < 0 { '-' } else { '+' }
}
fn write_string(text: &str, out: &mut String) {
let length: usize = text.chars().map(|c| c.len_utf16()).sum();
let _ = write!(out, "string:{length}:");
out.push_str(text);
}
fn write_array(items: &[Value], out: &mut String) {
let _ = write!(out, "array:{}:", items.len());
for item in items {
write(item, out);
}
}
fn utf16_cmp(a: &str, b: &str) -> std::cmp::Ordering {
a.encode_utf16().cmp(b.encode_utf16())
}
fn write_unordered(entries: Vec<String>, out: &mut String) {
let count = entries.len();
let _ = write!(out, "array:{count}:");
if count <= 1 {
for entry in entries {
out.push_str(&entry);
}
return;
}
let mut sorted = entries;
sorted.sort_by(|a, b| utf16_cmp(a, b));
let _ = write!(out, "array:{count}:");
for entry in sorted {
write_string(&entry, out);
}
}
fn write_set(items: &[Value], out: &mut String) {
let mut seen = HashSet::new();
let entries = items
.iter()
.filter_map(|item| {
if item.is_scalar() {
let written = canonical(item);
if !seen.insert(written.clone()) {
return None;
}
Some(written)
} else {
Some(canonical(item))
}
})
.collect();
write_unordered(entries, out);
}
fn write_map(entries: &[(String, Value)], out: &mut String) {
let mut kept: Vec<(&String, &Value)> = Vec::with_capacity(entries.len());
for (key, value) in entries {
match kept.iter_mut().find(|(existing, _)| *existing == key) {
Some(existing) => existing.1 = value,
None => kept.push((key, value)),
}
}
let serialized = kept
.iter()
.map(|(key, value)| {
let mut entry = String::from("array:2:");
write_string(key, &mut entry);
write(value, &mut entry);
entry
})
.collect();
write_unordered(serialized, out);
}
pub(crate) fn canonical(value: &Value) -> String {
let mut out = String::new();
write(value, &mut out);
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_null_is_the_word() {
assert_eq!(canonical(&Value::Null), "Null");
}
#[test]
fn booleans_are_labelled() {
assert_eq!(canonical(&Value::Bool(true)), "bool:true");
assert_eq!(canonical(&Value::Bool(false)), "bool:false");
}
#[test]
fn integers_and_equal_floats_are_the_same() {
assert_eq!(canonical(&Value::Number(1.0)), "number:1");
assert_eq!(canonical(&Value::Number(100.0)), "number:100");
assert_eq!(canonical(&Value::Number(-1.0)), "number:-1");
}
#[test]
fn negative_zero_is_written_as_zero() {
assert_eq!(canonical(&Value::Number(-0.0)), "number:0");
}
#[test]
fn the_special_numbers_have_names() {
assert_eq!(canonical(&Value::Number(f64::NAN)), "number:NaN");
assert_eq!(canonical(&Value::Number(f64::INFINITY)), "number:Infinity");
assert_eq!(
canonical(&Value::Number(f64::NEG_INFINITY)),
"number:-Infinity"
);
}
#[test]
fn a_string_carries_its_utf16_length() {
assert_eq!(canonical(&Value::String(String::new())), "string:0:");
assert_eq!(canonical(&Value::String("hello".into())), "string:5:hello");
assert_eq!(canonical(&Value::String("café".into())), "string:4:café");
assert_eq!(canonical(&Value::String("䏿–‡".into())), "string:2:䏿–‡");
assert_eq!(canonical(&Value::String("😀".into())), "string:2:😀");
}
#[test]
fn an_array_keeps_its_order() {
assert_eq!(canonical(&Value::Array(vec![])), "array:0:");
assert_eq!(
canonical(&Value::Array(vec![Value::Number(1.0), Value::Number(2.0)])),
"array:2:number:1number:2"
);
assert_eq!(
canonical(&Value::Array(vec![Value::Number(2.0), Value::Number(1.0)])),
"array:2:number:2number:1"
);
}
#[test]
fn a_map_is_written_in_key_order() {
let forward = Value::Map(vec![
("a".into(), Value::Number(1.0)),
("b".into(), Value::Number(2.0)),
]);
let backward = Value::Map(vec![
("b".into(), Value::Number(2.0)),
("a".into(), Value::Number(1.0)),
]);
assert_eq!(canonical(&forward), canonical(&backward));
}
#[test]
fn a_set_ignores_order_but_an_array_does_not() {
let one = Value::Number(1.0);
let two = Value::Number(2.0);
let set_forward = Value::Set(vec![one.clone(), two.clone()]);
let set_backward = Value::Set(vec![two.clone(), one.clone()]);
assert_eq!(canonical(&set_forward), canonical(&set_backward));
let array_forward = Value::Array(vec![one.clone(), two.clone()]);
let array_backward = Value::Array(vec![two, one]);
assert_ne!(canonical(&array_forward), canonical(&array_backward));
}
#[test]
fn a_set_drops_repeated_scalars_but_not_repeated_arrays() {
assert_eq!(
canonical(&Value::Set(vec![Value::Number(1.0), Value::Number(1.0)])),
"set:array:1:number:1"
);
assert_eq!(
canonical(&Value::Set(vec![
Value::Number(f64::NAN),
Value::Number(f64::NAN)
])),
"set:array:1:number:NaN"
);
let array = Value::Array(vec![Value::Number(1.0), Value::Number(2.0)]);
let two_arrays = Value::Set(vec![array.clone(), array]);
assert!(canonical(&two_arrays).starts_with("set:array:2:"));
}
#[test]
fn entries_are_ordered_by_utf16_code_unit() {
let emoji_first = Value::Set(vec![
Value::String("\u{ffff}z".into()),
Value::String("\u{1f600}".into()),
]);
let emoji_last = Value::Set(vec![
Value::String("\u{1f600}".into()),
Value::String("\u{ffff}z".into()),
]);
assert_eq!(canonical(&emoji_first), canonical(&emoji_last));
let written = canonical(&emoji_first);
let emoji_at = written.find('\u{1f600}').expect("emoji present");
let max_at = written.find('\u{ffff}').expect("U+FFFF present");
assert!(
emoji_at < max_at,
"emoji should sort before U+FFFF: {written}"
);
}
#[test]
fn a_repeated_map_key_keeps_the_last_value() {
let repeated = Value::Map(vec![
("a".into(), Value::Number(1.0)),
("b".into(), Value::Number(9.0)),
("a".into(), Value::Number(2.0)),
]);
let resolved = Value::Map(vec![
("a".into(), Value::Number(2.0)),
("b".into(), Value::Number(9.0)),
]);
assert_eq!(canonical(&repeated), canonical(&resolved));
}
#[test]
fn a_map_key_cannot_be_confused_with_a_value() {
let first = Value::Map(vec![("ab".into(), Value::Number(1.0))]);
let second = Value::Map(vec![("a".into(), Value::String("b1".into()))]);
assert_ne!(canonical(&first), canonical(&second));
}
}