downsample 0.0.5

keep downsampled history of data over long period of time
Documentation
/// A reduction function used when a level rolls over into the next level.
///
/// The two input slices contain one logical aggregation window. The window is
/// split only when it wraps around the internal ring buffer.
pub struct Reducer<T> {
    reduce: fn(&[T], &[T]) -> T,
}

impl<T> Clone for Reducer<T> {
    fn clone(&self) -> Self { *self }
}

impl<T> Copy for Reducer<T> {}

impl<T> Reducer<T> {
    pub const fn new(reduce: fn(&[T], &[T]) -> T) -> Self { Self { reduce } }

    pub fn reduce(self, first: &[T], second: &[T]) -> T { (self.reduce)(first, second) }
}

pub mod reducers {
    use super::Reducer;
    use alloc::vec::Vec;

    pub fn first<T>() -> Reducer<T>
    where
        T: Copy + Default,
    {
        Reducer::new(first_impl::<T>)
    }

    pub fn last<T>() -> Reducer<T>
    where
        T: Copy + Default,
    {
        Reducer::new(last_impl::<T>)
    }

    pub fn min<T>() -> Reducer<T>
    where
        T: Copy + Default + PartialOrd,
    {
        Reducer::new(min_impl::<T>)
    }

    pub fn minimum<T>() -> Reducer<T>
    where
        T: Copy + Default + PartialOrd,
    {
        min()
    }

    pub fn max<T>() -> Reducer<T>
    where
        T: Copy + Default + PartialOrd,
    {
        Reducer::new(max_impl::<T>)
    }

    pub fn maximum<T>() -> Reducer<T>
    where
        T: Copy + Default + PartialOrd,
    {
        max()
    }

    /// Returns a reducer that computes the lower median.
    ///
    /// For even-sized windows this returns the lower of the two middle values.
    /// The implementation copies the aggregation window into a temporary `Vec`
    /// and sorts it.
    pub fn median<T>() -> Reducer<T>
    where
        T: Copy + Default + Ord,
    {
        Reducer::new(median_impl::<T>)
    }

    fn first_impl<T>(first: &[T], second: &[T]) -> T
    where
        T: Copy + Default,
    {
        first.first().or_else(|| second.first()).copied().unwrap_or_default()
    }

    fn last_impl<T>(first: &[T], second: &[T]) -> T
    where
        T: Copy + Default,
    {
        second.last().or_else(|| first.last()).copied().unwrap_or_default()
    }

    fn min_impl<T>(first: &[T], second: &[T]) -> T
    where
        T: Copy + Default + PartialOrd,
    {
        let mut iter = first.iter().chain(second);
        let Some(mut min) = iter.next().copied() else {
            return T::default();
        };
        for value in iter {
            if *value < min {
                min = *value;
            }
        }
        min
    }

    fn max_impl<T>(first: &[T], second: &[T]) -> T
    where
        T: Copy + Default + PartialOrd,
    {
        let mut iter = first.iter().chain(second);
        let Some(mut max) = iter.next().copied() else {
            return T::default();
        };
        for value in iter {
            if *value > max {
                max = *value;
            }
        }
        max
    }

    fn median_impl<T>(first: &[T], second: &[T]) -> T
    where
        T: Copy + Default + Ord,
    {
        let count = first.len() + second.len();
        if count == 0 {
            return T::default();
        }

        let mut values = Vec::with_capacity(count);
        values.extend_from_slice(first);
        values.extend_from_slice(second);
        values.sort_unstable();
        values[(count - 1) / 2]
    }

    macro_rules! average_float {
        ($name:ident, $impl_name:ident, $t:ty) => {
            pub fn $name() -> Reducer<$t> { Reducer::new($impl_name) }

            fn $impl_name(first: &[$t], second: &[$t]) -> $t {
                let count = first.len() + second.len();
                if count == 0 {
                    return <$t>::default();
                }

                let mut total = <$t>::default();
                for value in first.iter().chain(second) {
                    total += *value;
                }
                total / count as $t
            }
        };
    }

    macro_rules! average_unsigned {
        ($name:ident, $impl_name:ident, $t:ty, $acc:ty) => {
            pub fn $name() -> Reducer<$t> { Reducer::new($impl_name) }

            fn $impl_name(first: &[$t], second: &[$t]) -> $t {
                let count = first.len() + second.len();
                if count == 0 {
                    return <$t>::default();
                }

                let mut total: $acc = 0;
                for value in first.iter().chain(second) {
                    total += *value as $acc;
                }
                (total / count as $acc) as $t
            }
        };
    }

    macro_rules! average_signed {
        ($name:ident, $impl_name:ident, $t:ty, $acc:ty) => {
            pub fn $name() -> Reducer<$t> { Reducer::new($impl_name) }

            fn $impl_name(first: &[$t], second: &[$t]) -> $t {
                let count = first.len() + second.len();
                if count == 0 {
                    return <$t>::default();
                }

                let mut total: $acc = 0;
                for value in first.iter().chain(second) {
                    total += *value as $acc;
                }
                (total / count as $acc) as $t
            }
        };
    }

