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(),
)
}