downsample 0.0.6

keep downsampled history of data over long period of time
Documentation
use crate::{
    Reducer,
    fixed_frequency::storage::{FixedStorage, FixedWriter, LevelMetadata},
};
use alloc::vec::Vec;
use core::num::NonZeroUsize;

#[derive(Clone, Copy)]
struct LevelInfo<T> {
    capacity: NonZeroUsize,
    aggregation_window: NonZeroUsize,
    reducer: Reducer<T>,
}

pub struct Builder<T> {
    raw_capacity: NonZeroUsize,
    levels:       Vec<LevelInfo<T>>,
    terminal:     Option<NonZeroUsize>,
}

impl<T> Builder<T> {
    /// raw_capacity: capacity of raw T data before any aggregation is done.
    pub fn new(raw_capacity: NonZeroUsize) -> Self {
        Self {
            raw_capacity,
            levels: Vec::new(),
            terminal: None,
        }
    }

    pub fn level(mut self, capacity: NonZeroUsize, aggregation_window: NonZeroUsize, reducer: Reducer<T>) -> Self {
        self.levels.push(LevelInfo {
            capacity,
            aggregation_window,
            reducer,
        });
        self
    }

    /// Configures a hidden aggregation window on the final level.
    ///
    /// When this terminal window fills, `FixedWriter::push_overflow` returns it
    /// as slices instead of reducing it inside this writer. This lets callers
    /// apply one or more reducers externally, for example to feed multiple
    /// child storages.
    pub fn terminal(mut self, aggregation_window: NonZeroUsize) -> Self {
        self.terminal = Some(aggregation_window);
        self
    }

    pub fn build(self) -> FixedWriter<T>
    where
        T: Default,
    {
        let mut levels = Vec::with_capacity(self.levels.len() + 1);
        let mut reducers = Vec::with_capacity(self.levels.len());
        let mut start = 0;
        let mut capacity = self.raw_capacity.get();

        for info in self.levels {
            let aggregation_window = info.aggregation_window.get();
            let storage_capacity = capacity + aggregation_window;
            let end = start + storage_capacity;
            levels.push(LevelMetadata::new(start, end, aggregation_window));
            reducers.push(info.reducer);
            start = end;
            capacity = info.capacity.get();
        }

        let terminal_window = self.terminal.map(NonZeroUsize::get).unwrap_or(0);
        let end = start + capacity + terminal_window;
        levels.push(LevelMetadata::new(start, end, terminal_window));

        let mut data = Vec::with_capacity(end);
        for _ in 0..end {
            data.push(T::default());
        }

        FixedWriter {
            storage: FixedStorage { data, levels },
            reducers,
        }
    }
}

#[cfg(all(test, feature = "reducers"))]
mod tests {
    use super::*;
    use crate::reducers;

    const fn nz(value: usize) -> NonZeroUsize { NonZeroUsize::new(value).expect("expected non-zero value") }

    #[test]
    fn construct_simple() {
        // 0s - 1s : 100Hz
        // 1s - 1m : 1Hz
        // 1m - 1h : 1/60 Hz
        // 1h - 1d : 1/3600 Hz
        let s: FixedWriter<f32> = Builder::new(nz(100))
            .level(nz(59), nz(100), reducers::average_f32())
            .level(nz(59), nz(60), reducers::average_f32())
            .level(nz(23), nz(60), reducers::average_f32())
            .build();

        assert_eq!(s.storage.level_count(), 4);

        let raw = s.storage.level_metadata(0).unwrap();
        assert_eq!(raw.capacity(), 100);
        assert_eq!(raw.aggregation_window(), 100);
        assert_eq!(raw.storage_capacity(), 200);

        let seconds = s.storage.level_metadata(1).unwrap();
        assert_eq!(seconds.capacity(), 59);
        assert_eq!(seconds.aggregation_window(), 60);
        assert_eq!(seconds.storage_capacity(), 119);

        let minutes = s.storage.level_metadata(2).unwrap();
        assert_eq!(minutes.capacity(), 59);
        assert_eq!(minutes.aggregation_window(), 60);
        assert_eq!(minutes.storage_capacity(), 119);

        let hours = s.storage.level_metadata(3).unwrap();
        assert_eq!(hours.capacity(), 23);
        assert_eq!(hours.aggregation_window(), 0);
        assert_eq!(hours.storage_capacity(), 23);
    }

    #[test]
    fn construct_with_terminal_aggregation() {
        let s: FixedWriter<u32> = Builder::new(nz(4))
            .level(nz(3), nz(2), reducers::average_u32())
            .terminal(nz(3))
            .build();

        assert_eq!(s.storage.level_count(), 2);

        let raw = s.storage.level_metadata(0).unwrap();
        assert_eq!(raw.capacity(), 4);
        assert_eq!(raw.aggregation_window(), 2);
        assert_eq!(raw.storage_capacity(), 6);

        let terminal = s.storage.level_metadata(1).unwrap();
        assert_eq!(terminal.capacity(), 3);
        assert_eq!(terminal.aggregation_window(), 3);
        assert_eq!(terminal.storage_capacity(), 6);
    }

    #[test]
    fn construct_use_simple() {
        // 0s - 1s : 100Hz
        // 1s - 1m : 1Hz
        // 1m - 1h : 1/60 Hz
        // 1h - 1d : 1/3600 Hz
        let mut s: FixedWriter<f32> = Builder::new(nz(100))
            .level(nz(59), nz(100), reducers::average_f32())
            .level(nz(59), nz(60), reducers::average_f32())
            .level(nz(23), nz(60), reducers::average_f32())
            .build();

        for _ in 0..1000 {
            s.push(0.0);
        }
    }
}