Skip to main content

wickra_embed_core/
ring.rs

1//! A fixed-capacity ring buffer of `f64`, allocation-free.
2//!
3//! This is the no-alloc replacement for the `Box<[f64]>` window `wickra-core`'s
4//! SMA uses. It preserves the two properties byte-parity depends on: [`push`]
5//! returns the evicted oldest value in the same order the std code subtracts it
6//! from the rolling sum, and [`chrono_sum`] folds oldest-to-newest exactly as
7//! the std reseed does.
8//!
9//! [`push`]: Ring::push
10//! [`chrono_sum`]: Ring::chrono_sum
11
12/// A ring buffer holding up to `N` `f64` values.
13#[derive(Debug, Clone)]
14pub struct Ring<const N: usize> {
15    buf: [f64; N],
16    /// Index of the next slot to write — also the oldest element once full.
17    head: usize,
18    /// Number of slots filled, saturating at `N`.
19    count: usize,
20}
21
22impl<const N: usize> Ring<N> {
23    /// A new, empty ring.
24    #[must_use]
25    pub const fn new() -> Self {
26        Self {
27            buf: [0.0; N],
28            head: 0,
29            count: 0,
30        }
31    }
32
33    /// Whether the ring holds `N` values.
34    #[must_use]
35    pub const fn is_full(&self) -> bool {
36        self.count == N
37    }
38
39    /// Number of values currently held.
40    #[must_use]
41    pub const fn len(&self) -> usize {
42        self.count
43    }
44
45    /// Whether the ring holds no values.
46    #[must_use]
47    pub const fn is_empty(&self) -> bool {
48        self.count == 0
49    }
50
51    /// Push `x`, overwriting the oldest slot once full.
52    ///
53    /// Returns the evicted oldest value if the ring was already full, or `None`
54    /// while still filling. The caller (SMA) subtracts the returned value from
55    /// its rolling sum *before* adding `x`, matching the std subtract-then-add
56    /// order.
57    pub fn push(&mut self, x: f64) -> Option<f64> {
58        let evicted = if self.count == N {
59            Some(self.buf[self.head])
60        } else {
61            self.count += 1;
62            None
63        };
64        self.buf[self.head] = x;
65        self.head += 1;
66        if self.head == N {
67            self.head = 0;
68        }
69        evicted
70    }
71
72    /// Sum the held values oldest-to-newest.
73    ///
74    /// When full this walks `buf[head..]` then `buf[..head]` — the exact
75    /// chronological order (and thus the exact fold, starting from `0.0`) that
76    /// `wickra-core` reseeds its rolling sum with, so the reseed is bit-for-bit
77    /// identical.
78    #[must_use]
79    pub fn chrono_sum(&self) -> f64 {
80        let mut sum = 0.0;
81        let mut idx = self.head;
82        let mut seen = 0;
83        while seen < self.count {
84            sum += self.buf[idx];
85            idx += 1;
86            if idx == N {
87                idx = 0;
88            }
89            seen += 1;
90        }
91        sum
92    }
93}
94
95impl<const N: usize> Default for Ring<N> {
96    fn default() -> Self {
97        Self::new()
98    }
99}
100
101#[cfg(test)]
102mod tests {
103    use super::Ring;
104
105    #[test]
106    fn fills_then_evicts_oldest() {
107        let mut ring = Ring::<3>::new();
108        assert_eq!(ring.push(1.0), None);
109        assert_eq!(ring.push(2.0), None);
110        assert!(!ring.is_full());
111        assert_eq!(ring.push(3.0), None);
112        assert!(ring.is_full());
113        // Now full: pushing evicts the oldest (1.0, then 2.0, ...).
114        assert_eq!(ring.push(4.0), Some(1.0));
115        assert_eq!(ring.push(5.0), Some(2.0));
116    }
117
118    #[test]
119    fn chrono_sum_is_oldest_to_newest_after_wraparound() {
120        let mut ring = Ring::<3>::new();
121        for x in [1.0, 2.0, 3.0, 4.0, 5.0] {
122            ring.push(x);
123        }
124        // Holds 3,4,5; chronological fold 0.0 + 3 + 4 + 5.
125        assert_eq!(ring.chrono_sum().to_bits(), (3.0_f64 + 4.0 + 5.0).to_bits());
126        assert_eq!(ring.len(), 3);
127    }
128}