running_buffer 0.1.0

data types for keeping track of changing values, allowing analysis in trends and histories.
Documentation
use crate::RunningBuffer;
use core::clone::Clone;
use core::default::Default;
use core::ops::Add;

/// A struct to keep a record of a changing variable:
/// Keeps `N_MOST_RECENT` exact previous values.
/// For older entries, keeps a total as `CUM`, which together with the total amount of tests can be
/// used to reconstruct an average.
/// The `BUF` type is the type that is kept inside a buffer; it has multiple instances, so it's
/// worth making it a little smaller, such as u16
/// The `CUM` type contains the sum of all of them, so you might want to have a bigger type, such
/// as u64.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug)]
pub struct History<T, CUM, const N_MOST_RECENT: usize>
where
    T: Default,
{
    pub recent: RunningBuffer<T, N_MOST_RECENT>,

    /// Sum of all history values together. Used to get an average value.
    /// Already includes the entries in most_recent.
    /// Larger type to avoid overflows.
    total_historic: CUM,

    // This needs to be saved in here, because speed can be checked unsupervised (keylogging),
    // while errors cannot.
    /// How many numbers have been summed together to get the total_historic. Used to get an
    /// average value.
    total_tests: u32,
}

impl<T, CUM, const N_MOST_RECENT: usize> Default for History<T, CUM, N_MOST_RECENT>
where
    T: Default,
    CUM: Default,
{
    fn default() -> Self {
        Self {
            recent: Default::default(),
            total_historic: Default::default(),
            total_tests: Default::default(),
        }
    }
}

impl<T, CUM, const N_MOST_RECENT: usize> History<T, CUM, N_MOST_RECENT>
where
    CUM: Clone + Add<Output = CUM>,
    T: From<CUM> + Default,
{
    /// Adds new trial, updating the total.
    /// For types that implement converting from the cumulative type → buffer type.
    pub fn push_in_cum_type(&mut self, val: CUM) {
        let as_buf = T::from(val.clone());
        self.recent.push(as_buf);
        self.total_tests += 1;
        let prev_sum = self.total_historic.clone();
        self.total_historic = prev_sum + val;
    }
}

impl<T, CUM, const N_MOST_RECENT: usize> History<T, CUM, N_MOST_RECENT>
where
    CUM: Clone + Add<Output = CUM> + From<T>,
    T: Clone + Default,
{
    /// Adds new entry, updating the total.
    /// For types that implement converting from the buffer → cumulative type.
    pub fn push(&mut self, val: T) {
        let as_cum = CUM::from(val.clone());
        self.recent.push(val);
        self.total_tests += 1;
        let prev_sum = self.total_historic.clone();
        self.total_historic = prev_sum + as_cum;
    }

    /// Consuming adder for [`History::push`]
    pub fn pushed(mut self, val: T) -> Self {
        self.push(val);
        self
    }
}

// impl<T, const N_MOST_RECENT: usize> HistoricScore<T, T, N_MOST_RECENT>
// where
//     T: std::clone::Clone + std::ops::Add<Output = T>,
// {
//     /// Adding a new trial for types
//     /// For if the buffer type and cumulative type are the same.
//     pub fn add_new_trial(&mut self, new_trial: T) {
//         self.most_recent.add_new(new_trial.clone());
//         self.total_tests += 1;
//         let prev_sum = self.total_historic.clone();
//         self.total_historic = prev_sum + new_trial;
//     }
// }

impl<T, CUM, const N: usize> History<T, CUM, N>
where
    CUM: Default,
    T: Default,
{
    pub fn new() -> History<T, CUM, N> {
        History {
            recent: Default::default(),
            total_historic: Default::default(),
            total_tests: 0,
        }
    }
}

