1use std::collections::VecDeque;
11
12use crate::error::{RillError, ensure_finite};
13use crate::stats::variance::VarianceKind;
14use crate::traits::OnlineStatistic;
15
16#[derive(Debug, Clone)]
36#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
37pub struct RollingMean {
38 window: VecDeque<f64>,
39 capacity: usize,
40}
41
42impl RollingMean {
43 pub fn new(capacity: usize) -> Result<Self, RillError> {
47 if capacity == 0 {
48 return Err(RillError::InvalidWindowSize);
49 }
50 Ok(Self {
51 window: VecDeque::with_capacity(capacity),
52 capacity,
53 })
54 }
55
56 pub const fn capacity(&self) -> usize {
58 self.capacity
59 }
60
61 pub fn len(&self) -> usize {
63 self.window.len()
64 }
65
66 pub fn is_empty(&self) -> bool {
68 self.window.is_empty()
69 }
70
71 pub fn value(&self) -> Option<f64> {
73 if self.window.is_empty() {
74 None
75 } else {
76 let sum: f64 = self.window.iter().sum();
77 if !sum.is_finite() {
78 return None;
79 }
80 Some(sum / self.window.len() as f64)
81 }
82 }
83}
84
85impl OnlineStatistic for RollingMean {
86 fn update(&mut self, value: f64) -> Result<(), RillError> {
87 ensure_finite("value", value)?;
88 if self.window.len() == self.capacity {
89 self.window.pop_front();
90 }
91 self.window.push_back(value);
92 Ok(())
93 }
94
95 fn samples_seen(&self) -> u64 {
96 self.window.len() as u64
97 }
98
99 fn reset(&mut self) {
100 self.window.clear();
101 }
102}
103
104#[derive(Debug, Clone)]
109#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
110pub struct RollingVariance {
111 window: VecDeque<f64>,
112 capacity: usize,
113 kind: VarianceKind,
114}
115
116impl RollingVariance {
117 pub fn new(capacity: usize, kind: VarianceKind) -> Result<Self, RillError> {
121 if capacity == 0 {
122 return Err(RillError::InvalidWindowSize);
123 }
124 Ok(Self {
125 window: VecDeque::with_capacity(capacity),
126 capacity,
127 kind,
128 })
129 }
130
131 pub const fn capacity(&self) -> usize {
133 self.capacity
134 }
135
136 pub const fn kind(&self) -> VarianceKind {
138 self.kind
139 }
140
141 pub fn len(&self) -> usize {
143 self.window.len()
144 }
145
146 pub fn is_empty(&self) -> bool {
148 self.window.is_empty()
149 }
150
151 pub fn mean(&self) -> Option<f64> {
153 if self.window.is_empty() {
154 None
155 } else {
156 let sum: f64 = self.window.iter().sum();
157 if !sum.is_finite() {
158 return None;
159 }
160 Some(sum / self.window.len() as f64)
161 }
162 }
163
164 pub fn value(&self) -> Option<f64> {
166 let n = self.window.len();
167 if n == 0 {
168 return None;
169 }
170 let denom = match self.kind {
171 VarianceKind::Population => n,
172 VarianceKind::Sample => {
173 if n < 2 {
174 return None;
175 }
176 n - 1
177 }
178 };
179 let mean = self.mean()?;
180 let ss = self.window.iter().map(|x| (x - mean).powi(2)).sum::<f64>();
181 if !ss.is_finite() {
182 return None;
183 }
184 Some(ss / denom as f64)
185 }
186
187 pub fn std_dev(&self) -> Option<f64> {
189 self.value().map(|v| v.sqrt())
190 }
191}
192
193impl OnlineStatistic for RollingVariance {
194 fn update(&mut self, value: f64) -> Result<(), RillError> {
195 ensure_finite("value", value)?;
196 if self.window.len() == self.capacity {
197 self.window.pop_front();
198 }
199 self.window.push_back(value);
200 Ok(())
201 }
202
203 fn samples_seen(&self) -> u64 {
204 self.window.len() as u64
205 }
206
207 fn reset(&mut self) {
208 self.window.clear();
209 }
210}
211
212#[cfg(test)]
213mod tests {
214 use super::*;
215
216 #[test]
217 fn rolling_mean_basic() {
218 let mut rm = RollingMean::new(3).unwrap();
219 for x in [1.0, 2.0, 3.0] {
220 rm.update(x).unwrap();
221 }
222 assert!((rm.value().unwrap() - 2.0).abs() < 1e-12);
223 }
224
225 #[test]
226 fn rolling_mean_evicts_oldest() {
227 let mut rm = RollingMean::new(2).unwrap();
228 rm.update(10.0).unwrap();
229 rm.update(20.0).unwrap();
230 rm.update(30.0).unwrap();
231 assert!((rm.value().unwrap() - 25.0).abs() < 1e-12);
232 }
233
234 #[test]
235 fn rolling_mean_empty_returns_none() {
236 let rm = RollingMean::new(5).unwrap();
237 assert!(rm.value().is_none());
238 }
239
240 #[test]
241 fn rolling_mean_zero_capacity_rejected() {
242 assert!(matches!(
243 RollingMean::new(0),
244 Err(RillError::InvalidWindowSize)
245 ));
246 }
247
248 #[test]
249 fn rolling_variance_population() {
250 let mut rv = RollingVariance::new(4, VarianceKind::Population).unwrap();
251 for x in [1.0, 2.0, 3.0, 4.0] {
252 rv.update(x).unwrap();
253 }
254 assert!((rv.value().unwrap() - 1.25).abs() < 1e-12);
256 }
257
258 #[test]
259 fn rolling_variance_evicts_correctly() {
260 let mut rv = RollingVariance::new(3, VarianceKind::Population).unwrap();
261 for x in [1.0, 2.0, 3.0, 4.0] {
262 rv.update(x).unwrap();
263 }
264 assert!((rv.value().unwrap() - 2.0 / 3.0).abs() < 1e-12);
266 }
267
268 #[test]
269 fn rolling_variance_sample_insufficient() {
270 let mut rv = RollingVariance::new(5, VarianceKind::Sample).unwrap();
271 rv.update(1.0).unwrap();
272 assert!(rv.value().is_none());
273 }
274
275 #[test]
276 fn rolling_variance_zero_capacity_rejected() {
277 assert!(matches!(
278 RollingVariance::new(0, VarianceKind::Population),
279 Err(RillError::InvalidWindowSize)
280 ));
281 }
282}