#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Envelope {
pub min: f64,
pub mean: f64,
pub max: f64,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct EnvelopeAcc {
min: f64,
max: f64,
sum: f64,
n: u64,
}
impl EnvelopeAcc {
pub fn push(&mut self, v: f64) {
if !v.is_finite() {
return;
}
if self.n == 0 {
self.min = v;
self.max = v;
} else {
if v < self.min {
self.min = v;
}
if v > self.max {
self.max = v;
}
}
self.sum += v;
self.n += 1;
}
pub fn push_opt(&mut self, v: Option<f64>) {
if let Some(v) = v {
self.push(v);
}
}
pub fn count(&self) -> u64 {
self.n
}
pub fn take(&mut self) -> Option<Envelope> {
if self.n == 0 {
return None;
}
let env = Envelope {
min: self.min,
mean: self.sum / self.n as f64,
max: self.max,
};
*self = Self::default();
Some(env)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn an_unobserved_interval_is_absent_not_zero() {
let mut acc = EnvelopeAcc::default();
assert_eq!(acc.take(), None);
assert_eq!(acc.count(), 0);
}
#[test]
fn the_envelope_keeps_what_a_mean_alone_would_hide() {
let mut acc = EnvelopeAcc::default();
for v in [0.0, 100.0, 100.0, 100.0, 0.0] {
acc.push(v);
}
let env = acc.take().unwrap();
assert_eq!(env.max, 100.0, "the excursion survives");
assert_eq!(env.min, 0.0);
assert_eq!(env.mean, 60.0);
}
#[test]
fn take_drains_so_intervals_do_not_bleed_into_each_other() {
let mut acc = EnvelopeAcc::default();
acc.push(50.0);
assert_eq!(acc.take().unwrap().mean, 50.0);
assert_eq!(acc.take(), None, "second interval starts empty");
acc.push(10.0);
let env = acc.take().unwrap();
assert_eq!(
(env.min, env.mean, env.max),
(10.0, 10.0, 10.0),
"no contribution from the drained interval"
);
}
#[test]
fn a_single_observation_is_a_degenerate_envelope() {
let mut acc = EnvelopeAcc::default();
acc.push(42.0);
assert_eq!(acc.take(), Some(Envelope { min: 42.0, mean: 42.0, max: 42.0 }));
}
#[test]
fn absent_observations_are_skipped_not_counted() {
let mut acc = EnvelopeAcc::default();
acc.push_opt(None);
acc.push_opt(Some(20.0));
acc.push_opt(None);
acc.push_opt(Some(40.0));
let env = acc.take().unwrap();
assert_eq!(env.mean, 30.0, "Nones must not count as zeros in the mean");
assert_eq!(env.min, 20.0);
}
#[test]
fn non_finite_readings_cannot_poison_the_interval() {
let mut acc = EnvelopeAcc::default();
acc.push(10.0);
acc.push(f64::NAN);
acc.push(f64::INFINITY);
acc.push(30.0);
let env = acc.take().unwrap();
assert!(
env.min.is_finite() && env.mean.is_finite() && env.max.is_finite(),
"no field may inherit NaN/inf from a bad reading",
);
assert_eq!(env.mean, 20.0, "only the two finite readings count");
assert_eq!(env.max, 30.0);
}
#[test]
fn negative_values_are_ordered_correctly() {
let mut acc = EnvelopeAcc::default();
for v in [-5.0, -1.0, -10.0] {
acc.push(v);
}
let env = acc.take().unwrap();
assert_eq!(env.min, -10.0);
assert_eq!(env.max, -1.0);
}
}