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::cmp::Ordering;
// The type ranks that give mixed values a total order. Absent sorts last in
// both directions, so it is ranked above every present value and then excluded
// from the direction flip below.
const RANK_BOOL: u8 = 0;
const RANK_NUMBER: u8 = 1;
const RANK_STRING: u8 = 2;
const RANK_ABSENT: u8 = 3;
/// # Panics
/// Panics if the value at the key is a list or a map.
fn rank_of(value: &Value, key: &str) -> u8 {
match value {
// `Value::get` returns Null for a missing field, and a document that
// nulls a field means the same thing, so both rank as absent.
Value::Null => RANK_ABSENT,
Value::Bool(_) => RANK_BOOL,
Value::Int(_) | Value::Float(_) => RANK_NUMBER,
Value::Str(_) => RANK_STRING,
_ => panic!("cannot sort by the list or map at \"{}\"", key),
}
}
fn compare_scalars(a: &Value, b: &Value) -> Ordering {
match (a, b) {
// Integers compare as integers: going through f64 would collapse
// values above 2^53 that are genuinely different.
(Value::Int(x), Value::Int(y)) => x.cmp(y),
// Rust compares &str by UTF-8 bytes, which is Unicode code point order,
// the same order Python uses and the one TypeScript spells out by hand.
(Value::Str(x), Value::Str(y)) => x.cmp(y),
(Value::Bool(x), Value::Bool(y)) => x.cmp(y),
_ => a.as_f64().partial_cmp(&b.as_f64()).unwrap_or(Ordering::Equal),
}
}
/// Stably sort `records` by `key`, ascending or descending.
///
/// Ties keep their input order in both directions, which is what lets a user
/// sort by one column and then another and get the multi-column sort they
/// expect rather than a reshuffle. `sort_by` is a stable sort, and the
/// comparator returns Equal for ties rather than falling back to position.
///
/// 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.
///
/// # Panics
/// Panics on an unknown direction, an empty key, or a list or map at the key.
pub fn sort_by(records: &[Value], key: &str, direction: &str) -> Vec<Value> {
if key.is_empty() {
panic!("sort_by needs a non-empty key name");
}
if direction != "asc" && direction != "desc" {
panic!("direction must be \"asc\" or \"desc\", received \"{}\"", direction);
}
// Rank every record up front. Panicking mid-comparison would make the
// failure depend on which comparisons the sort happened to perform.
let ranks: Vec<u8> = records.iter().map(|r| rank_of(r.get(key), key)).collect();
let descending = direction == "desc";
let mut order: Vec<usize> = (0..records.len()).collect();
order.sort_by(|&i, &j| {
let (ri, rj) = (ranks[i], ranks[j]);
// Absent values sink to the bottom whichever way the sort runs: nobody
// wants the rows they know nothing about at the top of a descending table.
if ri == RANK_ABSENT || rj == RANK_ABSENT {
return match (ri, rj) {
(RANK_ABSENT, RANK_ABSENT) => Ordering::Equal,
(RANK_ABSENT, _) => Ordering::Greater,
_ => Ordering::Less,
};
}
let base = if ri != rj {
ri.cmp(&rj)
} else {
compare_scalars(records[i].get(key), records[j].get(key))
};
if descending {
base.reverse()
} else {
base
}
});
order.into_iter().map(|i| records[i].clone()).collect()
}
pub fn fune_vector(args: &[Value]) -> Value {
Value::Arr(sort_by(args[0].as_arr(), args[1].as_str(), args[2].as_str()))
}