Skip to main content

high_roller/
rolling_max.rs

1use std::{collections::VecDeque, num::NonZeroUsize};
2
3// TODO: DOCS
4
5#[derive(Debug)]
6pub struct RollingMax<T> {
7    // Invariant: These are 1:1. They are conceptually a `(usize, T)` tuple,
8    // but split into two deques to avoid alignment padding when T is narrower
9    // than usize (e.g. u8/u16 on 64-bit targets).
10    deq: VecDeque<T>,
11    expires: VecDeque<usize>,
12
13    ct: usize,
14    cap: usize,
15}
16
17impl<T> RollingMax<T>
18where
19    T: PartialOrd,
20{
21    #[must_use]
22    pub fn new(capacity: NonZeroUsize) -> Self {
23        let cap: usize = capacity.into();
24        Self {
25            deq: VecDeque::with_capacity(cap),
26            expires: VecDeque::with_capacity(cap),
27            cap,
28            ct: 0,
29        }
30    }
31
32    pub fn push(&mut self, entry: T) {
33        self.ct = self.ct.wrapping_add(1);
34
35        while self
36            .expires
37            .front()
38            .is_some_and(|&exp| self.ct.wrapping_sub(exp) <= self.cap)
39        {
40            self.deq.pop_front();
41            self.expires.pop_front();
42        }
43
44        while self.deq.back().is_some_and(|tail| tail <= &entry) {
45            self.deq.pop_back();
46            self.expires.pop_back();
47        }
48
49        self.deq.push_back(entry);
50        self.expires.push_back(self.ct.wrapping_add(self.cap));
51    }
52
53    #[must_use]
54    pub fn max(&self) -> Option<&T> {
55        self.deq.front()
56    }
57}
58
59#[cfg(test)]
60mod tests {
61    use super::*;
62
63    fn nz(n: usize) -> NonZeroUsize {
64        NonZeroUsize::new(n).unwrap()
65    }
66
67    /// Push every value and collect the rolling max after each push.
68    fn maxes<T: PartialOrd + Copy>(vals: &[T], cap: usize) -> Vec<T> {
69        let mut rm = RollingMax::new(nz(cap));
70        vals.iter()
71            .map(|&v| {
72                rm.push(v);
73                *rm.max().unwrap()
74            })
75            .collect()
76    }
77
78    /// Verifies the zero-state guarantee: max must be None before any push.
79    #[test]
80    fn max_on_empty_is_none() {
81        let rm: RollingMax<i32> = RollingMax::new(nz(3));
82        assert_eq!(rm.max(), None);
83    }
84
85    /// A single push must always yield Some, regardless of window size.
86    #[test]
87    fn single_push_yields_some() {
88        let mut rm = RollingMax::new(nz(5));
89        rm.push(42i32);
90        assert_eq!(rm.max(), Some(&42));
91    }
92
93    /// Window=1: every element is its own maximum; exercises the path where
94    /// the entire deque is evicted on every push.
95    #[test]
96    fn window_of_one() {
97        assert_eq!(maxes(&[3, 1, 4, 1, 5], 1), vec![3, 1, 4, 1, 5]);
98    }
99
100    /// Window larger than the entire input: tracker never evicts, so the
101    /// running max is monotonically non-decreasing.
102    #[test]
103    fn window_larger_than_input() {
104        assert_eq!(maxes(&[2, 4, 1], 10), vec![2, 4, 4]);
105    }
106
107    /// Window exactly equal to input length: global max emerges only after
108    /// the last push.
109    #[test]
110    fn window_equals_input_length() {
111        assert_eq!(maxes(&[1, 3, 2, 5, 4], 5), vec![1, 3, 3, 5, 5]);
112    }
113
114    /// Core sliding-window case; this exact sequence caught the off-by-one
115    /// expiry bug where element `3` incorrectly survived into window [1,2,0].
116    #[test]
117    fn sliding_window_canonical() {
118        assert_eq!(maxes(&[1, 3, 1, 2, 0, 5], 3), vec![1, 3, 3, 3, 2, 5]);
119    }
120
121    /// Strictly increasing input: the monotone invariant discards every
122    /// predecessor, so the deque always holds exactly one element.
123    #[test]
124    fn strictly_increasing() {
125        assert_eq!(maxes(&[1, 2, 3, 4, 5], 3), vec![1, 2, 3, 4, 5]);
126    }
127
128    /// Strictly decreasing input: the oldest value leads the deque and must
129    /// survive until it expires, then yield to the next oldest.
130    #[test]
131    fn strictly_decreasing() {
132        assert_eq!(maxes(&[5, 4, 3, 2, 1], 3), vec![5, 5, 5, 4, 3]);
133    }
134
135    /// All-equal input: equal elements are pruned from the back (`<=`), so
136    /// the deque stays bounded and does not grow without limit.
137    #[test]
138    fn all_equal() {
139        assert_eq!(maxes(&[7i32; 6], 3), vec![7; 6]);
140    }
141
142    /// Negative values: ensures no implicit assumption about sign or zero.
143    #[test]
144    fn negative_values() {
145        assert_eq!(maxes(&[-3, -1, -4, -1, -5], 2), vec![-3, -1, -1, -1, -1]);
146    }
147
148    /// Float input: exercises the PartialOrd bound on a non-Ord type.
149    #[test]
150    fn float_values() {
151        assert_eq!(
152            maxes(&[1.0f64, 3.0, 2.0, 5.0, 4.0], 2),
153            vec![1.0, 3.0, 3.0, 5.0, 5.0]
154        );
155    }
156
157    /// The maximum must survive exactly `cap` pushes and be gone on the next;
158    /// guards against off-by-one errors at the expiry boundary.
159    #[test]
160    fn max_expires_at_exact_boundary() {
161        let mut rm = RollingMax::new(nz(3));
162        rm.push(99i32);
163        rm.push(1);
164        rm.push(1);
165        assert_eq!(rm.max(), Some(&99)); // 99 still in [99, 1, 1]
166        rm.push(1);
167        assert_eq!(rm.max(), Some(&1)); // 99 evicted; window is now [1, 1, 1]
168    }
169
170    /// Exercises the `usize` counter wrap-around: pre-seeds `ct` so that
171    /// expiry values cross the `usize::MAX → 0` boundary, verifying that the
172    /// wrapping arithmetic correctly evicts and retains elements.
173    #[test]
174    fn expiry_counter_wrapping() {
175        let cap = 3;
176        let mut rm = RollingMax {
177            deq: VecDeque::with_capacity(cap),
178            expires: VecDeque::with_capacity(cap),
179            cap: 3,
180            ct: usize::MAX - 3,
181        };
182
183        rm.push(10); // ct = usize::MAX-2, exp = 0  (wraps)
184        rm.push(5); // ct = usize::MAX-1, exp = 1  (wraps)
185        rm.push(8); // ct = usize::MAX,   exp = 2  (wraps)
186        assert_eq!(rm.max(), Some(&10)); // window = [10, 5, 8]
187
188        rm.push(6); // ct = 0 (wrap). exp=0 matches ct → evicts 10. window=[5,8,6]
189        assert_eq!(rm.max(), Some(&8));
190
191        rm.push(7); // ct = 1. No expiry yet. Monotone pops 6. window=[8,6,7]
192        assert_eq!(rm.max(), Some(&8));
193
194        rm.push(9); // ct = 2. exp=2 matches ct → evicts 8. Monotone pops 7. window=[6,7,9]
195        assert_eq!(rm.max(), Some(&9));
196    }
197}