use crate::tools::time::{DurationMillis, TimeMillis};
#[derive(Debug, Clone)]
pub struct DecayingCounter {
value: f64,
last_update_millis: TimeMillis,
time_constant_millis: f64,
}
impl DecayingCounter {
pub fn new(time_constant: DurationMillis) -> Self {
Self {
value: 0.0,
last_update_millis: TimeMillis::zero(),
time_constant_millis: time_constant.0 as f64,
}
}
pub fn record(&mut self, now: TimeMillis, count: u64) {
self.value = self.estimate(now) + count as f64;
self.last_update_millis = now;
}
pub fn estimate(&self, now: TimeMillis) -> f64 {
let elapsed_millis = (now.0 - self.last_update_millis.0).max(0) as f64;
self.value * (-elapsed_millis / self.time_constant_millis).exp()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tools::time::{MILLIS_IN_HOUR, MILLIS_IN_MINUTE};
#[test]
fn zero_before_any_record() {
let counter = DecayingCounter::new(MILLIS_IN_HOUR);
assert_eq!(counter.estimate(TimeMillis::zero()), 0.0);
assert_eq!(counter.estimate(TimeMillis(MILLIS_IN_HOUR.0)), 0.0);
}
#[test]
fn single_record_reads_count_immediately_then_decays() {
let mut counter = DecayingCounter::new(MILLIS_IN_HOUR);
let t0 = TimeMillis(MILLIS_IN_HOUR.0); counter.record(t0, 100);
assert!((counter.estimate(t0) - 100.0).abs() < 1e-9, "got {}", counter.estimate(t0));
let after_one_tau = TimeMillis(t0.0 + MILLIS_IN_HOUR.0);
let expected = 100.0 / std::f64::consts::E;
assert!((counter.estimate(after_one_tau) - expected).abs() < 1e-6, "got {}", counter.estimate(after_one_tau));
let much_later = TimeMillis(t0.0 + MILLIS_IN_HOUR.0 * 100);
assert!(counter.estimate(much_later) < 1e-6, "got {}", counter.estimate(much_later));
}
#[test]
fn steady_cadence_converges_near_rate_times_tau() {
let mut counter = DecayingCounter::new(MILLIS_IN_HOUR);
let mut now = TimeMillis::zero();
for _ in 0..10_000 {
now = TimeMillis(now.0 + MILLIS_IN_MINUTE.0);
counter.record(now, 1);
}
let estimate = counter.estimate(now);
assert!((estimate - 60.0).abs() < 1.0, "expected ~60, got {}", estimate);
}
#[test]
fn estimate_is_non_mutating() {
let mut counter = DecayingCounter::new(MILLIS_IN_HOUR);
counter.record(TimeMillis(MILLIS_IN_HOUR.0), 50);
let later = TimeMillis(MILLIS_IN_HOUR.0 * 3);
let first = counter.estimate(later);
let second = counter.estimate(later);
assert_eq!(first, second);
}
#[test]
fn backwards_now_does_not_inflate() {
let mut counter = DecayingCounter::new(MILLIS_IN_HOUR);
let t0 = TimeMillis(MILLIS_IN_HOUR.0 * 5);
counter.record(t0, 100);
let earlier = TimeMillis(t0.0 - MILLIS_IN_HOUR.0);
assert!(counter.estimate(earlier) <= 100.0 + 1e-9, "got {}", counter.estimate(earlier));
assert!((counter.estimate(earlier) - 100.0).abs() < 1e-9, "got {}", counter.estimate(earlier));
}
}