impl<T, CUM, const N: usize> History<T, CUM, N>
where
    CUM: Clone + Add<Output = CUM>,
    T: Default,
{
    /// Considers the RHS to be newer, and adds all of them to the LHS.
    pub fn joined(self, rhs: History<T, CUM, N>) -> History<T, CUM, N> {
        let total_historic = self.total_historic + rhs.total_historic;
        let total_tests = self.total_tests + rhs.total_tests;

        let older_count = self.recent.amount_of_entries() as usize;
        let rhs_count = rhs.recent.amount_of_entries() as usize;
        let mut lhs = self.recent.as_array();
        let mut newer = rhs.recent.as_array();

        // Number of leftover slots after placing rhs entries
        let leftover = N.saturating_sub(rhs_count);

        // Number of entries from self to keep (up to leftover)
        let to_keep = leftover.min(older_count);

        // NOTE: This can be done even more efficiently, as the values from the rhs are never
        // read again: They can just be taken. This requires an unsafe block though.
        // So for now, swap is also OK.

        // Step 1: shift current entries that will still be relevant using memswap.
        lhs.rotate_right(rhs_count);

        // Step 2: move entries from rhs into the front
        for i in 0..rhs_count {
            core::mem::swap(&mut lhs[i], &mut newer[i]);
        }

        // 3. Update n_entries to combined amount
        let n_entries = core::cmp::min(rhs_count + to_keep, N) as u16;
        Self {
            recent: RunningBuffer::new_unchecked(lhs, n_entries),
            total_historic,
            total_tests,
        }
    }

    // NOTE: Cannot easily make a .append() function because then you'd have to move out of self.
}

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

    #[test]
    fn push() {
        let mut s = History::<u32, u64, 2>::new();
        assert_eq!(s.recent.n_ago(0), None);
        assert_eq!(s.total_tests, 0);
        assert_eq!(s.total_historic, 0);

        s.push(100);
        assert_eq!(s.recent.n_ago(0).unwrap(), &100);
        assert_eq!(s.total_tests, 1);
        assert_eq!(s.total_historic, 100);

        s.push(101);
        assert_eq!(s.recent.n_ago(0).unwrap(), &101);
        assert_eq!(s.recent.n_ago(1).unwrap(), &100);
        assert_eq!(s.total_tests, 2);
        assert_eq!(s.total_historic, 201);

        // Only keeps 2 most recent, other only as cumulative.
        s.push(102);
        assert_eq!(s.recent.n_ago(0).unwrap(), &102);
        assert_eq!(s.recent.n_ago(1).unwrap(), &101);
        assert_eq!(s.recent.n_ago(2), None);
        assert_eq!(s.total_tests, 3);
        assert_eq!(s.total_historic, 303);
    }

    #[test]
    fn joined() {
        // Test where the total amount of things does still not fill up the complete buffer.
        let older = History::<u32, u64, 6>::new()
            .pushed(100)
            .pushed(101)
            .pushed(102);
        let newer = History::<u32, u64, 6>::new().pushed(103).pushed(104);
        let appended = older.joined(newer);
        let series = appended.clone().recent;
        assert_eq!(series.n_ago(0).unwrap(), &104);
        assert_eq!(series.n_ago(1).unwrap(), &103);
        assert_eq!(series.n_ago(2).unwrap(), &102);
        assert_eq!(series.n_ago(3).unwrap(), &101);
        assert_eq!(series.n_ago(4).unwrap(), &100);
        assert_eq!(series.n_ago(5), None);

        // Test where the total amount of things fills up the recent buffer.
        let older = History::<u32, u64, 3>::new()
            .pushed(100)
            .pushed(101)
            .pushed(102);
        let older_cum = older.total_historic;
        let older_tests = older.total_tests;
        let newer = History::<u32, u64, 3>::new().pushed(103).pushed(104);
        let newer_cum = newer.total_historic;
        let newer_tests = newer.total_tests;

        let appended = older.joined(newer);
        let series = appended.clone().recent;
        assert_eq!(series.n_ago(0).unwrap(), &104);
        assert_eq!(series.n_ago(1).unwrap(), &103);
        assert_eq!(series.n_ago(2).unwrap(), &102);
        assert_eq!(appended.total_historic, older_cum + newer_cum);
        assert_eq!(appended.total_tests, older_tests + newer_tests);
    }
}