Skip to main content

pond_rs/
rolling.rs

1//! `TimeSeries::rolling` and its options: a window of time or of rows
2//! around each row, or around each bucket of a sequence.
3//!
4//! The window states are ports of pond-ts's `rollingState`s (add a value as
5//! it enters the window, remove it as it leaves), so results match pond-ts
6//! bit for bit. `sum` / `avg` / `stdev` use pond-ts's double-double running
7//! sums (`reducers/window-accumulators.ts`), which do not drift
8//! ([PND-ROLLSTD]).
9
10use std::collections::VecDeque;
11
12use crate::column::ColumnData;
13use crate::duration::parse_duration;
14use crate::elements::Elements;
15use crate::error::{PondError, Result};
16use crate::reducer::{
17    Agg, InputKind, Inputs, Reducer, check_source_kind, clamp_at_zero, input_kind, output_column,
18    percentile_of_sorted,
19};
20use crate::reshape::At;
21use crate::sequence::Sequence;
22use crate::series::{Column, IntervalKeys, Key, TimeSeries};
23
24const CALL: &str = "rolling";
25
26/// pond-ts `SUM_SPLIT`: from this magnitude pond-ts moves a value into an
27/// exact big-integer lane so a running sum cannot get stuck at infinity.
28/// Batch pond-rs refuses such values instead (the live layer has the lane:
29/// `live/states.rs`).
30pub(crate) const SUM_SPLIT: f64 = pow2(960);
31/// pond-ts `STDEV_SPLIT`, the same idea for stdev.
32pub(crate) const STDEV_SPLIT: f64 = pow2(480);
33
34/// `2^e` exactly, for normal exponents.
35pub(crate) const fn pow2(e: u64) -> f64 {
36    f64::from_bits((e + 1023) << 52)
37}
38
39/// One column's window state: a value enters with `add`, leaves with
40/// `remove`; `snapshot` is the reducer over what is inside. Only present
41/// (finite) values reach a state.
42pub(crate) trait WindowState {
43    fn add(&mut self, row: usize, v: f64);
44    fn remove(&mut self, row: usize, v: f64);
45    /// `&mut`: the stdev state may move its reference point when read.
46    fn snapshot(&mut self) -> Option<f64>;
47}
48
49#[derive(Default)]
50struct CountState(i64);
51
52impl WindowState for CountState {
53    fn add(&mut self, _: usize, _: f64) {
54        self.0 += 1;
55    }
56    fn remove(&mut self, _: usize, _: f64) {
57        self.0 -= 1;
58    }
59    fn snapshot(&mut self) -> Option<f64> {
60        Some(self.0 as f64)
61    }
62}
63
64/// Dekker's split constant, 2^27 + 1.
65const SPLIT: f64 = 134_217_729.0;
66
67/// pond-ts `WindowSum`: a running sum held as `hi + lo` (double-double), so
68/// rounding errors are kept rather than piling up. `hi` is the sum rounded
69/// once; `n` resets the empty window to exactly 0.
70#[derive(Default, Clone)]
71pub(crate) struct WindowSum {
72    pub(crate) hi: f64,
73    lo: f64,
74    pub(crate) n: i64,
75}
76
77impl WindowSum {
78    pub(crate) fn add(&mut self, v: f64) {
79        self.n += 1;
80        self.acc(v);
81    }
82    pub(crate) fn remove(&mut self, v: f64) {
83        self.n -= 1;
84        if self.n == 0 {
85            self.hi = 0.0;
86            self.lo = 0.0;
87            return;
88        }
89        self.acc(-v);
90    }
91    fn acc(&mut self, v: f64) {
92        let h = self.hi;
93        let s = h + v;
94        let z = s - h;
95        let e = h - (s - z) + (v - z) + self.lo;
96        let t = s + e;
97        self.lo = e - (t - s);
98        self.hi = t;
99    }
100}
101
102/// pond-ts `LanedSum` (small lane only; see [`SUM_SPLIT`]).
103#[derive(Default)]
104struct SumState(WindowSum);
105
106impl WindowState for SumState {
107    fn add(&mut self, _: usize, v: f64) {
108        self.0.add(v);
109    }
110    fn remove(&mut self, _: usize, v: f64) {
111        self.0.remove(v);
112    }
113    fn snapshot(&mut self) -> Option<f64> {
114        Some(self.0.hi)
115    }
116}
117
118/// pond-ts avg `rollingState`: running sum, then divide.
119#[derive(Default)]
120struct AvgState(WindowSum);
121
122impl WindowState for AvgState {
123    fn add(&mut self, _: usize, v: f64) {
124        self.0.add(v);
125    }
126    fn remove(&mut self, _: usize, v: f64) {
127        self.0.remove(v);
128    }
129    fn snapshot(&mut self) -> Option<f64> {
130        (self.0.n != 0).then(|| self.0.hi / self.0.n as f64)
131    }
132}
133
134/// pond-ts `rollingMonotoneDeque`: (row, value) pairs, front is the answer;
135/// the earliest of equal values wins.
136#[derive(Default)]
137pub(crate) struct ExtremeState<const MAX: bool>(pub(crate) VecDeque<(usize, f64)>);
138
139impl<const MAX: bool> WindowState for ExtremeState<MAX> {
140    fn add(&mut self, row: usize, v: f64) {
141        while let Some(&(_, back)) = self.0.back() {
142            let keep = if MAX { back >= v } else { back <= v };
143            if keep {
144                break;
145            }
146            self.0.pop_back();
147        }
148        self.0.push_back((row, v));
149    }
150    fn remove(&mut self, row: usize, _: f64) {
151        if self.0.front().is_some_and(|&(r, _)| r == row) {
152            self.0.pop_front();
153        }
154    }
155    fn snapshot(&mut self) -> Option<f64> {
156        self.0.front().map(|&(_, v)| v)
157    }
158}
159
160/// pond-ts `rollingOrderedEntries`: present values in arrival order.
161struct OrderedState<const LAST: bool>(VecDeque<(usize, f64)>);
162
163impl<const LAST: bool> WindowState for OrderedState<LAST> {
164    fn add(&mut self, row: usize, v: f64) {
165        self.0.push_back((row, v));
166    }
167    fn remove(&mut self, row: usize, _: f64) {
168        if self.0.front().is_some_and(|&(r, _)| r == row) {
169            self.0.pop_front();
170        }
171    }
172    fn snapshot(&mut self) -> Option<f64> {
173        if LAST { self.0.back() } else { self.0.front() }.map(|&(_, v)| v)
174    }
175}
176
177/// pond-ts `rollingSortedArray` + `percentileOfSorted`.
178pub(crate) struct SortedState {
179    pub(crate) sorted: Vec<f64>,
180    pub(crate) q: f64,
181}
182
183impl WindowState for SortedState {
184    fn add(&mut self, _: usize, v: f64) {
185        let at = lower_bound(&self.sorted, v);
186        self.sorted.insert(at, v);
187    }
188    fn remove(&mut self, _: usize, v: f64) {
189        let at = lower_bound(&self.sorted, v);
190        if at < self.sorted.len() && self.sorted[at] == v {
191            self.sorted.remove(at);
192        }
193    }
194    fn snapshot(&mut self) -> Option<f64> {
195        (!self.sorted.is_empty()).then(|| percentile_of_sorted(&self.sorted, self.q))
196    }
197}
198
199/// pond-ts `difference` `rollingState`: two monotone deques give the
200/// largest and smallest in O(1) a row, as pond-ts's do since
201/// [PND-ROLLDIFF] (#799).
202///
203/// One case needs more: every value in the window is a zero. pond-ts's
204/// answer then follows its old sorted window, a run of `0` and `-0`
205/// inserted and removed at the front of the run (whichever zero leaves),
206/// so the run behaves as a stack of signs; the answer is the bottom minus
207/// the top, which can be `-0`. `zeros` is that stack. Otherwise one end is
208/// not a zero and the sign of the other cannot change the result.
209#[derive(Default)]
210pub(crate) struct DiffState {
211    max: ExtremeState<true>,
212    min: ExtremeState<false>,
213    zeros: VecDeque<f64>,
214}
215
216impl DiffState {
217    /// The snapshot, read without `&mut`.
218    #[cfg(feature = "live")]
219    pub(crate) fn peek(&self) -> Option<f64> {
220        let (hi, lo) = (self.max.0.front()?.1, self.min.0.front()?.1);
221        if hi == 0.0 && lo == 0.0 {
222            return Some(self.zeros.back()? - self.zeros.front()?);
223        }
224        Some(hi - lo)
225    }
226}
227
228impl WindowState for DiffState {
229    fn add(&mut self, row: usize, v: f64) {
230        self.max.add(row, v);
231        self.min.add(row, v);
232        if v == 0.0 {
233            self.zeros.push_front(v);
234        }
235    }
236    fn remove(&mut self, row: usize, v: f64) {
237        self.max.remove(row, v);
238        self.min.remove(row, v);
239        if v == 0.0 {
240            self.zeros.pop_front();
241        }
242    }
243    fn snapshot(&mut self) -> Option<f64> {
244        let (hi, lo) = (self.max.snapshot()?, self.min.snapshot()?);
245        if hi == 0.0 && lo == 0.0 {
246            return Some(self.zeros.back()? - self.zeros.front()?);
247        }
248        Some(hi - lo)
249    }
250}
251
252/// pond-ts `keep` `rollingState`, which counts each distinct value in a
253/// `Map` and answers when one is left. One distinct value is the same as
254/// the largest equal to the smallest, so two monotone deques answer it
255/// without hashing. A `Map` key turns `-0` into `0`, so a window of zeros
256/// gives `0` (`+ 0.0` does the same here).
257#[derive(Default)]
258struct KeepState {
259    max: ExtremeState<true>,
260    min: ExtremeState<false>,
261}
262
263impl WindowState for KeepState {
264    fn add(&mut self, row: usize, v: f64) {
265        self.max.add(row, v);
266        self.min.add(row, v);
267    }
268    fn remove(&mut self, row: usize, v: f64) {
269        self.max.remove(row, v);
270        self.min.remove(row, v);
271    }
272    fn snapshot(&mut self) -> Option<f64> {
273        let (hi, lo) = (self.max.snapshot()?, self.min.snapshot()?);
274        (hi == lo).then_some(hi + 0.0)
275    }
276}
277
278/// pond-ts `WindowStdev` (small lane only; see [`STDEV_SPLIT`]):
279/// double-double `Σd` and `Σd²` of shifted values `d = v − k`, with `d` and
280/// `d²` split exactly. The shift moves to the mean (an exact rewrite of the
281/// sums) when the mean drifts about one stdev away. Same operations in the
282/// same order as pond-ts, so the bits match.
283#[derive(Default, Clone)]
284pub(crate) struct StdevState {
285    pub(crate) n: f64,
286    k: f64,
287    s1h: f64,
288    s1l: f64,
289    s2h: f64,
290    s2l: f64,
291}
292
293impl StdevState {
294    fn acc(&mut self, v: f64, sign: f64) {
295        let k = self.k;
296        let dh = v - k;
297        let bb = dh - v;
298        let dl = v - (dh - bb) + (-k - bb);
299        let c = SPLIT * dh;
300        let ah = c - (c - dh);
301        let al = dh - ah;
302        let p = dh * dh;
303        let e = ah * ah - p + 2.0 * ah * al + al * al + 2.0 * dh * dl;
304        let mut h = self.s1h;
305        let mut x = sign * dh;
306        let mut s = h + x;
307        let mut z = s - h;
308        let mut er = h - (s - z) + (x - z) + self.s1l + sign * dl;
309        h = s + er;
310        self.s1l = er - (h - s);
311        self.s1h = h;
312        h = self.s2h;
313        x = sign * p;
314        s = h + x;
315        z = s - h;
316        er = h - (s - z) + (x - z) + self.s2l + sign * e;
317        h = s + er;
318        self.s2l = er - (h - s);
319        self.s2h = h;
320    }
321
322    /// pond-ts `WindowStdev.mean()`: the window mean (0 when empty, where
323    /// pond-ts gives `undefined` and its caller `?? 0`).
324    #[cfg(feature = "live")]
325    pub(crate) fn mean(&self) -> f64 {
326        if self.n == 0.0 {
327            return 0.0;
328        }
329        self.k + (self.s1h + self.s1l) / self.n
330    }
331
332    pub(crate) fn m2(&mut self) -> f64 {
333        let n = self.n;
334        if n <= 1.0 {
335            return 0.0;
336        }
337        let mut mh = self.s1h / n;
338        let mut q = self.s1h * mh;
339        if q > 0.5 * self.s2h {
340            self.reshift(mh);
341            mh = self.s1h / n;
342            q = self.s1h * mh;
343        }
344        self.s2h - q + self.s2l - 2.0 * mh * self.s1l
345    }
346
347    fn reshift(&mut self, delta: f64) {
348        let k = self.k;
349        let n = self.n;
350        let k_new = k + delta;
351        let gh = k_new - k;
352        let mut bb = gh - k_new;
353        let gl = k_new - (gh - bb) + (-k - bb);
354        let mut c = SPLIT * gh;
355        let ah = c - (c - gh);
356        let al = gh - ah;
357        c = SPLIT * n;
358        let bh = c - (c - n);
359        let bl = n - bh;
360        let nh = n * gh;
361        let nl = ah * bh - nh + ah * bl + al * bh + al * bl + n * gl;
362        let (s1h, s1l) = (self.s1h, self.s1l);
363        let mut s = s1h - nh;
364        bb = s - s1h;
365        let mut e = s1h - (s - bb) + (-nh - bb) + s1l - nl;
366        let u1h = s + e;
367        let u1l = e - (u1h - s);
368        s = s1h + u1h;
369        bb = s - s1h;
370        e = s1h - (s - bb) + (u1h - bb) + s1l + u1l;
371        let th = s + e;
372        let tl = e - (th - s);
373        c = SPLIT * th;
374        let ch = c - (c - th);
375        let cl = th - ch;
376        let ph = gh * th;
377        let pl = ah * ch - ph + ah * cl + al * ch + al * cl + gh * tl + gl * th;
378        let s2h = self.s2h;
379        s = s2h - ph;
380        bb = s - s2h;
381        e = s2h - (s - bb) + (-ph - bb) + self.s2l - pl;
382        let u2h = s + e;
383        self.s2l = e - (u2h - s);
384        self.s2h = u2h;
385        self.s1h = u1h;
386        self.s1l = u1l;
387        self.k = k_new;
388    }
389}
390
391impl WindowState for StdevState {
392    fn add(&mut self, _: usize, v: f64) {
393        if self.n == 0.0 {
394            *self = Self {
395                n: 1.0,
396                k: v,
397                ..Self::default()
398            };
399            return;
400        }
401        self.n += 1.0;
402        self.acc(v, 1.0);
403    }
404    fn remove(&mut self, _: usize, v: f64) {
405        if self.n <= 1.0 {
406            *self = Self::default();
407            return;
408        }
409        self.n -= 1.0;
410        self.acc(v, -1.0);
411    }
412    fn snapshot(&mut self) -> Option<f64> {
413        if self.n == 0.0 {
414            return None;
415        }
416        if self.n == 1.0 {
417            return Some(0.0);
418        }
419        let m2 = self.m2();
420        Some(clamp_at_zero(m2 / self.n).sqrt())
421    }
422}
423
424/// First index whose value is not below `v` (pond-ts `bisect`).
425pub(crate) fn lower_bound(sorted: &[f64], v: f64) -> usize {
426    sorted.partition_point(|&x| x < v)
427}
428
429/// One pass of a rolling window over a column: rows `enter` and `leave`,
430/// and output rows are `emit`ted with the window as it then stands.
431pub(crate) trait Window {
432    fn enter(&mut self, row: usize);
433    fn leave(&mut self, row: usize);
434    fn emit(&mut self, rows: std::ops::Range<usize>);
435}
436
437/// Where a rolling window sits around each row (pond-ts `alignment`).
438#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
439pub enum Alignment {
440    /// The window ends at the row: times in `(t - window, t]`, or the
441    /// `n` rows ending at this one (pond-ts default).
442    #[default]
443    Trailing,
444    /// The window starts at the row: times in `[t, t + window)`, or the
445    /// `n` rows starting at this one.
446    Leading,
447    /// The window is centred on the row: times in
448    /// `[t - window/2, t + window/2)`, or `n` rows around this one, one
449    /// more after it than before when `n` is even.
450    Centered,
451}
452
453/// Options for [`TimeSeries::rolling_with`] and
454/// [`TimeSeries::rolling_rows`]. `Rolling::default()` is pond-ts's
455/// default: a trailing window, every row given a value.
456#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
457pub struct Rolling {
458    /// Where the window sits around each row (default: trailing).
459    pub alignment: Alignment,
460    /// A row whose window holds fewer rows than this gets a gap in every
461    /// output column (pond-ts `minSamples`). Rows are counted whether or
462    /// not their value is a gap. 0 (the default) gives every row a value.
463    pub min_samples: usize,
464    /// Keep every column of the series and add the outputs after them
465    /// (pond-ts `{ append: true }`), instead of a result holding only the
466    /// key and the outputs. An output may not take the name of an existing
467    /// column, so [`Agg::of`](crate::Agg::of) (output named after its
468    /// source) is an error here; use [`Agg::new`](crate::Agg::new).
469    pub append: bool,
470}
471
472/// Options for [`TimeSeries::rolling_over`]. `RollingOver::default()` is
473/// pond-ts's default: a trailing window ending at each bucket's start,
474/// over the series' own time range, every bucket given a value.
475#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
476pub struct RollingOver {
477    /// Where the window sits around each bucket's point (default: trailing).
478    pub alignment: Alignment,
479    /// As [`Rolling::min_samples`], per bucket.
480    pub min_samples: usize,
481    /// The point of each bucket the window is placed at: its start, middle
482    /// or end. Also picks which buckets are used, as in
483    /// [`Align::sample`](crate::Align::sample).
484    pub sample: At,
485    /// As [`Align::range`](crate::Align::range).
486    pub range: Option<(i64, i64)>,
487}
488
489/// How long a window is: a time span in ms, or a number of rows. Or the
490/// windows themselves, one run of rows per output row, each starting and
491/// ending no earlier than the one before (`rolling_by_column`).
492#[derive(Clone, Copy)]
493pub(crate) enum Span<'a> {
494    Time(i64),
495    Rows(usize),
496    Windows(&'a [std::ops::Range<usize>]),
497}
498
499/// A row's place in time for a rolling window: whole ms (`i64`), or an
500/// `f64` for anchors that can fall on a half millisecond (the middle of a
501/// time range, which `smooth` uses).
502pub(crate) trait Anchor: Copy + PartialEq {
503    fn ms(self) -> f64;
504}
505
506impl Anchor for i64 {
507    fn ms(self) -> f64 {
508        // Keys are whole ms within ±8.64e15, so they convert exactly.
509        self as f64
510    }
511}
512
513impl Anchor for f64 {
514    fn ms(self) -> f64 {
515        self
516    }
517}
518
519/// One rolling pass's windows: one per row of `times`.
520#[derive(Clone, Copy)]
521pub(crate) struct Frame<'a, T: Anchor = i64> {
522    pub times: &'a [T],
523    pub span: Span<'a>,
524    pub alignment: Alignment,
525    pub min_samples: usize,
526}
527
528impl<T: Anchor> Frame<'_, T> {
529    /// The number of output rows.
530    pub fn len(&self) -> usize {
531        match self.span {
532            Span::Windows(w) => w.len(),
533            _ => self.times.len(),
534        }
535    }
536}
537
538/// Walk `frame`'s windows in row order, as pond-ts's rolling sweep does.
539/// A time window is shared by the rows sharing a time; a row window is
540/// per row. Each window's rows are a run `start..end` of rows that only
541/// moves forward. The order rows enter and leave in is pond-ts's (it can
542/// change the last bits of a running sum): a trailing or a row window
543/// takes rows in before letting rows go; a leading or centred time window
544/// lets rows go first. A window holding fewer than `min_samples` rows is
545/// not emitted, so those rows keep the gap each output starts with.
546pub(crate) fn walk<T: Anchor>(frame: &Frame<T>, w: &mut impl Window) {
547    let times = frame.times;
548    let rows = times.len();
549    let (mut start, mut end) = (0usize, 0usize);
550    let emit = |w: &mut _, out: std::ops::Range<usize>, start: usize, end: usize| {
551        if end - start >= frame.min_samples {
552            Window::emit(w, out);
553        }
554    };
555    if let Span::Windows(windows) = frame.span {
556        // Rows enter before rows leave, as pond-ts's `rollingByColumn`
557        // sweep does.
558        for (i, r) in windows.iter().enumerate() {
559            while end < r.end {
560                w.enter(end);
561                end += 1;
562            }
563            while start < r.start {
564                w.leave(start);
565                start += 1;
566            }
567            emit(w, i..i + 1, start, end);
568        }
569        return;
570    }
571    if let Span::Rows(n) = frame.span {
572        let left = (n - 1) / 2;
573        let right = n - 1 - left;
574        for i in 0..rows {
575            let (lo, hi) = match frame.alignment {
576                Alignment::Trailing => (i.saturating_sub(n - 1), i),
577                Alignment::Leading => (i, i.saturating_add(n - 1).min(rows - 1)),
578                Alignment::Centered => (
579                    i.saturating_sub(left),
580                    i.saturating_add(right).min(rows - 1),
581                ),
582            };
583            while end <= hi {
584                w.enter(end);
585                end += 1;
586            }
587            while start < lo {
588                w.leave(start);
589                start += 1;
590            }
591            emit(w, i..i + 1, start, end);
592        }
593        return;
594    }
595    let Span::Time(window_ms) = frame.span else {
596        unreachable!()
597    };
598    let window = window_ms as f64;
599    let mut group_start = 0;
600    while group_start < rows {
601        let anchor = times[group_start];
602        let mut group_end = group_start + 1;
603        while group_end < rows && times[group_end] == anchor {
604            group_end += 1;
605        }
606        // The edges are worked out in f64 exactly as pond-ts does. A
607        // centred window an odd number of ms long has edges on a half
608        // millisecond, which f64 rounds to even past 2^52 ms; computing them
609        // another way would disagree with pond-ts there.
610        let a = anchor.ms();
611        let t = |row: usize| times[row].ms();
612        match frame.alignment {
613            Alignment::Trailing => {
614                while end < rows && t(end) <= a {
615                    w.enter(end);
616                    end += 1;
617                }
618                let lower = a - window;
619                while start < end && t(start) <= lower {
620                    w.leave(start);
621                    start += 1;
622                }
623            }
624            Alignment::Leading | Alignment::Centered => {
625                let (lower, upper) = if frame.alignment == Alignment::Leading {
626                    (a, a + window)
627                } else {
628                    let half = window / 2.0;
629                    (a - half, a + half)
630                };
631                while start < end && t(start) < lower {
632                    w.leave(start);
633                    start += 1;
634                }
635                while end < rows && t(end) < upper {
636                    w.enter(end);
637                    end += 1;
638                }
639            }
640        }
641        emit(w, group_start..group_end, start, end);
642        group_start = group_end;
643    }
644}
645
646/// A [`WindowState`] over `f64` values, skipping gaps (non-finite values).
647struct Rolled<'a, S> {
648    state: S,
649    values: &'a [f64],
650    out: Vec<f64>,
651}
652
653impl<S: WindowState> Window for Rolled<'_, S> {
654    fn enter(&mut self, row: usize) {
655        let v = self.values[row];
656        if v.is_finite() {
657            self.state.add(row, v);
658        }
659    }
660    fn leave(&mut self, row: usize) {
661        let v = self.values[row];
662        if v.is_finite() {
663            self.state.remove(row, v);
664        }
665    }
666    fn emit(&mut self, rows: std::ops::Range<usize>) {
667        // A non-finite result (an overflow) is a gap, as in pond-ts.
668        let snap = self
669            .state
670            .snapshot()
671            .filter(|v| v.is_finite())
672            .unwrap_or(f64::NAN);
673        self.out[rows].fill(snap);
674    }
675}
676
677/// Run one column through `frame`'s windows with one reducer's state.
678fn roll<S: WindowState, T: Anchor>(state: S, frame: &Frame<T>, values: &[f64]) -> Vec<f64> {
679    let mut w = Rolled {
680        state,
681        values,
682        out: vec![f64::NAN; frame.len()],
683    };
684    walk(frame, &mut w);
685    w.out
686}
687
688pub(crate) fn roll_column<T: Anchor>(
689    frame: &Frame<T>,
690    values: &[f64],
691    reducer: Reducer,
692) -> Vec<f64> {
693    match reducer {
694        Reducer::Count => roll(CountState::default(), frame, values),
695        Reducer::Sum => roll(SumState::default(), frame, values),
696        Reducer::Avg => roll(AvgState::default(), frame, values),
697        Reducer::Min => roll(ExtremeState::<false>(VecDeque::new()), frame, values),
698        Reducer::Max => roll(ExtremeState::<true>(VecDeque::new()), frame, values),
699        Reducer::First => roll(OrderedState::<false>(VecDeque::new()), frame, values),
700        Reducer::Last => roll(OrderedState::<true>(VecDeque::new()), frame, values),
701        Reducer::Median => roll(
702            SortedState {
703                sorted: Vec::new(),
704                q: 50.0,
705            },
706            frame,
707            values,
708        ),
709        Reducer::Percentile(q) => roll(
710            SortedState {
711                sorted: Vec::new(),
712                q,
713            },
714            frame,
715            values,
716        ),
717        Reducer::Stdev => roll(StdevState::default(), frame, values),
718        Reducer::Difference => roll(DiffState::default(), frame, values),
719        Reducer::Keep => roll(KeepState::default(), frame, values),
720        Reducer::Unique | Reducer::Top(_) | Reducer::Samples => {
721            unreachable!("list reducers run in elements.rs")
722        }
723    }
724}
725
726impl TimeSeries {
727    /// For each row at time `t`, reduce the rows with time in
728    /// `(t - window, t]`: a trailing window. Rows sharing a time share a
729    /// window. A row keyed by a time range or an interval counts as its
730    /// begin, as in pond-ts. The result keeps this series' keys; each mapping entry becomes
731    /// a column that may have gaps.
732    ///
733    /// **Only the outputs are kept**, as in pond-ts: a source column is gone
734    /// from the result unless an output carries it (add
735    /// `Agg::new("close", "close", Reducer::Last)` to keep `close` beside
736    /// its window stats). For the stats over the last window only, without
737    /// a value per row, use [`TimeSeries::tail`] and
738    /// [`TimeSeries::reduce`].
739    ///
740    /// ```
741    /// use pond_rs::{Agg, Column, Reducer, TimeSeries};
742    /// let s = TimeSeries::new("cpu", vec![0, 1_000, 2_000],
743    ///     vec![Column::optional("v", [Some(1.0), None, Some(3.0)])])?;
744    /// let r = s.rolling("2s", &[Agg::new("v_avg", "v", Reducer::Avg)])?;
745    /// assert_eq!(r.column("v_avg")?.numbers()?.get(2), Some(3.0)); // (0s, 2s] holds only 3.0
746    /// # Ok::<(), pond_rs::PondError>(())
747    /// ```
748    ///
749    /// Column order follows pond-ts: entries made with [`Agg::new`] keep the
750    /// order given; when every entry is an [`Agg::of`] shorthand, columns
751    /// come out in this series' schema order.
752    ///
753    /// Gaps: a gap is not a value. `count` counts present values, `sum` of
754    /// none is 0, and the other reducers give a gap when the window has no
755    /// present values.
756    ///
757    /// Text, true/false and array columns take `count` (a number column),
758    /// `first`, `last` and `keep` (a column of the same kind), and `unique`,
759    /// `top<N>` and `samples` (an array column, an empty list for a window
760    /// with nothing present); any other reducer on them is an error. Decimal columns take every reducer:
761    /// `first`, `last`, `min`, `max`, `keep`, `sum` and `difference` give
762    /// exact decimals (`sum` and `difference` at precision 38, an error past
763    /// it), the rest give numbers.
764    ///
765    /// Signed zeros follow pond-ts's window states: a window of zeros gives
766    /// `keep` 0 whatever their signs, and `difference` can be `-0`: pond-ts
767    /// keeps a window's zeros as a stack of signs and answers the bottom
768    /// minus the top, so the sign depends on the order zeros came and went.
769    pub fn rolling(&self, window: &str, aggs: &[Agg]) -> Result<TimeSeries> {
770        self.rolling_with(window, aggs, Rolling::default())
771    }
772
773    /// [`TimeSeries::rolling`] with a choice of where the window sits
774    /// ([`Alignment`]) and a minimum number of rows per window
775    /// ([`Rolling::min_samples`]), as pond-ts
776    /// `rolling(window, mapping, { alignment, minSamples })`. A leading
777    /// window for a row at `t` holds the rows with time in
778    /// `[t, t + window)`; a centred one, `[t - window/2, t + window/2)`.
779    ///
780    /// ```
781    /// use pond_rs::{Agg, Alignment, Column, Reducer, Rolling, TimeSeries};
782    /// let s = TimeSeries::new("cpu", vec![0, 1_000, 2_000],
783    ///     vec![Column::required("v", vec![1.0, 2.0, 3.0])])?;
784    /// let next = s.rolling_with("2s", &[Agg::new("v_sum", "v", Reducer::Sum)],
785    ///     Rolling { alignment: Alignment::Leading, ..Rolling::default() })?;
786    /// assert_eq!(next.column("v_sum")?.numbers()?.get(0), Some(3.0)); // [0s, 2s) holds 1 and 2
787    /// # Ok::<(), pond_rs::PondError>(())
788    /// ```
789    pub fn rolling_with(&self, window: &str, aggs: &[Agg], options: Rolling) -> Result<TimeSeries> {
790        let window_ms = parse_duration(window)?;
791        self.roll(Span::Time(window_ms), aggs, options)
792    }
793
794    /// For each row, reduce a window of `rows` rows, whatever their times
795    /// (pond-ts `rolling({ count: rows }, mapping, options)`): the row and
796    /// the `rows - 1` before it by default. This is the "N-bar" window of a
797    /// trading study, which a time window gets wrong across weekends and
798    /// overnight. Rows sharing a time do not share a window here. Near
799    /// either end the window holds fewer rows; set
800    /// [`Rolling::min_samples`] to `rows` to give those rows gaps instead.
801    ///
802    /// ```
803    /// use pond_rs::{Agg, Column, Reducer, Rolling, TimeSeries};
804    /// let s = TimeSeries::new("bars", vec![0, 60_000, 259_200_000],
805    ///     vec![Column::required("close", vec![10.0, 12.0, 14.0])])?;
806    /// let sma2 = s.rolling_rows(2, &[Agg::new("sma", "close", Reducer::Avg)],
807    ///     Rolling { min_samples: 2, ..Rolling::default() })?;
808    /// let sma = sma2.column("sma")?.numbers()?;
809    /// assert_eq!((sma.get(0), sma.get(2)), (None, Some(13.0))); // across the weekend
810    /// # Ok::<(), pond_rs::PondError>(())
811    /// ```
812    ///
813    /// `rows` must be at least 1.
814    pub fn rolling_rows(&self, rows: usize, aggs: &[Agg], options: Rolling) -> Result<TimeSeries> {
815        if rows == 0 {
816            return Err(PondError::InvalidData {
817                call: CALL,
818                message: "a row window must hold at least 1 row".into(),
819            });
820        }
821        self.roll(Span::Rows(rows), aggs, options)
822    }
823
824    fn roll(&self, span: Span, aggs: &[Agg], options: Rolling) -> Result<TimeSeries> {
825        // Range- and interval-keyed rows are anchored at their begin, as in
826        // pond-ts.
827        let frame = Frame {
828            times: self.key().begin(),
829            span,
830            alignment: options.alignment,
831            min_samples: options.min_samples,
832        };
833        let plan = self.rolling_plan(aggs, self.key_name())?;
834        if options.append {
835            for agg in aggs {
836                let taken = agg.output == self.key_name()
837                    || self.columns().iter().any(|c| c.name() == agg.output);
838                if taken {
839                    return Err(PondError::InvalidData {
840                        call: CALL,
841                        message: format!(
842                            "output column '{}' already exists; with append, give each output its own name (Agg::new)",
843                            agg.output
844                        ),
845                    });
846                }
847            }
848        }
849        let mut columns = self.roll_columns(CALL, &frame, &plan)?;
850        if options.append {
851            let mut all = self.columns().to_vec();
852            all.append(&mut columns);
853            columns = all;
854        }
855        Ok(TimeSeries::from_parts(
856            self.name().to_string(),
857            self.key_name().to_string(),
858            self.shared_key(),
859            columns,
860        ))
861    }
862
863    /// Each entry of `plan` (a source column and its mapping entry) rolled
864    /// through `frame`'s windows, one output column each.
865    pub(crate) fn roll_columns(
866        &self,
867        call: &'static str,
868        frame: &Frame,
869        plan: &[(usize, &Agg)],
870    ) -> Result<Vec<Column>> {
871        for &(source, agg) in plan {
872            check_magnitude(call, &self.columns()[source], agg.reducer)?;
873        }
874        let frame = *frame;
875        let used: Vec<(usize, Reducer)> = plan
876            .iter()
877            .map(|&(source, agg)| (source, agg.reducer))
878            .collect();
879        let inputs = Inputs::build(self.columns(), &used);
880        crate::par::map(plan, |&(source, agg)| {
881            let data = self.columns()[source].data();
882            if agg.reducer.gives_list() {
883                let lists = Elements::new(data).roll(agg.reducer, &frame);
884                return Ok(Column::from_data(agg.output.clone(), lists, false));
885            }
886            let kind = input_kind(data, agg.reducer);
887            if let (ColumnData::Decimal(d), InputKind::ExactSum) = (data, kind) {
888                return crate::decimal::rolling_sum(call, &agg.output, d, &frame);
889            }
890            let input = inputs.get(source, kind);
891            if let (ColumnData::Decimal(d), Reducer::Difference) = (data, agg.reducer) {
892                let lo = roll_column(&frame, &input.values, Reducer::Min);
893                let hi = roll_column(&frame, &input.values, Reducer::Max);
894                return crate::decimal::difference(call, &agg.output, d, input, &lo, &hi);
895            }
896            let out = roll_column(&frame, &input.values, agg.reducer);
897            Ok(output_column(
898                agg.output.clone(),
899                data,
900                agg.reducer,
901                input,
902                out,
903            ))
904        })
905        .into_iter()
906        .collect()
907    }
908
909    /// One row per bucket of `sequence`, holding the reducers over a window
910    /// of `window` placed at the bucket (pond-ts
911    /// `rolling(sequence, window, mapping, options)`): a smoothed line on a
912    /// regular grid, such as a 5-minute average every minute. The window
913    /// sits at each bucket's sample point ([`RollingOver::sample`], its
914    /// start by default) as [`Alignment`] says, holding the rows whose
915    /// time (or start, for time-range and interval keys) is in it.
916    ///
917    /// ```
918    /// use pond_rs::{Agg, Column, Reducer, RollingOver, Sequence, TimeSeries};
919    /// let s = TimeSeries::new("cpu", vec![0, 30_000, 60_000, 90_000],
920    ///     vec![Column::required("v", vec![1.0, 2.0, 3.0, 4.0])])?;
921    /// let r = s.rolling_over(&Sequence::every("1m")?, "1m",
922    ///     &[Agg::new("v_avg", "v", Reducer::Avg)], RollingOver::default())?;
923    /// assert_eq!(r.key().begin(), &[0, 60_000]);
924    /// assert_eq!(r.column("v_avg")?.numbers()?.get(1), Some(2.5)); // (0s, 60s] holds 2 and 3
925    /// # Ok::<(), pond_rs::PondError>(())
926    /// ```
927    ///
928    /// The result is keyed by the buckets (an interval labelled by its
929    /// start, named `"interval"`). Buckets are picked as in
930    /// [`TimeSeries::align`]: those whose sample point is in
931    /// [`RollingOver::range`], by default the series' own range.
932    ///
933    /// Each window is reduced as [`TimeSeries::aggregate`] reduces a bucket,
934    /// with the same reducers, kinds and rules (an empty window gives
935    /// `count` 0, `sum` 0, an empty list for the list reducers, and gaps
936    /// otherwise); columns come out in the order [`TimeSeries::rolling`]
937    /// gives. An empty series with no range gives an empty result. An
938    /// output named `"interval"` is an error.
939    pub fn rolling_over(
940        &self,
941        sequence: &Sequence,
942        window: &str,
943        aggs: &[Agg],
944        options: RollingOver,
945    ) -> Result<TimeSeries> {
946        let window_ms = parse_duration(window)?;
947        let plan = self.rolling_plan(aggs, "interval")?;
948        let buckets = match self.grid_range(CALL, options.range)? {
949            None => crate::grid::no_buckets(),
950            Some((begin, end)) => sequence.buckets_sampled(begin, end, options.sample)?,
951        };
952        let ranges = grid_windows(
953            self.key().begin(),
954            &buckets,
955            options.sample,
956            window_ms,
957            options.alignment,
958        );
959        let plan: Vec<(&Agg, usize)> = plan
960            .into_iter()
961            .map(|(source, agg)| (agg, source))
962            .collect();
963        let mut columns = self.reduce_columns(CALL, &plan, &ranges)?;
964        if ranges.iter().any(|r| r.len() < options.min_samples) {
965            let keep: Vec<Option<usize>> = ranges
966                .iter()
967                .enumerate()
968                .map(|(i, r)| (r.len() >= options.min_samples).then_some(i))
969                .collect();
970            for c in &mut columns {
971                *c = Column::from_data(c.name().to_string(), c.data().take(&keep), false);
972            }
973        }
974        Ok(TimeSeries::from_parts(
975            self.name().to_string(),
976            "interval".to_string(),
977            Key::Interval(buckets),
978            columns,
979        ))
980    }
981
982    /// Check `aggs` for a rolling call and pair each with its source
983    /// column, in output order: the order given, or this series' schema
984    /// order when every entry is an [`Agg::of`] shorthand (pond-ts sorts
985    /// those). `key` is the result's key name, which no output may take.
986    fn rolling_plan<'a>(&self, aggs: &'a [Agg], key: &str) -> Result<Vec<(usize, &'a Agg)>> {
987        let mut plan = Vec::with_capacity(aggs.len());
988        for (i, agg) in aggs.iter().enumerate() {
989            let source = self.column_index(CALL, &agg.source)?;
990            agg.reducer.check(CALL)?;
991            if agg.output == key || aggs[..i].iter().any(|a| a.output == agg.output) {
992                return Err(PondError::DuplicateOutput {
993                    call: CALL,
994                    column: agg.output.clone(),
995                });
996            }
997            check_source_kind(CALL, &self.columns()[source], agg.reducer)?;
998            plan.push((source, agg));
999        }
1000        if aggs.iter().all(|a| a.shorthand) {
1001            // Stable, so equal positions keep their given order.
1002            plan.sort_by_key(|&(source, _)| source);
1003        }
1004        Ok(plan)
1005    }
1006}
1007
1008/// Each bucket's window as a run of rows. `starts` are the rows' start
1009/// times (sorted). The window sits at the bucket's sample point, and both
1010/// are worked out in f64 exactly as pond-ts does (`sampleTime` and
1011/// `anchorInWindow`), as in [`walk`].
1012fn grid_windows(
1013    starts: &[i64],
1014    buckets: &IntervalKeys,
1015    sample: At,
1016    window_ms: i64,
1017    alignment: Alignment,
1018) -> Vec<std::ops::Range<usize>> {
1019    let w = window_ms as f64;
1020    let t = |row: usize| starts[row] as f64;
1021    let n = starts.len();
1022    let (mut lo, mut hi) = (0usize, 0usize);
1023    buckets
1024        .begin
1025        .iter()
1026        .zip(&buckets.end)
1027        .map(|(&b, &e)| {
1028            let (b, e) = (b as f64, e as f64);
1029            let p = match sample {
1030                At::Begin => b,
1031                At::Center => b + (e - b) / 2.0,
1032                At::End => e,
1033            };
1034            // A row is in when `from <= t < to` (trailing: `from < t <= to`).
1035            let (from, to) = match alignment {
1036                Alignment::Trailing => (p - w, p),
1037                Alignment::Leading => (p, p + w),
1038                Alignment::Centered => (p - w / 2.0, p + w / 2.0),
1039            };
1040            let trailing = alignment == Alignment::Trailing;
1041            while lo < n && (t(lo) < from || (trailing && t(lo) == from)) {
1042                lo += 1;
1043            }
1044            hi = hi.max(lo);
1045            while hi < n && (t(hi) < to || (trailing && t(hi) == to)) {
1046                hi += 1;
1047            }
1048            lo..hi
1049        })
1050        .collect()
1051}
1052
1053/// Refuse values pond-ts would route to its big-integer lanes.
1054pub(crate) fn check_magnitude(call: &'static str, column: &Column, reducer: Reducer) -> Result<()> {
1055    check_magnitude_in(call, column, reducer, None)
1056}
1057
1058/// [`check_magnitude`] over only `rows` of the column (all rows when
1059/// `None`), naming the row as numbered in the column.
1060pub(crate) fn check_magnitude_in(
1061    call: &'static str,
1062    column: &Column,
1063    reducer: Reducer,
1064    rows: Option<&[u32]>,
1065) -> Result<()> {
1066    let split = match reducer {
1067        Reducer::Sum | Reducer::Avg => SUM_SPLIT,
1068        Reducer::Stdev => STDEV_SPLIT,
1069        _ => return Ok(()),
1070    };
1071    // Decimals reach the f64 kernels below 1e76 (38 digits, scale down to
1072    // -38), far under either split (2^480 is about 3e144).
1073    let Ok(values) = column.numbers().map(|c| c.values()) else {
1074        return Ok(());
1075    };
1076    let too_big = |&row: &usize| values[row].is_finite() && values[row].abs() >= split;
1077    let found = match rows {
1078        None => (0..values.len()).find(too_big),
1079        Some(rows) => rows.iter().map(|&r| r as usize).find(too_big),
1080    };
1081    match found {
1082        None => Ok(()),
1083        Some(row) => Err(PondError::Unsupported {
1084            call,
1085            message: format!(
1086                "column '{}' row {row} holds {:e}, too large for a rolling {:?} (limit {split:e})",
1087                column.name(),
1088                values[row],
1089                reducer
1090            ),
1091        }),
1092    }
1093}
1094
1095#[cfg(test)]
1096mod tests {
1097    use super::*;
1098
1099    #[test]
1100    fn extreme_times_keep_their_own_row() {
1101        use crate::MAX_TIME_MS;
1102        for t in [-MAX_TIME_MS, MAX_TIME_MS] {
1103            let s = TimeSeries::new("s", vec![t], vec![Column::required("v", vec![7.0])]).unwrap();
1104            for window in ["1ms", "100000000d"] {
1105                let r = s
1106                    .rolling(window, &[Agg::new("n", "v", Reducer::Count)])
1107                    .unwrap();
1108                assert_eq!(
1109                    r.column("n").unwrap().numbers().unwrap().get(0),
1110                    Some(1.0),
1111                    "{t} {window}"
1112                );
1113            }
1114        }
1115        let too_early =
1116            TimeSeries::new("s", vec![i64::MIN], vec![Column::required("v", vec![7.0])]);
1117        assert!(too_early.is_err());
1118    }
1119
1120    /// `DiffState` against a line-for-line port of pond-ts's sorted window,
1121    /// on windows full of zeros of both signs.
1122    #[test]
1123    fn difference_matches_the_sorted_window_bit_for_bit() {
1124        struct Sorted(Vec<f64>);
1125        impl WindowState for Sorted {
1126            fn add(&mut self, _: usize, v: f64) {
1127                let at = lower_bound(&self.0, v);
1128                self.0.insert(at, v);
1129            }
1130            fn remove(&mut self, _: usize, v: f64) {
1131                let at = lower_bound(&self.0, v);
1132                if at < self.0.len() && self.0[at] == v {
1133                    self.0.remove(at);
1134                }
1135            }
1136            fn snapshot(&mut self) -> Option<f64> {
1137                Some(self.0.last()? - self.0.first()?)
1138            }
1139        }
1140        let mut seed = 11u64;
1141        let mut next = || {
1142            seed = seed
1143                .wrapping_mul(6364136223846793005)
1144                .wrapping_add(1442695040888963407);
1145            seed >> 33
1146        };
1147        for _ in 0..300 {
1148            let n = 1 + next() as usize % 60;
1149            let mut t = 0;
1150            let times: Vec<i64> = (0..n)
1151                .map(|_| {
1152                    t += next() as i64 % 3;
1153                    t
1154                })
1155                .collect();
1156            let values: Vec<f64> = (0..n)
1157                .map(|_| [0.0, -0.0, 0.0, -0.0, 1.0, -2.0, f64::NAN][next() as usize % 7])
1158                .collect();
1159            let window = 1 + next() as i64 % 8;
1160            let frame = Frame {
1161                times: &times,
1162                span: Span::Time(window),
1163                alignment: Alignment::Trailing,
1164                min_samples: 0,
1165            };
1166            let want = roll(Sorted(Vec::new()), &frame, &values);
1167            let got = roll(DiffState::default(), &frame, &values);
1168            let bits = |xs: &[f64]| xs.iter().map(|x| x.to_bits()).collect::<Vec<_>>();
1169            assert_eq!(bits(&got), bits(&want), "{times:?} {values:?} {window}");
1170        }
1171    }
1172
1173    #[test]
1174    fn splits_are_the_pond_ts_powers_of_two() {
1175        assert_eq!(SUM_SPLIT, 2f64.powi(960));
1176        assert_eq!(STDEV_SPLIT, 2f64.powi(480));
1177    }
1178}