use crate::canon::round_to;
use crate::verdict::Mismatch;
use serde_json::Value;
const DISPLAY_QUANTUM: f64 = 1e-8;
fn close(a: f64, b: f64, atol: f64, rtol: f64) -> bool {
(a - b).abs() <= atol + rtol * a.abs().max(b.abs())
}
fn join(path: &str, key: &str) -> String {
if path.is_empty() {
key.to_string()
} else {
format!("{path}.{key}")
}
}
fn walk(
path: &str,
claimed: &Value,
actual: &Value,
atol: f64,
rtol: f64,
out: &mut Vec<Mismatch>,
) {
match (claimed, actual) {
(Value::Number(a), Value::Number(b)) => {
let (a, b) = (
a.as_f64().unwrap_or(f64::NAN),
b.as_f64().unwrap_or(f64::NAN),
);
if !close(a, b, atol, rtol) {
out.push(Mismatch {
field: path.to_string(),
claimed: a,
actual: b,
delta: round_to(b - a, DISPLAY_QUANTUM),
});
}
}
(Value::Array(a), Value::Array(b)) => {
if a.len() != b.len() {
let (la, lb) = (a.len() as f64, b.len() as f64);
out.push(Mismatch {
field: format!("{path}[len]"),
claimed: la,
actual: lb,
delta: round_to(lb - la, DISPLAY_QUANTUM),
});
}
for (i, (ai, bi)) in a.iter().zip(b.iter()).enumerate() {
walk(&format!("{path}[{i}]"), ai, bi, atol, rtol, out);
}
}
(Value::Object(a), Value::Object(b)) => {
for (key, av) in a {
if let Some(bv) = b.get(key) {
walk(&join(path, key), av, bv, atol, rtol, out);
}
}
}
_ => {}
}
}
#[must_use]
pub fn compare(claimed: &Value, actual: &Value, atol: f64, rtol: f64) -> Vec<Mismatch> {
let mut out = Vec::new();
walk("", claimed, actual, atol, rtol, &mut out);
out.sort_by(|x, y| x.field.cmp(&y.field));
out
}