Skip to main content

oxihuman_core/
sliding_window.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Fixed-size sliding window over f32 samples with stats.
6
7use std::collections::VecDeque;
8
9/// A fixed-capacity sliding window of f32 samples.
10#[allow(dead_code)]
11pub struct SlidingWindow {
12    capacity: usize,
13    samples: VecDeque<f32>,
14    sum: f32,
15    min_val: f32,
16    max_val: f32,
17}
18
19#[allow(dead_code)]
20impl SlidingWindow {
21    pub fn new(capacity: usize) -> Self {
22        let cap = capacity.max(1);
23        Self {
24            capacity: cap,
25            samples: VecDeque::with_capacity(cap),
26            sum: 0.0,
27            min_val: f32::INFINITY,
28            max_val: f32::NEG_INFINITY,
29        }
30    }
31
32    /// Push a sample, dropping the oldest if at capacity.
33    pub fn push(&mut self, value: f32) {
34        if self.samples.len() == self.capacity {
35            self.samples.pop_front();
36            // Recompute sum and extremes.
37            self.sum = self.samples.iter().sum();
38            self.min_val = self.samples.iter().cloned().fold(f32::INFINITY, f32::min);
39            self.max_val = self
40                .samples
41                .iter()
42                .cloned()
43                .fold(f32::NEG_INFINITY, f32::max);
44        }
45        self.samples.push_back(value);
46        self.sum += value;
47        self.min_val = self.min_val.min(value);
48        self.max_val = self.max_val.max(value);
49    }
50
51    pub fn mean(&self) -> f32 {
52        if self.samples.is_empty() {
53            0.0
54        } else {
55            self.sum / self.samples.len() as f32
56        }
57    }
58
59    pub fn sum(&self) -> f32 {
60        self.sum
61    }
62
63    pub fn min(&self) -> Option<f32> {
64        if self.samples.is_empty() {
65            None
66        } else {
67            Some(self.min_val)
68        }
69    }
70
71    pub fn max(&self) -> Option<f32> {
72        if self.samples.is_empty() {
73            None
74        } else {
75            Some(self.max_val)
76        }
77    }
78
79    pub fn len(&self) -> usize {
80        self.samples.len()
81    }
82
83    pub fn capacity(&self) -> usize {
84        self.capacity
85    }
86
87    pub fn is_empty(&self) -> bool {
88        self.samples.is_empty()
89    }
90
91    pub fn is_full(&self) -> bool {
92        self.samples.len() == self.capacity
93    }
94
95    pub fn as_slice(&self) -> Vec<f32> {
96        self.samples.iter().cloned().collect()
97    }
98
99    pub fn clear(&mut self) {
100        self.samples.clear();
101        self.sum = 0.0;
102        self.min_val = f32::INFINITY;
103        self.max_val = f32::NEG_INFINITY;
104    }
105
106    /// Variance of the window.
107    pub fn variance(&self) -> f32 {
108        if self.samples.len() < 2 {
109            return 0.0;
110        }
111        let m = self.mean();
112        self.samples.iter().map(|&x| (x - m) * (x - m)).sum::<f32>() / self.samples.len() as f32
113    }
114}
115
116impl Default for SlidingWindow {
117    fn default() -> Self {
118        Self::new(16)
119    }
120}
121
122pub fn new_sliding_window(capacity: usize) -> SlidingWindow {
123    SlidingWindow::new(capacity)
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129
130    #[test]
131    fn basic_push_and_mean() {
132        let mut w = new_sliding_window(4);
133        w.push(1.0);
134        w.push(2.0);
135        w.push(3.0);
136        w.push(4.0);
137        assert!((w.mean() - 2.5).abs() < 1e-5);
138    }
139
140    #[test]
141    fn drops_oldest_when_full() {
142        let mut w = new_sliding_window(3);
143        w.push(1.0);
144        w.push(2.0);
145        w.push(3.0);
146        w.push(4.0); // drops 1.0
147        assert_eq!(w.len(), 3);
148        assert!((w.sum() - 9.0).abs() < 1e-5);
149    }
150
151    #[test]
152    fn min_max() {
153        let mut w = new_sliding_window(5);
154        w.push(3.0);
155        w.push(1.0);
156        w.push(5.0);
157        assert_eq!(w.min(), Some(1.0));
158        assert_eq!(w.max(), Some(5.0));
159    }
160
161    #[test]
162    fn empty_window() {
163        let w = new_sliding_window(4);
164        assert!(w.is_empty());
165        assert_eq!(w.min(), None);
166        assert_eq!(w.max(), None);
167        assert!((w.mean()).abs() < 1e-6);
168    }
169
170    #[test]
171    fn is_full() {
172        let mut w = new_sliding_window(2);
173        w.push(1.0);
174        assert!(!w.is_full());
175        w.push(2.0);
176        assert!(w.is_full());
177    }
178
179    #[test]
180    fn clear() {
181        let mut w = new_sliding_window(4);
182        w.push(5.0);
183        w.clear();
184        assert!(w.is_empty());
185        assert!((w.sum()).abs() < 1e-6);
186    }
187
188    #[test]
189    fn variance_uniform() {
190        let mut w = new_sliding_window(4);
191        for _ in 0..4 {
192            w.push(2.0);
193        }
194        assert!(w.variance().abs() < 1e-5);
195    }
196
197    #[test]
198    fn capacity_constant() {
199        let w = new_sliding_window(8);
200        assert_eq!(w.capacity(), 8);
201    }
202
203    #[test]
204    fn as_slice_ordered() {
205        let mut w = new_sliding_window(3);
206        w.push(10.0);
207        w.push(20.0);
208        w.push(30.0);
209        assert_eq!(w.as_slice(), vec![10.0, 20.0, 30.0]);
210    }
211
212    #[test]
213    fn single_element_variance_zero() {
214        let mut w = new_sliding_window(5);
215        w.push(7.0);
216        assert!(w.variance().abs() < 1e-6);
217    }
218}