rust 49 lines · 8 steps

A time-decayed moving average in Rust

An exponentially weighted moving average whose smoothing factor adapts to the real time elapsed between samples.

Explained by highlit
1use std::time::{Duration, Instant};
2 
3pub struct Ewma {
4 alpha: f64,
5 value: Option<f64>,
6 last_update: Option<Instant>,
7 half_life: Duration,
8}
9 
10impl Ewma {
11 pub fn new(half_life: Duration) -> Self {
12 Ewma {
13 alpha: 0.0,
14 value: None,
15 last_update: None,
16 half_life,
17 }
18 }
19 
20 pub fn observe(&mut self, sample: f64) {
21 self.observe_at(sample, Instant::now());
22 }
23 
24 pub fn observe_at(&mut self, sample: f64, now: Instant) {
25 match (self.value, self.last_update) {
26 (Some(prev), Some(last)) => {
27 let elapsed = now.saturating_duration_since(last).as_secs_f64();
28 let decay = (-elapsed * std::f64::consts::LN_2
29 / self.half_life.as_secs_f64())
30 .exp();
31 self.alpha = 1.0 - decay;
32 self.value = Some(prev * decay + sample * self.alpha);
33 }
34 _ => {
35 self.value = Some(sample);
36 }
37 }
38 self.last_update = Some(now);
39 }
40 
41 pub fn value(&self) -> Option<f64> {
42 self.value
43 }
44 
45 pub fn reset(&mut self) {
46 self.value = None;
47 self.last_update = None;
48 }
49}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Deriving the decay from elapsed time and a half-life makes the average robust to irregular sampling intervals.
  2. 2Using Option to represent an uninitialized average lets the first sample seed the state cleanly.
  3. 3Splitting observe from observe_at isolates the wall clock, making the core logic deterministic and testable.

Related explainers

Share this explainer

Here's the card — post it anywhere.

A time-decayed moving average in Rust — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code