use std::sync::Mutex;
#[derive(Debug)]
pub struct BinomialSummary {
capacity: usize,
samples: Mutex<Vec<f64>>,
reductions: Mutex<u32>,
}
impl BinomialSummary {
pub fn new(capacity: usize) -> Self {
assert!(capacity >= 2, "BinomialSummary capacity must be >= 2");
Self {
capacity,
samples: Mutex::new(Vec::with_capacity(capacity)),
reductions: Mutex::new(0),
}
}
pub fn capacity(&self) -> usize {
self.capacity
}
pub fn record(&self, value: f64) {
let mut g = self.samples.lock().unwrap_or_else(|e| e.into_inner());
if g.len() >= self.capacity {
let mut reduced: Vec<f64> = Vec::with_capacity(self.capacity / 2 + 1);
let mut it = g.iter().copied();
loop {
match (it.next(), it.next()) {
(Some(a), Some(b)) => reduced.push((a + b) * 0.5),
(Some(a), None) => {
reduced.push(a);
break;
}
(None, _) => break,
}
}
*g = reduced;
let mut r = self.reductions.lock().unwrap_or_else(|e| e.into_inner());
*r += 1;
}
g.push(value);
}
pub fn snapshot(&self) -> Vec<f64> {
self.samples
.lock()
.unwrap_or_else(|e| e.into_inner())
.clone()
}
pub fn len(&self) -> usize {
self.samples.lock().unwrap_or_else(|e| e.into_inner()).len()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn reductions(&self) -> u32 {
*self.reductions.lock().unwrap_or_else(|e| e.into_inner())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn records_up_to_capacity() {
let b = BinomialSummary::new(8);
for i in 0..5 {
b.record(i as f64);
}
assert_eq!(b.len(), 5);
assert_eq!(b.reductions(), 0);
assert_eq!(b.snapshot(), vec![0.0, 1.0, 2.0, 3.0, 4.0]);
}
#[test]
fn reduces_on_overflow() {
let b = BinomialSummary::new(4);
for i in 0..4 {
b.record(i as f64);
}
assert_eq!(b.snapshot(), vec![0.0, 1.0, 2.0, 3.0]);
assert_eq!(b.reductions(), 0);
b.record(4.0);
assert_eq!(b.snapshot(), vec![0.5, 2.5, 4.0]);
assert_eq!(b.reductions(), 1);
assert!(b.len() <= b.capacity());
}
#[test]
fn odd_length_carries_last() {
let b = BinomialSummary::new(3);
for i in 1..=3 {
b.record(i as f64);
}
b.record(4.0);
assert_eq!(b.snapshot(), vec![1.5, 3.0, 4.0]);
}
#[test]
fn capacity_never_exceeded_across_many_records() {
let cap = 16usize;
let b = BinomialSummary::new(cap);
for i in 0..1_000 {
b.record(i as f64);
}
assert!(b.len() <= cap, "len {} > capacity {}", b.len(), cap);
assert!(b.reductions() >= 1);
}
#[test]
fn snapshot_does_not_drain() {
let b = BinomialSummary::new(4);
b.record(10.0);
b.record(20.0);
let a = b.snapshot();
let c = b.snapshot();
assert_eq!(a, c);
assert_eq!(b.len(), 2);
}
#[test]
#[should_panic(expected = "capacity must be >= 2")]
fn rejects_tiny_capacity() {
let _ = BinomialSummary::new(1);
}
}