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
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Deriving the decay from elapsed time and a half-life makes the average robust to irregular sampling intervals.
- 2Using Option to represent an uninitialized average lets the first sample seed the state cleanly.
- 3Splitting observe from observe_at isolates the wall clock, making the core logic deterministic and testable.
Related explainers
rust
use std::fs::File; use std::io::{Read, Seek, SeekFrom}; #[derive(Debug)]
Parsing an HTTP Range header in Rust
http-range
parsing
file-io
Intermediate
10 steps
rust
use rand::distributions::{Alphanumeric, DistString}; use rand::rngs::OsRng; #[derive(Debug, Clone, PartialEq, Eq)]
A newtype wrapper for API tokens in Rust
newtype-pattern
randomness
encapsulation
Intermediate
6 steps
rust
pub fn normalize_path(input: &str) -> String { let is_absolute = input.starts_with('/'); let has_trailing_slash = input.len() > 1 && input.ends_with('/'); let mut stack: Vec<&str> = Vec::new();
Normalizing filesystem paths in Rust
string-processing
stack
path-manipulation
Intermediate
8 steps
rust
use axum::{ extract::Query, response::IntoResponse, Json,
Parsing query strings in Axum handlers
deserialization
query-parameters
defaults
Intermediate
7 steps
rust
use axum::{ body::{Body, Bytes}, extract::Request, http::StatusCode,
Logging request and response sizes in Axum
middleware
http
streaming-bodies
Advanced
8 steps
rust
use std::borrow::Cow; pub fn escape_html(input: &str) -> Cow<'_, str> { let needs_escape = input
Zero-copy HTML escaping with Cow in Rust
clone-on-write
zero-copy
string-escaping
Intermediate
6 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/a-time-decayed-moving-average-in-rust-explained-rust-f4d8/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.