Functional Weave
Code in Rust

collections.diff@1.0.0

impl/rust.rs

5,898 bytes · the Rust implementation · view raw

Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.

use super::funejson::Value;  ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one
use std::collections::HashMap;

/// Largest integer JavaScript can hold exactly; beyond it the three languages disagree.
const SAFE_INTEGER: i64 = 9007199254740991;

/// A record's key as text, rendered the way collections.group-by-key renders a
/// group name so the two agree on what "the same key" means. None when absent.
///
/// # Panics
/// Panics on a float, a list or a map, or an integer outside the safe range.
fn key_of(value: &Value, key: &str) -> Option<String> {
    match value {
        Value::Null => None,
        Value::Str(s) => Some(s.clone()),
        Value::Bool(b) => Some((if *b { "true" } else { "false" }).to_string()),
        Value::Int(i) => {
            if i.abs() > SAFE_INTEGER {
                panic!("cannot diff by the out-of-range number {} at \"{}\"", i, key);
            }
            Some(i.to_string())
        }
        Value::Float(f) => panic!("cannot diff by the fractional number {} at \"{}\"", f, key),
        _ => panic!("cannot diff by the list or map at \"{}\"", key),
    }
}

/// Deep JSON equality, spelled out so all three languages agree: numbers by
/// value (1 equals 1.0), no coercion between types (true is not 1, "1" is not
/// 1), lists in order, and a missing map field equal to a null one.
fn same(a: &Value, b: &Value) -> bool {
    match (a, b) {
        (Value::Null, Value::Null) => true,
        (Value::Bool(x), Value::Bool(y)) => x == y,
        (Value::Int(x), Value::Int(y)) => x == y,
        (Value::Int(_), Value::Float(_)) | (Value::Float(_), Value::Int(_)) | (Value::Float(_), Value::Float(_)) => {
            a.as_f64() == b.as_f64()
        }
        (Value::Str(x), Value::Str(y)) => x == y,
        (Value::Arr(x), Value::Arr(y)) => x.len() == y.len() && x.iter().zip(y).all(|(p, q)| same(p, q)),
        (Value::Obj(x), Value::Obj(y)) => {
            // `get` answers Null for a missing field, which is the rule.
            x.iter().all(|(k, v)| same(v, b.get(k))) && y.iter().all(|(k, v)| same(a.get(k), v))
        }
        _ => false,
    }
}

fn field_names(record: &Value) -> Vec<String> {
    match record {
        Value::Obj(pairs) => pairs.iter().map(|(k, _)| k.clone()).collect(),
        _ => Vec::new(),
    }
}

/// Key every record of one list, refusing missing and duplicate keys.
///
/// # Panics
/// Panics on a record with no key and on a duplicate key.
fn index(records: &[Value], key: &str, side: &str) -> (Vec<String>, HashMap<String, usize>) {
    let mut order: Vec<String> = Vec::new();
    let mut by_key: HashMap<String, usize> = HashMap::new();
    for (i, record) in records.iter().enumerate() {
        let k = match key_of(record.get(key), key) {
            Some(k) => k,
            None => panic!("record {} in {} has no value at \"{}\"", i, side, key),
        };
        // Picking one of two duplicates would report changes that never happened.
        if by_key.contains_key(&k) {
            panic!("duplicate key \"{}\" in {}", k, side);
        }
        by_key.insert(k.clone(), i);
        order.push(k);
    }
    (order, by_key)
}

/// Compare two lists of records matched by `key`: what was added, what was
/// removed, and what changed and in which fields.
///
/// # Panics
/// Panics on an empty key, a record without a key, a duplicate key, or a key
/// value that cannot be rendered.
pub fn diff_by_key(before: &[Value], after: &[Value], key: &str) -> RecordDiff<Value> {
    if key.is_empty() {
        panic!("diff_by_key needs a non-empty key name");
    }

    let (before_order, before_keys) = index(before, key, "before");
    let (after_order, after_keys) = index(after, key, "after");

    let mut added: Vec<Value> = Vec::new();
    let mut changed: Vec<RecordChange<Value>> = Vec::new();
    let mut unchanged: i64 = 0;

    for (i, k) in after_order.iter().enumerate() {
        let next = &after[i];
        let prev = match before_keys.get(k) {
            Some(j) => &before[*j],
            None => {
                added.push(next.clone());
                continue;
            }
        };
        let mut names = field_names(prev);
        for name in field_names(next) {
            if !names.contains(&name) {
                names.push(name);
            }
        }
        let mut fields: Vec<String> = names
            .into_iter()
            .filter(|name| !same(prev.get(name), next.get(name)))
            .collect();
        // String ordering in Rust is byte order, which for UTF-8 is code point order.
        fields.sort();
        if fields.is_empty() {
            unchanged += 1;
        } else {
            changed.push(RecordChange {
                key: k.clone(),
                before: prev.clone(),
                after: next.clone(),
                fields,
            });
        }
    }

    let removed: Vec<Value> = before_order
        .iter()
        .enumerate()
        .filter(|(_, k)| !after_keys.contains_key(*k))
        .map(|(j, _)| before[j].clone())
        .collect();

    RecordDiff {
        added,
        removed,
        changed,
        unchanged,
    }
}

pub fn record_change_to_value(change: &RecordChange<Value>) -> Value {
    Value::obj(vec![
        ("key", Value::str(&change.key)),
        ("before", change.before.clone()),
        ("after", change.after.clone()),
        ("fields", Value::Arr(change.fields.iter().map(|f| Value::str(f)).collect())),
    ])
}

pub fn record_diff_to_value(diff: &RecordDiff<Value>) -> Value {
    Value::obj(vec![
        ("added", Value::Arr(diff.added.clone())),
        ("removed", Value::Arr(diff.removed.clone())),
        ("changed", Value::Arr(diff.changed.iter().map(record_change_to_value).collect())),
        ("unchanged", Value::Int(diff.unchanged)),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    record_diff_to_value(&diff_by_key(args[0].as_arr(), args[1].as_arr(), args[2].as_str()))
}