Functional Weave
Code in TypeScript

collections.group-by-key@1.0.0

impl/rust.rs

3,109 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

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

/// The name of the group a value belongs to.
///
/// Each language has its own default string conversion and they disagree
/// (Python prints True where JavaScript prints true, and 1.0 where JavaScript
/// prints 1), so the rendering is spelled out here instead of inherited.
///
/// # Panics
/// Panics on a float, a list or a map, or an integer outside the safe range.
fn group_name_of(value: &Value, key: &str) -> String {
    match value {
        // Absent and null are the same thing: `Value::get` returns Null for a
        // missing field, and a document that nulls a field means the same.
        Value::Null => String::new(),
        Value::Str(s) => s.clone(),
        Value::Bool(b) => (if *b { "true" } else { "false" }).to_string(),
        Value::Int(i) => {
            if i.abs() > SAFE_INTEGER {
                panic!("cannot group by the out-of-range number {} at \"{}\"", i, key);
            }
            i.to_string()
        }
        Value::Float(f) => panic!("cannot group by the fractional number {} at \"{}\"", f, key),
        _ => panic!("cannot group by the list or map at \"{}\"", key),
    }
}

/// Group `records` by the value at `key`.
///
/// Records keep their input order inside each group, so grouping a sorted list
/// never unsorts it - the property callers actually depend on.
///
/// Records are `Value` because they are open JSON maps; a struct would be a
/// lie about data whose shape differs from one record to the next. The result
/// is the groups in first-appearance order, as `map<T>` is in Rust.
pub fn group_by_key(records: &[Value], key: &str) -> Vec<(String, Vec<Value>)> {
    let mut groups: Vec<(String, Vec<Value>)> = Vec::new();

    for record in records {
        let name = group_name_of(record.get(key), key);
        // Linear scan rather than a hash map: it keeps first-appearance order
        // without a second pass, and group counts are small in practice.
        match groups.iter_mut().find(|(existing, _)| *existing == name) {
            Some((_, bucket)) => bucket.push(record.clone()),
            None => groups.push((name, vec![record.clone()])),
        }
    }

    groups
}

/// How many records fall in each group, without carrying the records themselves.
pub fn count_by_key(records: &[Value], key: &str) -> Vec<(String, i64)> {
    group_by_key(records, key)
        .into_iter()
        .map(|(name, bucket)| (name, bucket.len() as i64))
        .collect()
}

pub fn fune_vector(args: &[Value]) -> Value {
    // Refuse what the typed signature cannot hold, with the wording TypeScript
    // and Python use, rather than let the conversion below quietly change it.
    if args[1].as_str().is_empty() {
        panic!("groupByKey needs a non-empty key name");
    }
    Value::Obj(
        group_by_key(args[0].as_arr(), args[1].as_str())
            .into_iter()
            .map(|(name, bucket)| (name, Value::Arr(bucket)))
            .collect(),
    )
}