1#[derive(Clone, Copy, Debug)]
38pub struct Window<const N: usize> {
39 samples: [f32; N],
40 capacity: usize,
41 len: usize,
42 next: usize,
43}
44
45impl<const N: usize> Window<N> {
46 pub fn new() -> Self {
52 Self::with_capacity(N)
53 }
54
55 pub fn with_capacity(capacity: usize) -> Self {
68 Self {
69 samples: [0.0; N],
70 capacity: capacity.min(N),
71 len: 0,
72 next: 0,
73 }
74 }
75
76 pub fn push(&mut self, reading: f32) {
82 if self.capacity == 0 || !reading.is_finite() {
83 return;
84 }
85 self.samples[self.next] = reading;
86 self.next = (self.next + 1) % self.capacity;
87 if self.len < self.capacity {
88 self.len += 1;
89 }
90 }
91
92 pub fn len(&self) -> usize {
94 self.len
95 }
96
97 pub fn is_empty(&self) -> bool {
99 self.len == 0
100 }
101
102 pub fn is_full(&self) -> bool {
104 self.len == self.capacity
105 }
106
107 pub fn capacity(&self) -> usize {
110 self.capacity
111 }
112
113 pub fn latest(&self) -> Option<f32> {
115 if self.len == 0 {
116 return None;
117 }
118 Some(self.samples[(self.next + self.capacity - 1) % self.capacity])
119 }
120
121 pub fn oldest(&self) -> Option<f32> {
123 if self.len == 0 {
124 return None;
125 }
126 let index = if self.is_full() { self.next } else { 0 };
127 Some(self.samples[index])
128 }
129
130 pub fn min(&self) -> Option<f32> {
132 self.samples[..self.len].iter().copied().reduce(f32::min)
133 }
134
135 pub fn max(&self) -> Option<f32> {
137 self.samples[..self.len].iter().copied().reduce(f32::max)
138 }
139
140 pub fn range(&self) -> Option<f32> {
142 Some(self.max()? - self.min()?)
143 }
144
145 pub fn mean(&self) -> Option<f32> {
147 if self.len == 0 {
148 return None;
149 }
150 let sum: f32 = self.samples[..self.len].iter().sum();
151 Some(sum / self.len as f32)
152 }
153
154 pub fn variance(&self) -> Option<f32> {
160 let mean = self.mean()?;
161 let sum_squared: f32 = self.samples[..self.len]
162 .iter()
163 .map(|reading| {
164 let deviation = reading - mean;
165 deviation * deviation
166 })
167 .sum();
168 Some(sum_squared / self.len as f32)
169 }
170}
171
172impl<const N: usize> Default for Window<N> {
173 fn default() -> Self {
174 Self::new()
175 }
176}
177
178#[cfg(test)]
179mod tests {
180 use super::*;
181
182 fn approx(a: f32, b: f32) -> bool {
183 (a - b).abs() < 1e-4
184 }
185
186 #[test]
187 fn an_empty_window_has_no_statistics() {
188 let window = Window::<3>::new();
189 assert!(window.is_empty());
190 assert_eq!(window.len(), 0);
191 assert_eq!(window.min(), None);
192 assert_eq!(window.max(), None);
193 assert_eq!(window.mean(), None);
194 assert_eq!(window.range(), None);
195 assert_eq!(window.variance(), None);
196 assert_eq!(window.latest(), None);
197 assert_eq!(window.oldest(), None);
198 }
199
200 #[test]
201 fn fills_to_capacity_then_stays_full() {
202 let mut window = Window::<3>::new();
203 window.push(1.0);
204 assert_eq!(window.len(), 1);
205 assert!(!window.is_full());
206 window.push(2.0);
207 window.push(3.0);
208 assert!(window.is_full());
209 window.push(4.0);
210 assert_eq!(window.len(), 3);
211 assert_eq!(window.capacity(), 3);
212 }
213
214 #[test]
215 fn reports_spread_over_the_held_readings() {
216 let mut window = Window::<4>::new();
217 for reading in [40.0, 42.0, 38.0, 41.0] {
218 window.push(reading);
219 }
220 assert_eq!(window.min(), Some(38.0));
221 assert_eq!(window.max(), Some(42.0));
222 assert_eq!(window.range(), Some(4.0));
223 assert!(approx(window.mean().unwrap(), 40.25));
224 }
225
226 #[test]
227 fn variance_matches_the_hand_computed_value() {
228 let mut window = Window::<3>::new();
230 for reading in [2.0, 4.0, 6.0] {
231 window.push(reading);
232 }
233 assert!(approx(window.variance().unwrap(), 8.0 / 3.0));
234 }
235
236 #[test]
237 fn the_oldest_reading_is_evicted_when_full() {
238 let mut window = Window::<3>::new();
239 for reading in [10.0, 20.0, 30.0, 40.0] {
240 window.push(reading);
241 }
242 assert_eq!(window.oldest(), Some(20.0));
244 assert_eq!(window.latest(), Some(40.0));
245 assert_eq!(window.min(), Some(20.0));
246 assert_eq!(window.max(), Some(40.0));
247 }
248
249 #[test]
250 fn a_smaller_capacity_keeps_fewer_readings() {
251 let mut window = Window::<32>::with_capacity(3);
252 for reading in [10.0, 20.0, 30.0, 40.0] {
253 window.push(reading);
254 }
255 assert_eq!(window.capacity(), 3);
256 assert!(window.is_full());
257 assert_eq!(window.oldest(), Some(20.0));
258 assert_eq!(window.latest(), Some(40.0));
259 assert_eq!(Window::<4>::with_capacity(9).capacity(), 4);
260 }
261
262 #[test]
263 fn a_reading_that_is_not_a_number_is_not_kept() {
264 let mut window = Window::<4>::new();
265 window.push(2.0);
266 window.push(f32::NAN);
267 window.push(f32::INFINITY);
268 window.push(4.0);
269 assert_eq!(window.len(), 2);
270 assert_eq!(window.mean(), Some(3.0));
271 assert_eq!(window.max(), Some(4.0));
272 }
273
274 #[test]
275 fn latest_and_oldest_track_before_the_window_fills() {
276 let mut window = Window::<5>::new();
277 window.push(7.0);
278 assert_eq!(window.latest(), Some(7.0));
279 assert_eq!(window.oldest(), Some(7.0));
280 window.push(9.0);
281 assert_eq!(window.latest(), Some(9.0));
282 assert_eq!(window.oldest(), Some(7.0));
283 }
284}