Functional Weave
Code in Rust

stats.percentile@1.0.0

impl/rust.rs

2,949 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

const POW10: [f64; 13] = [
    1.0, 10.0, 100.0, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10, 1e11, 1e12,
];

// Half away from zero on the binary64 value, then -0 becomes 0. Only floor,
// * and / are used, so every language rounds the same double the same way.
fn round_to(x: f64, decimals: usize) -> f64 {
    let scale = POW10[decimals];
    let y = x.abs() * scale;
    let mut r = y.floor();
    if y - r >= 0.5 {
        r += 1.0;
    }
    let out = r / scale;
    (if x < 0.0 { -out } else { out }) + 0.0
}

/// The p-th percentile of `values` by a named method.
///
/// There is no single "95th percentile": the caller names the definition, so
/// a dashboard and a billing job cannot quietly disagree.
///
/// # Panics
/// Panics on an empty list, a non-finite value, p outside 0..=100, an unknown
/// method, or decimals outside 0..=12.
pub fn percentile(values: &[f64], p: f64, method: &str, decimals: i64) -> f64 {
    if values.is_empty() {
        panic!("values must not be empty");
    }
    for v in values {
        if !v.is_finite() {
            panic!("values must be finite numbers, received {}", v);
        }
    }
    if !p.is_finite() || p < 0.0 || p > 100.0 {
        panic!("p must be between 0 and 100, received {}", p);
    }
    if !(0..=12).contains(&decimals) {
        panic!("decimals must be a whole number from 0 to 12, received {}", decimals);
    }

    let mut sorted = values.to_vec();
    sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
    let n = sorted.len();

    let result = match method {
        "nearest-rank" => {
            // The smallest value with at least p% of the sample at or below it.
            let mut rank = ((p * n as f64) / 100.0).ceil() as usize;
            if rank < 1 {
                rank = 1;
            }
            sorted[rank - 1]
        }
        "linear" => {
            // Hyndman & Fan type 7: zero-based position (n - 1) * p / 100.
            let h = ((n - 1) as f64 * p) / 100.0;
            let lo = h.floor();
            let i = lo as usize;
            if i >= n - 1 {
                sorted[n - 1]
            } else {
                sorted[i] + (h - lo) * (sorted[i + 1] - sorted[i])
            }
        }
        other => panic!("unknown percentile method \"{}\"", other),
    };
    round_to(result, decimals as usize)
}

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.
    for v in args[0].as_arr() {
        if !matches!(v, Value::Int(_) | Value::Float(_)) {
            panic!("values must be finite numbers, received {:?}", v);
        }
    }
    let values: Vec<f64> = args[0].as_arr().iter().map(|v| v.as_f64()).collect();
    Value::Float(percentile(
        &values,
        args[1].as_f64(),
        args[2].as_str(),
        args[3].as_i64(),
    ))
}