use std::collections::VecDeque;
use crate::error::{RillError, ensure_finite};
use crate::stats::variance::VarianceKind;
use crate::traits::OnlineStatistic;
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct RollingMean {
window: VecDeque<f64>,
capacity: usize,
}
impl RollingMean {
pub fn new(capacity: usize) -> Result<Self, RillError> {
if capacity == 0 {
return Err(RillError::InvalidWindowSize);
}
Ok(Self {
window: VecDeque::with_capacity(capacity),
capacity,
})
}
pub const fn capacity(&self) -> usize {
self.capacity
}
pub fn len(&self) -> usize {
self.window.len()
}
pub fn is_empty(&self) -> bool {
self.window.is_empty()
}
pub fn value(&self) -> Option<f64> {
if self.window.is_empty() {
None
} else {
let sum: f64 = self.window.iter().sum();
Some(sum / self.window.len() as f64)
}
}
}
impl OnlineStatistic for RollingMean {
fn update(&mut self, value: f64) -> Result<(), RillError> {
ensure_finite("value", value)?;
if self.window.len() == self.capacity {
self.window.pop_front();
}
self.window.push_back(value);
Ok(())
}
fn samples_seen(&self) -> u64 {
self.window.len() as u64
}
fn reset(&mut self) {
self.window.clear();
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct RollingVariance {
window: VecDeque<f64>,
capacity: usize,
kind: VarianceKind,
}
impl RollingVariance {
pub fn new(capacity: usize, kind: VarianceKind) -> Result<Self, RillError> {
if capacity == 0 {
return Err(RillError::InvalidWindowSize);
}
Ok(Self {
window: VecDeque::with_capacity(capacity),
capacity,
kind,
})
}
pub const fn capacity(&self) -> usize {
self.capacity
}
pub const fn kind(&self) -> VarianceKind {
self.kind
}
pub fn len(&self) -> usize {
self.window.len()
}
pub fn is_empty(&self) -> bool {
self.window.is_empty()
}
pub fn mean(&self) -> Option<f64> {
if self.window.is_empty() {
None
} else {
let sum: f64 = self.window.iter().sum();
Some(sum / self.window.len() as f64)
}
}
pub fn value(&self) -> Option<f64> {
let n = self.window.len();
if n == 0 {
return None;
}
let denom = match self.kind {
VarianceKind::Population => n,
VarianceKind::Sample => {
if n < 2 {
return None;
}
n - 1
}
};
let mean = self.mean()?;
let ss = self.window.iter().map(|x| (x - mean).powi(2)).sum::<f64>();
Some(ss / denom as f64)
}
pub fn std_dev(&self) -> Option<f64> {
self.value().map(|v| v.sqrt())
}
}
impl OnlineStatistic for RollingVariance {
fn update(&mut self, value: f64) -> Result<(), RillError> {
ensure_finite("value", value)?;
if self.window.len() == self.capacity {
self.window.pop_front();
}
self.window.push_back(value);
Ok(())
}
fn samples_seen(&self) -> u64 {
self.window.len() as u64
}
fn reset(&mut self) {
self.window.clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rolling_mean_basic() {
let mut rm = RollingMean::new(3).unwrap();
for x in [1.0, 2.0, 3.0] {
rm.update(x).unwrap();
}
assert!((rm.value().unwrap() - 2.0).abs() < 1e-12);
}
#[test]
fn rolling_mean_evicts_oldest() {
let mut rm = RollingMean::new(2).unwrap();
rm.update(10.0).unwrap();
rm.update(20.0).unwrap();
rm.update(30.0).unwrap();
assert!((rm.value().unwrap() - 25.0).abs() < 1e-12);
}
#[test]
fn rolling_mean_empty_returns_none() {
let rm = RollingMean::new(5).unwrap();
assert!(rm.value().is_none());
}
#[test]
fn rolling_mean_zero_capacity_rejected() {
assert!(matches!(
RollingMean::new(0),
Err(RillError::InvalidWindowSize)
));
}
#[test]
fn rolling_variance_population() {
let mut rv = RollingVariance::new(4, VarianceKind::Population).unwrap();
for x in [1.0, 2.0, 3.0, 4.0] {
rv.update(x).unwrap();
}
assert!((rv.value().unwrap() - 1.25).abs() < 1e-12);
}
#[test]
fn rolling_variance_evicts_correctly() {
let mut rv = RollingVariance::new(3, VarianceKind::Population).unwrap();
for x in [1.0, 2.0, 3.0, 4.0] {
rv.update(x).unwrap();
}
assert!((rv.value().unwrap() - 2.0 / 3.0).abs() < 1e-12);
}
#[test]
fn rolling_variance_sample_insufficient() {
let mut rv = RollingVariance::new(5, VarianceKind::Sample).unwrap();
rv.update(1.0).unwrap();
assert!(rv.value().is_none());
}
#[test]
fn rolling_variance_zero_capacity_rejected() {
assert!(matches!(
RollingVariance::new(0, VarianceKind::Population),
Err(RillError::InvalidWindowSize)
));
}
}