use std::collections::VecDeque;
use crate::column::ColumnData;
use crate::duration::parse_duration;
use crate::elements::Elements;
use crate::error::{PondError, Result};
use crate::reducer::{
Agg, InputKind, Inputs, Reducer, check_source_kind, clamp_at_zero, input_kind, output_column,
percentile_of_sorted,
};
use crate::reshape::At;
use crate::sequence::Sequence;
use crate::series::{Column, IntervalKeys, Key, TimeSeries};
const CALL: &str = "rolling";
pub(crate) const SUM_SPLIT: f64 = pow2(960);
pub(crate) const STDEV_SPLIT: f64 = pow2(480);
pub(crate) const fn pow2(e: u64) -> f64 {
f64::from_bits((e + 1023) << 52)
}
pub(crate) trait WindowState {
fn add(&mut self, row: usize, v: f64);
fn remove(&mut self, row: usize, v: f64);
fn snapshot(&mut self) -> Option<f64>;
}
#[derive(Default)]
struct CountState(i64);
impl WindowState for CountState {
fn add(&mut self, _: usize, _: f64) {
self.0 += 1;
}
fn remove(&mut self, _: usize, _: f64) {
self.0 -= 1;
}
fn snapshot(&mut self) -> Option<f64> {
Some(self.0 as f64)
}
}
const SPLIT: f64 = 134_217_729.0;
#[derive(Default, Clone)]
pub(crate) struct WindowSum {
pub(crate) hi: f64,
lo: f64,
pub(crate) n: i64,
}
impl WindowSum {
pub(crate) fn add(&mut self, v: f64) {
self.n += 1;
self.acc(v);
}
pub(crate) fn remove(&mut self, v: f64) {
self.n -= 1;
if self.n == 0 {
self.hi = 0.0;
self.lo = 0.0;
return;
}
self.acc(-v);
}
fn acc(&mut self, v: f64) {
let h = self.hi;
let s = h + v;
let z = s - h;
let e = h - (s - z) + (v - z) + self.lo;
let t = s + e;
self.lo = e - (t - s);
self.hi = t;
}
}
#[derive(Default)]
struct SumState(WindowSum);
impl WindowState for SumState {
fn add(&mut self, _: usize, v: f64) {
self.0.add(v);
}
fn remove(&mut self, _: usize, v: f64) {
self.0.remove(v);
}
fn snapshot(&mut self) -> Option<f64> {
Some(self.0.hi)
}
}
#[derive(Default)]
struct AvgState(WindowSum);
impl WindowState for AvgState {
fn add(&mut self, _: usize, v: f64) {
self.0.add(v);
}
fn remove(&mut self, _: usize, v: f64) {
self.0.remove(v);
}
fn snapshot(&mut self) -> Option<f64> {
(self.0.n != 0).then(|| self.0.hi / self.0.n as f64)
}
}
#[derive(Default)]
pub(crate) struct ExtremeState<const MAX: bool>(pub(crate) VecDeque<(usize, f64)>);
impl<const MAX: bool> WindowState for ExtremeState<MAX> {
fn add(&mut self, row: usize, v: f64) {
while let Some(&(_, back)) = self.0.back() {
let keep = if MAX { back >= v } else { back <= v };
if keep {
break;
}
self.0.pop_back();
}
self.0.push_back((row, v));
}
fn remove(&mut self, row: usize, _: f64) {
if self.0.front().is_some_and(|&(r, _)| r == row) {
self.0.pop_front();
}
}
fn snapshot(&mut self) -> Option<f64> {
self.0.front().map(|&(_, v)| v)
}
}
struct OrderedState<const LAST: bool>(VecDeque<(usize, f64)>);
impl<const LAST: bool> WindowState for OrderedState<LAST> {
fn add(&mut self, row: usize, v: f64) {
self.0.push_back((row, v));
}
fn remove(&mut self, row: usize, _: f64) {
if self.0.front().is_some_and(|&(r, _)| r == row) {
self.0.pop_front();
}
}
fn snapshot(&mut self) -> Option<f64> {
if LAST { self.0.back() } else { self.0.front() }.map(|&(_, v)| v)
}
}
pub(crate) struct SortedState {
pub(crate) sorted: Vec<f64>,
pub(crate) q: f64,
}
impl WindowState for SortedState {
fn add(&mut self, _: usize, v: f64) {
let at = lower_bound(&self.sorted, v);
self.sorted.insert(at, v);
}
fn remove(&mut self, _: usize, v: f64) {
let at = lower_bound(&self.sorted, v);
if at < self.sorted.len() && self.sorted[at] == v {
self.sorted.remove(at);
}
}
fn snapshot(&mut self) -> Option<f64> {
(!self.sorted.is_empty()).then(|| percentile_of_sorted(&self.sorted, self.q))
}
}
#[derive(Default)]
pub(crate) struct DiffState {
max: ExtremeState<true>,
min: ExtremeState<false>,
zeros: VecDeque<f64>,
}
impl DiffState {
#[cfg(feature = "live")]
pub(crate) fn peek(&self) -> Option<f64> {
let (hi, lo) = (self.max.0.front()?.1, self.min.0.front()?.1);
if hi == 0.0 && lo == 0.0 {
return Some(self.zeros.back()? - self.zeros.front()?);
}
Some(hi - lo)
}
}
impl WindowState for DiffState {
fn add(&mut self, row: usize, v: f64) {
self.max.add(row, v);
self.min.add(row, v);
if v == 0.0 {
self.zeros.push_front(v);
}
}
fn remove(&mut self, row: usize, v: f64) {
self.max.remove(row, v);
self.min.remove(row, v);
if v == 0.0 {
self.zeros.pop_front();
}
}
fn snapshot(&mut self) -> Option<f64> {
let (hi, lo) = (self.max.snapshot()?, self.min.snapshot()?);
if hi == 0.0 && lo == 0.0 {
return Some(self.zeros.back()? - self.zeros.front()?);
}
Some(hi - lo)
}
}
#[derive(Default)]
struct KeepState {
max: ExtremeState<true>,
min: ExtremeState<false>,
}
impl WindowState for KeepState {
fn add(&mut self, row: usize, v: f64) {
self.max.add(row, v);
self.min.add(row, v);
}
fn remove(&mut self, row: usize, v: f64) {
self.max.remove(row, v);
self.min.remove(row, v);
}
fn snapshot(&mut self) -> Option<f64> {
let (hi, lo) = (self.max.snapshot()?, self.min.snapshot()?);
(hi == lo).then_some(hi + 0.0)
}
}
#[derive(Default, Clone)]
pub(crate) struct StdevState {
pub(crate) n: f64,
k: f64,
s1h: f64,
s1l: f64,
s2h: f64,
s2l: f64,
}
impl StdevState {
fn acc(&mut self, v: f64, sign: f64) {
let k = self.k;
let dh = v - k;
let bb = dh - v;
let dl = v - (dh - bb) + (-k - bb);
let c = SPLIT * dh;
let ah = c - (c - dh);
let al = dh - ah;
let p = dh * dh;
let e = ah * ah - p + 2.0 * ah * al + al * al + 2.0 * dh * dl;
let mut h = self.s1h;
let mut x = sign * dh;
let mut s = h + x;
let mut z = s - h;
let mut er = h - (s - z) + (x - z) + self.s1l + sign * dl;
h = s + er;
self.s1l = er - (h - s);
self.s1h = h;
h = self.s2h;
x = sign * p;
s = h + x;
z = s - h;
er = h - (s - z) + (x - z) + self.s2l + sign * e;
h = s + er;
self.s2l = er - (h - s);
self.s2h = h;
}
#[cfg(feature = "live")]
pub(crate) fn mean(&self) -> f64 {
if self.n == 0.0 {
return 0.0;
}
self.k + (self.s1h + self.s1l) / self.n
}
pub(crate) fn m2(&mut self) -> f64 {
let n = self.n;
if n <= 1.0 {
return 0.0;
}
let mut mh = self.s1h / n;
let mut q = self.s1h * mh;
if q > 0.5 * self.s2h {
self.reshift(mh);
mh = self.s1h / n;
q = self.s1h * mh;
}
self.s2h - q + self.s2l - 2.0 * mh * self.s1l
}
fn reshift(&mut self, delta: f64) {
let k = self.k;
let n = self.n;
let k_new = k + delta;
let gh = k_new - k;
let mut bb = gh - k_new;
let gl = k_new - (gh - bb) + (-k - bb);
let mut c = SPLIT * gh;
let ah = c - (c - gh);
let al = gh - ah;
c = SPLIT * n;
let bh = c - (c - n);
let bl = n - bh;
let nh = n * gh;
let nl = ah * bh - nh + ah * bl + al * bh + al * bl + n * gl;
let (s1h, s1l) = (self.s1h, self.s1l);
let mut s = s1h - nh;
bb = s - s1h;
let mut e = s1h - (s - bb) + (-nh - bb) + s1l - nl;
let u1h = s + e;
let u1l = e - (u1h - s);
s = s1h + u1h;
bb = s - s1h;
e = s1h - (s - bb) + (u1h - bb) + s1l + u1l;
let th = s + e;
let tl = e - (th - s);
c = SPLIT * th;
let ch = c - (c - th);
let cl = th - ch;
let ph = gh * th;
let pl = ah * ch - ph + ah * cl + al * ch + al * cl + gh * tl + gl * th;
let s2h = self.s2h;
s = s2h - ph;
bb = s - s2h;
e = s2h - (s - bb) + (-ph - bb) + self.s2l - pl;
let u2h = s + e;
self.s2l = e - (u2h - s);
self.s2h = u2h;
self.s1h = u1h;
self.s1l = u1l;
self.k = k_new;
}
}
impl WindowState for StdevState {
fn add(&mut self, _: usize, v: f64) {
if self.n == 0.0 {
*self = Self {
n: 1.0,
k: v,
..Self::default()
};
return;
}
self.n += 1.0;
self.acc(v, 1.0);
}
fn remove(&mut self, _: usize, v: f64) {
if self.n <= 1.0 {
*self = Self::default();
return;
}
self.n -= 1.0;
self.acc(v, -1.0);
}
fn snapshot(&mut self) -> Option<f64> {
if self.n == 0.0 {
return None;
}
if self.n == 1.0 {
return Some(0.0);
}
let m2 = self.m2();
Some(clamp_at_zero(m2 / self.n).sqrt())
}
}
pub(crate) fn lower_bound(sorted: &[f64], v: f64) -> usize {
sorted.partition_point(|&x| x < v)
}
pub(crate) trait Window {
fn enter(&mut self, row: usize);
fn leave(&mut self, row: usize);
fn emit(&mut self, rows: std::ops::Range<usize>);
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Alignment {
#[default]
Trailing,
Leading,
Centered,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Rolling {
pub alignment: Alignment,
pub min_samples: usize,
pub append: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct RollingOver {
pub alignment: Alignment,
pub min_samples: usize,
pub sample: At,
pub range: Option<(i64, i64)>,
}
#[derive(Clone, Copy)]
pub(crate) enum Span<'a> {
Time(i64),
Rows(usize),
Windows(&'a [std::ops::Range<usize>]),
}
pub(crate) trait Anchor: Copy + PartialEq {
fn ms(self) -> f64;
}
impl Anchor for i64 {
fn ms(self) -> f64 {
self as f64
}
}
impl Anchor for f64 {
fn ms(self) -> f64 {
self
}
}
#[derive(Clone, Copy)]
pub(crate) struct Frame<'a, T: Anchor = i64> {
pub times: &'a [T],
pub span: Span<'a>,
pub alignment: Alignment,
pub min_samples: usize,
}
impl<T: Anchor> Frame<'_, T> {
pub fn len(&self) -> usize {
match self.span {
Span::Windows(w) => w.len(),
_ => self.times.len(),
}
}
}
pub(crate) fn walk<T: Anchor>(frame: &Frame<T>, w: &mut impl Window) {
let times = frame.times;
let rows = times.len();
let (mut start, mut end) = (0usize, 0usize);
let emit = |w: &mut _, out: std::ops::Range<usize>, start: usize, end: usize| {
if end - start >= frame.min_samples {
Window::emit(w, out);
}
};
if let Span::Windows(windows) = frame.span {
for (i, r) in windows.iter().enumerate() {
while end < r.end {
w.enter(end);
end += 1;
}
while start < r.start {
w.leave(start);
start += 1;
}
emit(w, i..i + 1, start, end);
}
return;
}
if let Span::Rows(n) = frame.span {
let left = (n - 1) / 2;
let right = n - 1 - left;
for i in 0..rows {
let (lo, hi) = match frame.alignment {
Alignment::Trailing => (i.saturating_sub(n - 1), i),
Alignment::Leading => (i, i.saturating_add(n - 1).min(rows - 1)),
Alignment::Centered => (
i.saturating_sub(left),
i.saturating_add(right).min(rows - 1),
),
};
while end <= hi {
w.enter(end);
end += 1;
}
while start < lo {
w.leave(start);
start += 1;
}
emit(w, i..i + 1, start, end);
}
return;
}
let Span::Time(window_ms) = frame.span else {
unreachable!()
};
let window = window_ms as f64;
let mut group_start = 0;
while group_start < rows {
let anchor = times[group_start];
let mut group_end = group_start + 1;
while group_end < rows && times[group_end] == anchor {
group_end += 1;
}
let a = anchor.ms();
let t = |row: usize| times[row].ms();
match frame.alignment {
Alignment::Trailing => {
while end < rows && t(end) <= a {
w.enter(end);
end += 1;
}
let lower = a - window;
while start < end && t(start) <= lower {
w.leave(start);
start += 1;
}
}
Alignment::Leading | Alignment::Centered => {
let (lower, upper) = if frame.alignment == Alignment::Leading {
(a, a + window)
} else {
let half = window / 2.0;
(a - half, a + half)
};
while start < end && t(start) < lower {
w.leave(start);
start += 1;
}
while end < rows && t(end) < upper {
w.enter(end);
end += 1;
}
}
}
emit(w, group_start..group_end, start, end);
group_start = group_end;
}
}
struct Rolled<'a, S> {
state: S,
values: &'a [f64],
out: Vec<f64>,
}
impl<S: WindowState> Window for Rolled<'_, S> {
fn enter(&mut self, row: usize) {
let v = self.values[row];
if v.is_finite() {
self.state.add(row, v);
}
}
fn leave(&mut self, row: usize) {
let v = self.values[row];
if v.is_finite() {
self.state.remove(row, v);
}
}
fn emit(&mut self, rows: std::ops::Range<usize>) {
let snap = self
.state
.snapshot()
.filter(|v| v.is_finite())
.unwrap_or(f64::NAN);
self.out[rows].fill(snap);
}
}
fn roll<S: WindowState, T: Anchor>(state: S, frame: &Frame<T>, values: &[f64]) -> Vec<f64> {
let mut w = Rolled {
state,
values,
out: vec![f64::NAN; frame.len()],
};
walk(frame, &mut w);
w.out
}
pub(crate) fn roll_column<T: Anchor>(
frame: &Frame<T>,
values: &[f64],
reducer: Reducer,
) -> Vec<f64> {
match reducer {
Reducer::Count => roll(CountState::default(), frame, values),
Reducer::Sum => roll(SumState::default(), frame, values),
Reducer::Avg => roll(AvgState::default(), frame, values),
Reducer::Min => roll(ExtremeState::<false>(VecDeque::new()), frame, values),
Reducer::Max => roll(ExtremeState::<true>(VecDeque::new()), frame, values),
Reducer::First => roll(OrderedState::<false>(VecDeque::new()), frame, values),
Reducer::Last => roll(OrderedState::<true>(VecDeque::new()), frame, values),
Reducer::Median => roll(
SortedState {
sorted: Vec::new(),
q: 50.0,
},
frame,
values,
),
Reducer::Percentile(q) => roll(
SortedState {
sorted: Vec::new(),
q,
},
frame,
values,
),
Reducer::Stdev => roll(StdevState::default(), frame, values),
Reducer::Difference => roll(DiffState::default(), frame, values),
Reducer::Keep => roll(KeepState::default(), frame, values),
Reducer::Unique | Reducer::Top(_) | Reducer::Samples => {
unreachable!("list reducers run in elements.rs")
}
}
}
impl TimeSeries {
pub fn rolling(&self, window: &str, aggs: &[Agg]) -> Result<TimeSeries> {
self.rolling_with(window, aggs, Rolling::default())
}
pub fn rolling_with(&self, window: &str, aggs: &[Agg], options: Rolling) -> Result<TimeSeries> {
let window_ms = parse_duration(window)?;
self.roll(Span::Time(window_ms), aggs, options)
}
pub fn rolling_rows(&self, rows: usize, aggs: &[Agg], options: Rolling) -> Result<TimeSeries> {
if rows == 0 {
return Err(PondError::InvalidData {
call: CALL,
message: "a row window must hold at least 1 row".into(),
});
}
self.roll(Span::Rows(rows), aggs, options)
}
fn roll(&self, span: Span, aggs: &[Agg], options: Rolling) -> Result<TimeSeries> {
let frame = Frame {
times: self.key().begin(),
span,
alignment: options.alignment,
min_samples: options.min_samples,
};
let plan = self.rolling_plan(aggs, self.key_name())?;
if options.append {
for agg in aggs {
let taken = agg.output == self.key_name()
|| self.columns().iter().any(|c| c.name() == agg.output);
if taken {
return Err(PondError::InvalidData {
call: CALL,
message: format!(
"output column '{}' already exists; with append, give each output its own name (Agg::new)",
agg.output
),
});
}
}
}
let mut columns = self.roll_columns(CALL, &frame, &plan)?;
if options.append {
let mut all = self.columns().to_vec();
all.append(&mut columns);
columns = all;
}
Ok(TimeSeries::from_parts(
self.name().to_string(),
self.key_name().to_string(),
self.shared_key(),
columns,
))
}
pub(crate) fn roll_columns(
&self,
call: &'static str,
frame: &Frame,
plan: &[(usize, &Agg)],
) -> Result<Vec<Column>> {
for &(source, agg) in plan {
check_magnitude(call, &self.columns()[source], agg.reducer)?;
}
let frame = *frame;
let used: Vec<(usize, Reducer)> = plan
.iter()
.map(|&(source, agg)| (source, agg.reducer))
.collect();
let inputs = Inputs::build(self.columns(), &used);
crate::par::map(plan, |&(source, agg)| {
let data = self.columns()[source].data();
if agg.reducer.gives_list() {
let lists = Elements::new(data).roll(agg.reducer, &frame);
return Ok(Column::from_data(agg.output.clone(), lists, false));
}
let kind = input_kind(data, agg.reducer);
if let (ColumnData::Decimal(d), InputKind::ExactSum) = (data, kind) {
return crate::decimal::rolling_sum(call, &agg.output, d, &frame);
}
let input = inputs.get(source, kind);
if let (ColumnData::Decimal(d), Reducer::Difference) = (data, agg.reducer) {
let lo = roll_column(&frame, &input.values, Reducer::Min);
let hi = roll_column(&frame, &input.values, Reducer::Max);
return crate::decimal::difference(call, &agg.output, d, input, &lo, &hi);
}
let out = roll_column(&frame, &input.values, agg.reducer);
Ok(output_column(
agg.output.clone(),
data,
agg.reducer,
input,
out,
))
})
.into_iter()
.collect()
}
pub fn rolling_over(
&self,
sequence: &Sequence,
window: &str,
aggs: &[Agg],
options: RollingOver,
) -> Result<TimeSeries> {
let window_ms = parse_duration(window)?;
let plan = self.rolling_plan(aggs, "interval")?;
let buckets = match self.grid_range(CALL, options.range)? {
None => crate::grid::no_buckets(),
Some((begin, end)) => sequence.buckets_sampled(begin, end, options.sample)?,
};
let ranges = grid_windows(
self.key().begin(),
&buckets,
options.sample,
window_ms,
options.alignment,
);
let plan: Vec<(&Agg, usize)> = plan
.into_iter()
.map(|(source, agg)| (agg, source))
.collect();
let mut columns = self.reduce_columns(CALL, &plan, &ranges)?;
if ranges.iter().any(|r| r.len() < options.min_samples) {
let keep: Vec<Option<usize>> = ranges
.iter()
.enumerate()
.map(|(i, r)| (r.len() >= options.min_samples).then_some(i))
.collect();
for c in &mut columns {
*c = Column::from_data(c.name().to_string(), c.data().take(&keep), false);
}
}
Ok(TimeSeries::from_parts(
self.name().to_string(),
"interval".to_string(),
Key::Interval(buckets),
columns,
))
}
fn rolling_plan<'a>(&self, aggs: &'a [Agg], key: &str) -> Result<Vec<(usize, &'a Agg)>> {
let mut plan = Vec::with_capacity(aggs.len());
for (i, agg) in aggs.iter().enumerate() {
let source = self.column_index(CALL, &agg.source)?;
agg.reducer.check(CALL)?;
if agg.output == key || aggs[..i].iter().any(|a| a.output == agg.output) {
return Err(PondError::DuplicateOutput {
call: CALL,
column: agg.output.clone(),
});
}
check_source_kind(CALL, &self.columns()[source], agg.reducer)?;
plan.push((source, agg));
}
if aggs.iter().all(|a| a.shorthand) {
plan.sort_by_key(|&(source, _)| source);
}
Ok(plan)
}
}
fn grid_windows(
starts: &[i64],
buckets: &IntervalKeys,
sample: At,
window_ms: i64,
alignment: Alignment,
) -> Vec<std::ops::Range<usize>> {
let w = window_ms as f64;
let t = |row: usize| starts[row] as f64;
let n = starts.len();
let (mut lo, mut hi) = (0usize, 0usize);
buckets
.begin
.iter()
.zip(&buckets.end)
.map(|(&b, &e)| {
let (b, e) = (b as f64, e as f64);
let p = match sample {
At::Begin => b,
At::Center => b + (e - b) / 2.0,
At::End => e,
};
let (from, to) = match alignment {
Alignment::Trailing => (p - w, p),
Alignment::Leading => (p, p + w),
Alignment::Centered => (p - w / 2.0, p + w / 2.0),
};
let trailing = alignment == Alignment::Trailing;
while lo < n && (t(lo) < from || (trailing && t(lo) == from)) {
lo += 1;
}
hi = hi.max(lo);
while hi < n && (t(hi) < to || (trailing && t(hi) == to)) {
hi += 1;
}
lo..hi
})
.collect()
}
pub(crate) fn check_magnitude(call: &'static str, column: &Column, reducer: Reducer) -> Result<()> {
check_magnitude_in(call, column, reducer, None)
}
pub(crate) fn check_magnitude_in(
call: &'static str,
column: &Column,
reducer: Reducer,
rows: Option<&[u32]>,
) -> Result<()> {
let split = match reducer {
Reducer::Sum | Reducer::Avg => SUM_SPLIT,
Reducer::Stdev => STDEV_SPLIT,
_ => return Ok(()),
};
let Ok(values) = column.numbers().map(|c| c.values()) else {
return Ok(());
};
let too_big = |&row: &usize| values[row].is_finite() && values[row].abs() >= split;
let found = match rows {
None => (0..values.len()).find(too_big),
Some(rows) => rows.iter().map(|&r| r as usize).find(too_big),
};
match found {
None => Ok(()),
Some(row) => Err(PondError::Unsupported {
call,
message: format!(
"column '{}' row {row} holds {:e}, too large for a rolling {:?} (limit {split:e})",
column.name(),
values[row],
reducer
),
}),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn extreme_times_keep_their_own_row() {
use crate::MAX_TIME_MS;
for t in [-MAX_TIME_MS, MAX_TIME_MS] {
let s = TimeSeries::new("s", vec![t], vec![Column::required("v", vec![7.0])]).unwrap();
for window in ["1ms", "100000000d"] {
let r = s
.rolling(window, &[Agg::new("n", "v", Reducer::Count)])
.unwrap();
assert_eq!(
r.column("n").unwrap().numbers().unwrap().get(0),
Some(1.0),
"{t} {window}"
);
}
}
let too_early =
TimeSeries::new("s", vec![i64::MIN], vec![Column::required("v", vec![7.0])]);
assert!(too_early.is_err());
}
#[test]
fn difference_matches_the_sorted_window_bit_for_bit() {
struct Sorted(Vec<f64>);
impl WindowState for Sorted {
fn add(&mut self, _: usize, v: f64) {
let at = lower_bound(&self.0, v);
self.0.insert(at, v);
}
fn remove(&mut self, _: usize, v: f64) {
let at = lower_bound(&self.0, v);
if at < self.0.len() && self.0[at] == v {
self.0.remove(at);
}
}
fn snapshot(&mut self) -> Option<f64> {
Some(self.0.last()? - self.0.first()?)
}
}
let mut seed = 11u64;
let mut next = || {
seed = seed
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
seed >> 33
};
for _ in 0..300 {
let n = 1 + next() as usize % 60;
let mut t = 0;
let times: Vec<i64> = (0..n)
.map(|_| {
t += next() as i64 % 3;
t
})
.collect();
let values: Vec<f64> = (0..n)
.map(|_| [0.0, -0.0, 0.0, -0.0, 1.0, -2.0, f64::NAN][next() as usize % 7])
.collect();
let window = 1 + next() as i64 % 8;
let frame = Frame {
times: ×,
span: Span::Time(window),
alignment: Alignment::Trailing,
min_samples: 0,
};
let want = roll(Sorted(Vec::new()), &frame, &values);
let got = roll(DiffState::default(), &frame, &values);
let bits = |xs: &[f64]| xs.iter().map(|x| x.to_bits()).collect::<Vec<_>>();
assert_eq!(bits(&got), bits(&want), "{times:?} {values:?} {window}");
}
}
#[test]
fn splits_are_the_pond_ts_powers_of_two() {
assert_eq!(SUM_SPLIT, 2f64.powi(960));
assert_eq!(STDEV_SPLIT, 2f64.powi(480));
}
}