malevich 1.18.2

Terminal plotting: a small grammar of marks, honest axes, millions of points
Documentation
use super::Window;
use crate::stat::Reducer;

#[test]
fn trailing_means_smooth_without_a_warmup_gap() {
    let smoothed = Window::new(3).mean(&[3.0, 6.0, 9.0, 12.0]);
    assert_eq!(smoothed, [3.0, 4.5, 6.0, 9.0]);
}

#[test]
fn gaps_are_excluded_and_all_gap_windows_stay_gaps() {
    let smoothed = Window::new(2).mean(&[1.0, f64::NAN, 5.0]);
    assert_eq!(smoothed[0], 1.0);
    assert_eq!(smoothed[1], 1.0);
    assert_eq!(smoothed[2], 5.0);
    let gaps = Window::new(1).mean(&[f64::NAN]);
    assert!(gaps[0].is_nan());
}

#[test]
fn the_other_reducers_reduce() {
    let window = Window::new(2);
    assert_eq!(window.sum(&[1.0, 2.0, 3.0]), [1.0, 3.0, 5.0]);
    assert_eq!(window.min(&[3.0, 1.0, 2.0]), [3.0, 1.0, 1.0]);
    assert_eq!(window.max(&[1.0, 3.0, 2.0]), [1.0, 3.0, 3.0]);
}

#[test]
fn optimized_rolling_strategies_match_one_shot_reduction() {
    let values: Vec<f64> = (0..257)
        .map(|index| match index % 29 {
            0 => f64::NAN,
            1 => f64::INFINITY,
            _ => ((index * 37) % 101) as f64 - 50.0,
        })
        .collect();
    let reducers = [
        Reducer::Count,
        Reducer::Sum,
        Reducer::Mean,
        Reducer::Min,
        Reducer::Max,
        Reducer::Median,
        Reducer::Percentile(0.9),
    ];

    for size in [1, 2, 7, 64, 999] {
        let window = Window::new(size);
        for reducer in reducers {
            let actual = window.reduce(&values, reducer);
            let expected: Vec<f64> = (0..values.len())
                .map(|end| {
                    let start = (end + 1).saturating_sub(size);
                    reducer.reduce(&values[start..=end])
                })
                .collect();
            for (index, (&actual, &expected)) in actual.iter().zip(&expected).enumerate() {
                assert!(
                    (actual.is_nan() && expected.is_nan())
                        || (actual - expected).abs()
                            <= f64::EPSILON * 32.0 * actual.abs().max(expected.abs()).max(1.0),
                    "size {size}, reducer {reducer:?}, index {index}: {actual:?} != {expected:?}"
                );
            }
        }
    }
}

#[test]
fn an_overflowing_mean_does_not_poison_later_windows() {
    let means = Window::new(2).mean(&[f64::MAX, f64::MAX, -f64::MAX, 1.0]);
    assert_eq!(means[0], f64::MAX);
    assert_eq!(means[1], f64::MAX);
    assert_eq!(means[2], 0.0);
    assert_eq!(means[3], -f64::MAX / 2.0);
}

#[test]
#[should_panic(expected = "Reducer::Percentile requires a position in [0, 1]")]
fn invalid_percentiles_are_rejected_even_for_empty_input() {
    Window::new(3).reduce(&[], Reducer::Percentile(2.0));
}