Functional Weave
Code in Rust

stats.mean-median@1.0.0

impl/rust.rs

4,284 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 MAX_SAFE: i128 = 9_007_199_254_740_991;

fn gcd(a: i64, b: i64) -> i64 {
    let (mut a, mut b) = (a.abs(), b.abs());
    while b != 0 {
        let t = a % b;
        a = b;
        b = t;
    }
    a
}

fn fraction(numerator: i64, denominator: i64) -> Fraction {
    let g = match gcd(numerator, denominator) {
        0 => 1,
        g => g,
    };
    Fraction {
        numerator: numerator / g,
        denominator: denominator / g,
    }
}

/// Mean, median and mode of a list of integers.
///
/// Mean and median are exact fractions; their float forms come from a single
/// correctly rounded division, so every language returns the same double.
///
/// # Panics
/// Panics on an empty list, a value outside the safe integer range, or a sum
/// or median outside it.
pub fn mean_median_mode(values: &[i64]) -> CentralTendency {
    if values.is_empty() {
        panic!("values must not be empty");
    }
    let mut total: i128 = 0;
    for &v in values {
        if (v as i128).abs() > MAX_SAFE {
            panic!(
                "values must be safe integers (magnitude at most 9007199254740991), received {}",
                v
            );
        }
        total += v as i128;
    }
    if total.abs() > MAX_SAFE {
        panic!("sum exceeds the safe integer range");
    }
    let sum = total as i64;
    let count = values.len();

    let mut sorted = values.to_vec();
    sorted.sort_unstable();
    let median = if count % 2 == 1 {
        Fraction {
            numerator: sorted[(count - 1) / 2],
            denominator: 1,
        }
    } else {
        let pair = sorted[count / 2 - 1] as i128 + sorted[count / 2] as i128;
        if pair % 2 == 0 {
            Fraction {
                numerator: (pair / 2) as i64,
                denominator: 1,
            }
        } else {
            if pair.abs() > MAX_SAFE {
                panic!("median exceeds the safe integer range");
            }
            Fraction {
                numerator: pair as i64,
                denominator: 2,
            }
        }
    };

    // Runs of equal values in the sorted copy give frequencies in ascending
    // value order, so the modes come out sorted without a second sort.
    let mut modes: Vec<i64> = Vec::new();
    let mut mode_frequency = 0usize;
    let mut i = 0usize;
    while i < count {
        let mut j = i;
        while j < count && sorted[j] == sorted[i] {
            j += 1;
        }
        let run = j - i;
        if run > mode_frequency {
            mode_frequency = run;
            modes = vec![sorted[i]];
        } else if run == mode_frequency {
            modes.push(sorted[i]);
        }
        i = j;
    }

    CentralTendency {
        count: count as i64,
        sum,
        mean: fraction(sum, count as i64),
        mean_value: sum as f64 / count as f64 + 0.0,
        median_value: median.numerator as f64 / median.denominator as f64 + 0.0,
        median,
        modes,
        mode_frequency: mode_frequency as i64,
    }
}

pub fn fraction_to_value(f: &Fraction) -> Value {
    Value::obj(vec![
        ("numerator", Value::Int(f.numerator)),
        ("denominator", Value::Int(f.denominator)),
    ])
}

pub fn central_tendency_to_value(c: &CentralTendency) -> Value {
    Value::obj(vec![
        ("count", Value::Int(c.count)),
        ("sum", Value::Int(c.sum)),
        ("mean", fraction_to_value(&c.mean)),
        ("meanValue", Value::Float(c.mean_value)),
        ("median", fraction_to_value(&c.median)),
        ("medianValue", Value::Float(c.median_value)),
        ("modes", Value::Arr(c.modes.iter().map(|m| Value::Int(*m)).collect())),
        ("modeFrequency", Value::Int(c.mode_frequency)),
    ])
}

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(_)) && !matches!(v, Value::Float(f) if f.fract() == 0.0) {
            panic!("values must be integers, received {:?}", v);
        }
    }
    let values: Vec<i64> = args[0].as_arr().iter().map(|v| v.as_i64()).collect();
    central_tendency_to_value(&mean_median_mode(&values))
}