#[derive(Clone, Copy, Debug)]
pub struct Trend<const N: usize> {
samples: [f32; N],
len: usize,
next: usize,
}
impl<const N: usize> Trend<N> {
pub fn new() -> Self {
Self {
samples: [0.0; N],
len: 0,
next: 0,
}
}
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 slope(&self) -> Option<f32> {
if self.len < 2 {
return None;
}
let count = self.len as f32;
let mean_x = (count - 1.0) / 2.0;
let mut mean_y = 0.0;
for i in 0..self.len {
mean_y += self.ordered(i);
}
mean_y /= count;
let mut covariance = 0.0;
let mut variance_x = 0.0;
for i in 0..self.len {
let dx = i as f32 - mean_x;
covariance += dx * (self.ordered(i) - mean_y);
variance_x += dx * dx;
}
if variance_x == 0.0 {
return None;
}
Some(covariance / variance_x)
}
pub fn len(&self) -> usize {
self.len
}
pub fn is_empty(&self) -> bool {
self.len == 0
}
fn ordered(&self, index: usize) -> f32 {
let physical = if self.len == N {
(self.next + index) % N
} else {
index
};
self.samples[physical]
}
}
impl<const N: usize> Default for Trend<N> {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn approx(a: f32, b: f32) -> bool {
(a - b).abs() < 1e-4
}
#[test]
fn a_perfect_rising_line_has_its_exact_slope() {
let mut trend = Trend::<5>::new();
for reading in [1.0, 3.0, 5.0, 7.0, 9.0] {
trend.push(reading);
}
assert!(approx(trend.slope().unwrap(), 2.0));
}
#[test]
fn a_falling_line_has_a_negative_slope() {
let mut trend = Trend::<3>::new();
for reading in [10.0, 8.0, 6.0] {
trend.push(reading);
}
assert!(approx(trend.slope().unwrap(), -2.0));
}
#[test]
fn a_flat_signal_has_zero_slope() {
let mut trend = Trend::<4>::new();
for reading in [5.0, 5.0, 5.0, 5.0] {
trend.push(reading);
}
assert!(approx(trend.slope().unwrap(), 0.0));
}
#[test]
fn slope_matches_a_hand_computed_least_squares_fit() {
let mut trend = Trend::<5>::new();
for reading in [4.0, 5.0, 7.0, 10.0, 15.0] {
trend.push(reading);
}
assert!(approx(trend.slope().unwrap(), 2.7));
}
#[test]
fn fewer_than_two_readings_has_no_slope() {
let mut trend = Trend::<3>::new();
assert_eq!(trend.slope(), None);
trend.push(5.0);
assert_eq!(trend.slope(), None);
}
#[test]
fn the_slope_follows_the_window_as_it_slides() {
let mut trend = Trend::<3>::new();
for reading in [0.0, 0.0, 0.0, 10.0, 20.0, 30.0] {
trend.push(reading);
}
assert!(approx(trend.slope().unwrap(), 10.0));
}
}