use std::{collections::HashMap, hash::Hash, time::Duration};
use crate::Timestamp;
#[derive(Debug)]
pub struct Ewma<K: Hash + Eq> {
alpha: f64,
entries: HashMap<K, (f64, Timestamp)>,
}
impl<K: Hash + Eq + Clone> Ewma<K> {
pub fn new(alpha: f64) -> Self {
assert!(alpha > 0.0 && alpha <= 1.0, "alpha must be in (0, 1]");
Self {
alpha,
entries: HashMap::new(),
}
}
pub fn record(&mut self, key: K, sample: f64) -> f64 {
self.record_at(key, sample, Timestamp::default())
}
pub fn record_at(&mut self, key: K, sample: f64, now: Timestamp) -> f64 {
let entry = self.entries.entry(key).or_insert((sample, now));
entry.1 = now;
if entry.0.is_nan() {
entry.0 = sample;
} else {
entry.0 = self.alpha * sample + (1.0 - self.alpha) * entry.0;
}
entry.0
}
pub fn get(&self, key: &K) -> Option<f64> {
self.entries.get(key).map(|(v, _)| *v)
}
pub fn iter(&self) -> impl Iterator<Item = (&K, f64)> {
self.entries.iter().map(|(k, (v, _))| (k, *v))
}
pub fn evict_stale(&mut self, now: Timestamp, ttl: Duration) {
let now_dur = now.to_duration();
self.entries
.retain(|_, (_, last_ts)| now_dur.saturating_sub(last_ts.to_duration()) <= ttl);
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
}
impl<K: Hash + Eq + Clone> crate::correlate::Mergeable for Ewma<K> {
fn merge(&mut self, other: Self) {
assert_eq!(
self.alpha, other.alpha,
"Ewma::merge requires matching alpha",
);
for (k, (v_other, ts_other)) in other.entries {
match self.entries.get_mut(&k) {
Some((v_self, ts_self)) => {
*v_self = 0.5 * (*v_self + v_other);
if ts_other > *ts_self {
*ts_self = ts_other;
}
}
None => {
self.entries.insert(k, (v_other, ts_other));
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn alpha_one_returns_last_sample() {
let mut e: Ewma<u32> = Ewma::new(1.0);
e.record(1, 10.0);
e.record(1, 5.0);
let v = e.get(&1).unwrap();
assert!((v - 5.0).abs() < 1e-9);
}
#[test]
fn alpha_half_averages_last_two() {
let mut e: Ewma<u32> = Ewma::new(0.5);
e.record(1, 10.0);
e.record(1, 20.0);
let v = e.get(&1).unwrap();
assert!((v - 15.0).abs() < 1e-9);
}
#[test]
fn per_key_isolation() {
let mut e: Ewma<u32> = Ewma::new(1.0);
e.record(1, 10.0);
e.record(2, 20.0);
assert_eq!(e.get(&1).unwrap(), 10.0);
assert_eq!(e.get(&2).unwrap(), 20.0);
}
#[test]
fn evict_stale_drops_old_entries() {
let mut e: Ewma<u32> = Ewma::new(0.5);
e.record_at(1, 1.0, Timestamp::new(0, 0));
e.record_at(2, 2.0, Timestamp::new(100, 0));
e.evict_stale(Timestamp::new(100, 0), Duration::from_secs(10));
assert!(e.get(&1).is_none());
assert!(e.get(&2).is_some());
}
}