Functional Weave
Code in Python

stats.moving-average@1.0.0

impl/rust.rs

2,997 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
}

// Summed afresh, left to right, so the result never depends on the history
// a running sum would carry.
fn window_mean(values: &[f64], end: usize, window: usize) -> f64 {
    let mut sum = 0.0;
    for v in &values[end + 1 - window..=end] {
        sum += *v;
    }
    sum / window as f64
}

/// Simple or exponential moving average, one value per full window.
///
/// The exponential average is seeded with the simple average of the first
/// window and carried unrounded; only the returned values are rounded.
///
/// # Panics
/// Panics on a non-finite value, a window below 1, an unknown kind, or
/// decimals outside 0..=12.
pub fn moving_average(values: &[f64], window: i64, kind: &str, decimals: i64) -> Vec<f64> {
    for v in values {
        if !v.is_finite() {
            panic!("values must be finite numbers, received {}", v);
        }
    }
    if window < 1 {
        panic!("window must be a whole number of 1 or greater, received {}", window);
    }
    if kind != "simple" && kind != "exponential" {
        panic!("unknown moving average kind \"{}\"", kind);
    }
    if !(0..=12).contains(&decimals) {
        panic!("decimals must be a whole number from 0 to 12, received {}", decimals);
    }

    let window = window as usize;
    let places = decimals as usize;
    let mut out = Vec::new();
    if values.len() < window {
        return out;
    }

    if kind == "simple" {
        for end in window - 1..values.len() {
            out.push(round_to(window_mean(values, end, window), places));
        }
        return out;
    }

    let alpha = 2.0 / (window as f64 + 1.0);
    let mut ema = window_mean(values, window - 1, window);
    out.push(round_to(ema, places));
    for v in &values[window..] {
        ema = ema + alpha * (*v - ema);
        out.push(round_to(ema, places));
    }
    out
}

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();
    let out = moving_average(&values, args[1].as_i64(), args[2].as_str(), args[3].as_i64());
    Value::Arr(out.into_iter().map(Value::Float).collect())
}