Functional Weave
Code in TypeScript

collections.dedupe-by-key@1.0.0

impl/rust.rs

2,902 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::HashSet;

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

/// The identity of a record, or `None` when it has none.
///
/// Rendered exactly as collections.group-by-key renders a group name, so the
/// two capabilities agree on what "the same key" means.
///
/// # 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 {
        // A record with no key is not a duplicate of anything: two records
        // that both lack an id are two unknowns, not one thing seen twice.
        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 dedupe by the out-of-range number {} at \"{}\"", i, key);
            }
            Some(i.to_string())
        }
        Value::Float(f) => panic!("cannot dedupe by the fractional number {} at \"{}\"", f, key),
        _ => panic!("cannot dedupe by the list or map at \"{}\"", key),
    }
}

/// Remove records that share the value at `key`, keeping the first or the
/// last of each set.
///
/// Survivors keep their own positions: with "last", the survivor sits where
/// the last occurrence was, which is what "latest wins" means in a change feed.
///
/// # Panics
/// Panics on an empty key, an unknown `keep`, or an ungroupable key value.
pub fn dedupe_by_key(records: &[Value], key: &str, keep: &str) -> Vec<Value> {
    if key.is_empty() {
        panic!("dedupe_by_key needs a non-empty key name");
    }
    if keep != "first" && keep != "last" {
        panic!("keep must be \"first\" or \"last\", received \"{}\"", keep);
    }

    // Every key is rendered up front, so a bad value panics whichever mode runs.
    let keys: Vec<Option<String>> = records.iter().map(|r| key_of(r.get(key), key)).collect();

    let mut seen: HashSet<String> = HashSet::new();
    let mut survives = vec![false; records.len()];
    let mut order: Vec<usize> = (0..records.len()).collect();
    // Walking backwards for "last" makes the last occurrence the first one seen.
    if keep == "last" {
        order.reverse();
    }
    for i in order {
        match &keys[i] {
            None => survives[i] = true,
            Some(k) => {
                if seen.insert(k.clone()) {
                    survives[i] = true;
                }
            }
        }
    }

    records
        .iter()
        .enumerate()
        .filter(|(i, _)| survives[*i])
        .map(|(_, r)| r.clone())
        .collect()
}

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