Functional Weave
Code in Python

stats.weighted-average@1.0.0

impl/rust.rs

2,844 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 super::math_round_div::round_div;  ← from math.round-div ^1.0.0 · built alongside by fune

const MAX_SAFE: i128 = 9_007_199_254_740_991;

/// Weighted average of integers, rounded once at the end.
///
/// The numerator and denominator are exact integers, so the rounding mode the
/// caller names is the only rounding that ever happens.
///
/// # Panics
/// Panics on mismatched or empty lists, a negative weight, all-zero weights,
/// decimals outside 0..=9, or a weighted sum too large to hold exactly.
pub fn weighted_average(values: &[i64], weights: &[i64], decimals: i64, mode: &str) -> f64 {
    if values.len() != weights.len() {
        panic!(
            "values and weights must be the same length, received {} and {}",
            values.len(),
            weights.len()
        );
    }
    if values.is_empty() {
        panic!("values must not be empty");
    }
    if !(0..=9).contains(&decimals) {
        panic!("decimals must be a whole number from 0 to 9, received {}", decimals);
    }

    let mut numerator: i128 = 0;
    let mut denominator: i128 = 0;
    for (&v, &w) in values.iter().zip(weights) {
        if (v as i128).abs() > MAX_SAFE {
            panic!("values must be integers, received {}", v);
        }
        if (w as i128).abs() > MAX_SAFE {
            panic!("weights must be integers, received {}", w);
        }
        if w < 0 {
            panic!("weights must not be negative, received {}", w);
        }
        numerator += v as i128 * w as i128;
        denominator += w as i128;
    }
    if denominator == 0 {
        panic!("weights must not all be zero");
    }

    let scale = 10i128.pow(decimals as u32);
    let scaled = numerator * scale;
    if scaled.abs() > MAX_SAFE || denominator > MAX_SAFE {
        panic!("weighted sum is too large to average exactly; lower decimals or rescale the values");
    }
    round_div(scaled as i64, denominator as i64, mode) as f64 / scale as f64 + 0.0
}

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