    average_float!(average_f32, average_f32_impl, f32);
    average_float!(average_f64, average_f64_impl, f64);
    average_unsigned!(average_u8, average_u8_impl, u8, u64);
    average_unsigned!(average_u16, average_u16_impl, u16, u64);
    average_unsigned!(average_u32, average_u32_impl, u32, u128);
    average_unsigned!(average_u64, average_u64_impl, u64, u128);
    average_unsigned!(average_u128, average_u128_impl, u128, u128);
    average_unsigned!(average_usize, average_usize_impl, usize, u128);
    average_signed!(average_i8, average_i8_impl, i8, i64);
    average_signed!(average_i16, average_i16_impl, i16, i64);
    average_signed!(average_i32, average_i32_impl, i32, i128);
    average_signed!(average_i64, average_i64_impl, i64, i128);
    average_signed!(average_i128, average_i128_impl, i128, i128);
    average_signed!(average_isize, average_isize_impl, isize, i128);

    pub fn median_f32() -> Reducer<f32> { Reducer::new(median_f32_impl) }

    pub fn median_f64() -> Reducer<f64> { Reducer::new(median_f64_impl) }

    fn median_f32_impl(first: &[f32], second: &[f32]) -> f32 {
        let count = first.len() + second.len();
        let mut values = Vec::with_capacity(first.len() + second.len());
        values.extend(first.iter().chain(second).copied().filter(|value| !value.is_nan()));
        if values.is_empty() {
            if count > 0 {
                return f32::NAN;
            }
            return f32::default();
        }

        values.sort_unstable_by(f32::total_cmp);
        values[(values.len() - 1) / 2]
    }

    fn median_f64_impl(first: &[f64], second: &[f64]) -> f64 {
        let count = first.len() + second.len();
        let mut values = Vec::with_capacity(first.len() + second.len());
        values.extend(first.iter().chain(second).copied().filter(|value| !value.is_nan()));
        if values.is_empty() {
            if count > 0 {
                return f64::NAN;
            }
            return f64::default();
        }

        values.sort_unstable_by(f64::total_cmp);
        values[(values.len() - 1) / 2]
    }
}

#[cfg(test)]
mod tests {
    use super::reducers;

    #[test]
    fn average_handles_split_windows() {
        assert_eq!(reducers::average_f32().reduce(&[1.0, 2.0], &[3.0, 4.0]), 2.5);
        assert_eq!(reducers::average_u32().reduce(&[1, 2], &[3, 4]), 2);
        assert_eq!(reducers::average_u128().reduce(&[1, 2], &[3, 4]), 2);
        assert_eq!(reducers::average_i128().reduce(&[-2, -1], &[1, 2]), 0);
    }

    #[test]
    fn average_handles_integer_edge_values() {
        assert_eq!(
            reducers::average_u64().reduce(&[u64::MAX, u64::MAX - 2], &[]),
            u64::MAX - 1
        );
        assert_eq!(
            reducers::average_usize().reduce(&[usize::MAX, usize::MAX - 2], &[]),
            usize::MAX - 1
        );
        assert_eq!(
            reducers::average_u128().reduce(&[u128::MAX / 2, u128::MAX / 2], &[]),
            u128::MAX / 2
        );
        assert_eq!(
            reducers::average_i64().reduce(&[i64::MIN + 2, i64::MIN + 4], &[]),
            i64::MIN + 3
        );
        assert_eq!(
            reducers::average_i64().reduce(&[i64::MAX - 1, i64::MAX - 3], &[]),
            i64::MAX - 2
        );
    }

    #[test]
    fn min_max_handle_empty_windows() {
        assert_eq!(reducers::min::<i32>().reduce(&[], &[]), 0);
        assert_eq!(reducers::max::<i32>().reduce(&[1, 5], &[3]), 5);
        assert_eq!(reducers::minimum::<i32>().reduce(&[1, -5], &[3]), -5);
        assert_eq!(reducers::maximum::<i32>().reduce(&[1, -5], &[3]), 3);
    }

    #[test]
    fn median_handles_split_windows() {
        assert_eq!(reducers::median::<i32>().reduce(&[9, 1], &[5]), 5);
        assert_eq!(reducers::median::<i32>().reduce(&[9, 1], &[5, 3]), 3);
        assert_eq!(reducers::median::<i32>().reduce(&[], &[]), 0);
    }

    #[test]
    fn median_float_ignores_nan_values() {
        assert_eq!(reducers::median_f32().reduce(&[f32::NAN, 9.0, 1.0], &[5.0]), 5.0);
        assert!(reducers::median_f32().reduce(&[f32::NAN], &[f32::NAN]).is_nan());
        assert_eq!(reducers::median_f32().reduce(&[], &[]), 0.0);
        assert_eq!(reducers::median_f64().reduce(&[f64::NAN, 9.0, 1.0], &[5.0, 3.0]), 3.0);
        assert!(reducers::median_f64().reduce(&[f64::NAN], &[f64::NAN]).is_nan());
        assert_eq!(reducers::median_f64().reduce(&[], &[]), 0.0);
    }
}