vector_common/stats/
time_ewma.rs1use std::time::Instant;
2
3#[derive(Clone, Copy, Debug)]
4struct State {
5 average: f64,
6 point: f64,
7 reference: Instant,
8}
9
10#[derive(Clone, Copy, Debug)]
17pub struct TimeEwma {
18 state: Option<State>,
19 half_life_seconds: f64,
20}
21
22impl TimeEwma {
23 #[must_use]
24 pub const fn new(half_life_seconds: f64) -> Self {
25 Self {
26 state: None,
27 half_life_seconds,
28 }
29 }
30
31 #[must_use]
32 pub fn average(&self) -> Option<f64> {
33 self.state.map(|state| state.average)
34 }
35
36 pub fn update(&mut self, point: f64, reference: Instant) -> f64 {
41 let average = match self.state {
42 None => point,
43 Some(state) => {
44 if let Some(duration) = reference.checked_duration_since(state.reference) {
45 let k = (-duration.as_secs_f64() / self.half_life_seconds).exp2();
46 k * state.average + (1.0 - k) * state.point
49 } else {
50 state.average
51 }
52 }
53 };
54 self.state = Some(State {
55 average,
56 point,
57 reference,
58 });
59 average
60 }
61}
62
63#[cfg(test)]
64mod tests {
65 use super::TimeEwma;
66 use std::time::{Duration, Instant};
67
68 #[test]
69 #[expect(clippy::float_cmp, reason = "exact values for this test")]
70 fn time_ewma_uses_previous_point_duration() {
71 let mut ewma = TimeEwma::new(1.0);
72 let t0 = Instant::now();
73 let t1 = t0 + Duration::from_secs(1);
74 let t2 = t1 + Duration::from_secs(1);
75
76 assert_eq!(ewma.average(), None);
77 assert_eq!(ewma.update(0.0, t0), 0.0);
78 assert_eq!(ewma.average(), Some(0.0));
79 assert_eq!(ewma.update(10.0, t1), 0.0);
80 assert_eq!(ewma.average(), Some(0.0));
81 assert_eq!(ewma.update(10.0, t2), 5.0);
82 assert_eq!(ewma.average(), Some(5.0));
83 }
84}