Functional Weave
Code in TypeScript

collections.sort-by@1.0.0

impl/rust.rs

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