#[derive(Clone, Copy, Debug)]
pub struct Median<const N: usize> {
samples: [f32; N],
len: usize,
next: usize,
}
impl<const N: usize> Median<N> {
pub fn new() -> Self {
Self {
samples: [0.0; N],
len: 0,
next: 0,
}
}
pub fn update(&mut self, reading: f32) -> f32 {
self.push(reading);
self.median().unwrap_or(reading)
}
pub fn push(&mut self, reading: f32) {
if N == 0 {
return;
}
self.samples[self.next] = reading;
self.next = (self.next + 1) % N;
if self.len < N {
self.len += 1;
}
}
pub fn median(&self) -> Option<f32> {
if self.len == 0 {
return None;
}
let mut sorted = [0.0f32; N];
sorted[..self.len].copy_from_slice(&self.samples[..self.len]);
let window = &mut sorted[..self.len];
window.sort_unstable_by(f32::total_cmp);
let mid = self.len / 2;
if self.len % 2 == 1 {
Some(window[mid])
} else {
Some((window[mid - 1] + window[mid]) / 2.0)
}
}
pub fn len(&self) -> usize {
self.len
}
pub fn is_empty(&self) -> bool {
self.len == 0
}
}
impl<const N: usize> Default for Median<N> {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn odd_window_takes_the_middle_value() {
let mut median = Median::<3>::new();
median.update(3.0);
median.update(1.0);
assert_eq!(median.update(2.0), 2.0); }
#[test]
fn even_window_averages_the_two_middle_values() {
let mut median = Median::<4>::new();
for reading in [1.0, 2.0, 3.0, 4.0] {
median.push(reading);
}
assert_eq!(median.median(), Some(2.5));
}
#[test]
fn a_single_spike_is_rejected() {
let mut median = Median::<5>::new();
for reading in [10.0, 10.0, 99.0, 10.0, 10.0] {
median.push(reading);
}
assert_eq!(median.median(), Some(10.0));
}
#[test]
fn an_empty_filter_has_no_median() {
let median = Median::<3>::new();
assert!(median.is_empty());
assert_eq!(median.median(), None);
}
#[test]
fn the_window_evicts_oldest_when_full() {
let mut median = Median::<3>::new();
for reading in [1.0, 2.0, 3.0, 100.0, 100.0] {
median.push(reading);
}
assert_eq!(median.median(), Some(100.0));
assert_eq!(median.len(), 3);
}
}