Skip to main content

polars_time/windows/
group_by.rs

1use std::collections::VecDeque;
2
3use polars_arrow::legacy::time_zone::Tz;
4use polars_arrow::trusted_len::TrustedLen;
5use polars_core::prelude::*;
6use polars_core::runtime::RAYON;
7use polars_core::utils::_split_offsets;
8use polars_core::utils::flatten::flatten_par;
9use polars_defs::time::duration::Duration;
10use polars_defs::time::group_by::{ClosedWindow, DynamicWindowPlacement, StartBy};
11use rayon::prelude::*;
12
13use crate::prelude::*;
14
15#[allow(clippy::too_many_arguments)]
16fn update_groups_and_bounds(
17    bounds_iter: BoundsIter<'_>,
18    mut start: usize,
19    time: &[i64],
20    closed_window: ClosedWindow,
21    include_lower_bound: bool,
22    include_upper_bound: bool,
23    placement: Option<&DynamicWindowPlacement>,
24    lower_bound: &mut Vec<i64>,
25    upper_bound: &mut Vec<i64>,
26    groups: &mut Vec<[IdxSize; 2]>,
27) {
28    let mut iter = bounds_iter.into_iter();
29    let mut stride = 0;
30
31    'bounds: while let Some(bi) = iter.nth(stride) {
32        if let Some(placement) = placement {
33            if placement.start_range.is_past(bi.start) {
34                break;
35            }
36            if placement.start_range.is_before(bi.start) {
37                // Skip windows that start before the range. `get_stride` never overshoots,
38                // so windows close to the range are still visited one by one.
39                stride = iter.get_stride(placement.start_range.start());
40                continue 'bounds;
41            }
42        }
43
44        let mut has_member = false;
45        // find starting point of window
46        for &t in &time[start..time.len().saturating_sub(1)] {
47            // the window is behind the time values.
48            if bi.is_future(t, closed_window) {
49                stride = iter.get_stride(t);
50                continue 'bounds;
51            }
52            if bi.is_member_entry(t, closed_window) {
53                has_member = true;
54                break;
55            }
56            // element drops out of the window
57            start += 1;
58        }
59
60        // update stride so we can fast-forward in case of sparse data
61        stride = if has_member {
62            0
63        } else {
64            debug_assert!(start < time.len());
65            iter.get_stride(time[start])
66        };
67
68        // find members of this window
69        let mut end = start;
70
71        // last value isn't always added
72        if end == time.len() - 1 {
73            let t = time[end];
74            if bi.is_member(t, closed_window) {
75                if include_lower_bound {
76                    lower_bound.push(bi.start);
77                }
78                if include_upper_bound {
79                    upper_bound.push(bi.stop);
80                }
81                groups.push([end as IdxSize, 1])
82            }
83            continue;
84        }
85        for &t in &time[end..] {
86            if !bi.is_member_exit(t, closed_window) {
87                break;
88            }
89            end += 1;
90        }
91        let len = end - start;
92
93        if include_lower_bound {
94            lower_bound.push(bi.start);
95        }
96        if include_upper_bound {
97            upper_bound.push(bi.stop);
98        }
99        groups.push([start as IdxSize, len as IdxSize])
100    }
101}
102
103/// Window boundaries are created based on the given `Window`, which is defined by:
104/// - every
105/// - period
106/// - offset
107///
108/// And every window boundary we search for the values that fit that window by the given
109/// `ClosedWindow`. The groups are returned as `GroupTuples` together with the lower bound and upper
110/// bound timestamps. These timestamps indicate the start (lower) and end (upper) of the window of
111/// that group.
112///
113/// If `include_boundaries` is `false` those `lower` and `upper` vectors will be empty.
114#[allow(clippy::too_many_arguments)]
115pub fn group_by_windows(
116    window: Window,
117    time: &[i64],
118    closed_window: ClosedWindow,
119    tu: TimeUnit,
120    tz: &Option<TimeZone>,
121    include_lower_bound: bool,
122    include_upper_bound: bool,
123    start_by: StartBy,
124    placement: Option<DynamicWindowPlacement>,
125) -> PolarsResult<(GroupsSlice, Vec<i64>, Vec<i64>)> {
126    let start = time[0];
127    // the boundary we define here is not yet correct. It doesn't take 'period' into account
128    // and it doesn't have the proper starting point. This boundary is used as a proxy to find
129    // the proper 'boundary' in  'window.get_overlapping_bounds_iter'.
130    let boundary = if time.len() > 1 {
131        // +1 because left or closed boundary could match the next window if it is on the boundary
132        let stop = time[time.len() - 1] + 1;
133        Bounds::new_checked(start, stop)
134    } else {
135        let stop = start + 1;
136        Bounds::new_checked(start, stop)
137    };
138
139    let size = window.estimate_overlapping_bounds(tu, boundary);
140    let size_lower = if include_lower_bound { size } else { 0 };
141    let size_upper = if include_upper_bound { size } else { 0 };
142    let mut lower_bound = Vec::with_capacity(size_lower);
143    let mut upper_bound = Vec::with_capacity(size_upper);
144
145    let mut groups = Vec::with_capacity(size);
146    let start_offset = 0;
147
148    match tz {
149        #[cfg(feature = "timezones")]
150        Some(tz) => {
151            update_groups_and_bounds(
152                window.get_overlapping_bounds_iter(
153                    boundary,
154                    closed_window,
155                    tu,
156                    tz.parse::<Tz>().ok().as_ref(),
157                    start_by,
158                    placement.map(|p| p.origin),
159                )?,
160                start_offset,
161                time,
162                closed_window,
163                include_lower_bound,
164                include_upper_bound,
165                placement.as_ref(),
166                &mut lower_bound,
167                &mut upper_bound,
168                &mut groups,
169            );
170        },
171        _ => {
172            update_groups_and_bounds(
173                window.get_overlapping_bounds_iter(
174                    boundary,
175                    closed_window,
176                    tu,
177                    None,
178                    start_by,
179                    placement.map(|p| p.origin),
180                )?,
181                start_offset,
182                time,
183                closed_window,
184                include_lower_bound,
185                include_upper_bound,
186                placement.as_ref(),
187                &mut lower_bound,
188                &mut upper_bound,
189                &mut groups,
190            );
191        },
192    };
193
194    Ok((groups, lower_bound, upper_bound))
195}
196
197// t is right at the end of the window
198// ------t---
199// [------]
200#[inline]
201#[allow(clippy::too_many_arguments)]
202pub(crate) fn group_by_values_iter_lookbehind(
203    period: Duration,
204    offset: Duration,
205    time: &[i64],
206    closed_window: ClosedWindow,
207    tu: TimeUnit,
208    tz: Option<Tz>,
209    start_offset: usize,
210    upper_bound: Option<usize>,
211) -> PolarsResult<impl TrustedLen<Item = PolarsResult<(IdxSize, IdxSize)>> + '_> {
212    debug_assert_eq!(offset, -period);
213
214    let upper_bound = upper_bound.unwrap_or(time.len());
215    // Use binary search to find the initial start as that is behind.
216    let mut start = if let Some(&t) = time.get(start_offset) {
217        let lower = offset.add(tu, t, tz.as_ref())?;
218        // We have `period == -offset`, so `t + offset + period` is equal to `t`,
219        // and `upper` is trivially equal to `t` itself. Using the trivial calculation,
220        // instead of `upper = lower + period`, avoids issues around
221        // `t - 1mo + 1mo` not round-tripping.
222        let upper = t;
223        let b = Bounds::new(lower, upper);
224        let slice = &time[..start_offset];
225        slice.partition_point(|v| !b.is_member(*v, closed_window))
226    } else {
227        0
228    };
229    let mut end = start;
230    let mut last = time[start_offset];
231    Ok(time[start_offset..upper_bound]
232        .iter()
233        .enumerate()
234        .map(move |(mut i, t)| {
235            // Fast path for duplicates.
236            if *t == last && i > 0 {
237                let len = end - start;
238                let offset = start as IdxSize;
239                return Ok((offset, len as IdxSize));
240            }
241            last = *t;
242            i += start_offset;
243
244            let lower = offset.add(tu, *t, tz.as_ref())?;
245            let upper = *t;
246
247            let b = Bounds::new(lower, upper);
248
249            for &t in unsafe { time.get_unchecked(start..i) } {
250                if b.is_member_entry(t, closed_window) {
251                    break;
252                }
253                start += 1;
254            }
255
256            // faster path, check if `i` is member.
257            if b.is_member_exit(*t, closed_window) {
258                end = i;
259            } else {
260                end = std::cmp::max(end, start);
261            }
262            // we still must loop to consume duplicates
263            for &t in unsafe { time.get_unchecked(end..) } {
264                if !b.is_member_exit(t, closed_window) {
265                    break;
266                }
267                end += 1;
268            }
269
270            let len = end - start;
271            let offset = start as IdxSize;
272
273            Ok((offset, len as IdxSize))
274        }))
275}
276
277// this one is correct for all lookbehind/lookaheads, but is slower
278// window is completely behind t and t itself is not a member
279// ---------------t---
280//  [---]
281#[allow(clippy::too_many_arguments)]
282pub(crate) fn group_by_values_iter_window_behind_t(
283    period: Duration,
284    offset: Duration,
285    time: &[i64],
286    closed_window: ClosedWindow,
287    tu: TimeUnit,
288    tz: Option<Tz>,
289    start_offset: usize,
290    upper_bound: Option<usize>,
291) -> PolarsResult<impl TrustedLen<Item = PolarsResult<(IdxSize, IdxSize)>> + '_> {
292    let upper_bound = upper_bound.unwrap_or(time.len());
293    let mut start = first_member_row(period, offset, time, closed_window, tu, tz, start_offset)?;
294    let mut end = start;
295    let mut last = time[start_offset];
296    let mut started = false;
297    Ok(time[start_offset..upper_bound].iter().map(move |lower| {
298        // Fast path for duplicates.
299        if *lower == last && started {
300            let len = end - start;
301            let offset = start as IdxSize;
302            return Ok((offset, len as IdxSize));
303        }
304        last = *lower;
305        started = true;
306        let lower = offset.add(tu, *lower, tz.as_ref())?;
307        let upper = period.add(tu, lower, tz.as_ref())?;
308
309        let b = Bounds::new(lower, upper);
310        if b.is_future(time[0], closed_window) {
311            Ok((0, 0))
312        } else {
313            for &t in &time[start..] {
314                if b.is_member_entry(t, closed_window) {
315                    break;
316                }
317                start += 1;
318            }
319
320            end = std::cmp::max(start, end);
321            for &t in &time[end..] {
322                if !b.is_member_exit(t, closed_window) {
323                    break;
324                }
325                end += 1;
326            }
327
328            let len = end - start;
329            let offset = start as IdxSize;
330
331            Ok((offset, len as IdxSize))
332        }
333    }))
334}
335
336// window is within -1 periods of t
337// ----t---
338//  [---]
339#[allow(clippy::too_many_arguments)]
340pub(crate) fn group_by_values_iter_partial_lookbehind(
341    period: Duration,
342    offset: Duration,
343    time: &[i64],
344    closed_window: ClosedWindow,
345    tu: TimeUnit,
346    tz: Option<Tz>,
347    start_offset: usize,
348    upper_bound: Option<usize>,
349) -> PolarsResult<impl TrustedLen<Item = PolarsResult<(IdxSize, IdxSize)>> + '_> {
350    let upper_bound = upper_bound.unwrap_or(time.len());
351    let mut start = first_member_row(period, offset, time, closed_window, tu, tz, start_offset)?;
352    let mut end = start;
353    let mut last = time[start_offset];
354    let rows = &time[start_offset..upper_bound];
355    Ok(rows.iter().enumerate().map(move |(i, lower)| {
356        // Fast path for duplicates.
357        if *lower == last && i > 0 {
358            let len = end - start;
359            let offset = start as IdxSize;
360            return Ok((offset, len as IdxSize));
361        }
362        last = *lower;
363        let i = i + start_offset;
364
365        let lower = offset.add(tu, *lower, tz.as_ref())?;
366        let upper = period.add(tu, lower, tz.as_ref())?;
367
368        let b = Bounds::new(lower, upper);
369
370        for &t in &time[start..] {
371            if b.is_member_entry(t, closed_window) || start == i {
372                break;
373            }
374            start += 1;
375        }
376
377        end = std::cmp::max(start, end);
378        for &t in &time[end..] {
379            if !b.is_member_exit(t, closed_window) {
380                break;
381            }
382            end += 1;
383        }
384
385        let len = end - start;
386        let offset = start as IdxSize;
387
388        Ok((offset, len as IdxSize))
389    }))
390}
391
392/// The first row of `time[..start_offset]` that can be a member of the window of row
393/// `start_offset`, so a window search need not walk the rows before it.
394fn first_member_row(
395    period: Duration,
396    offset: Duration,
397    time: &[i64],
398    closed_window: ClosedWindow,
399    tu: TimeUnit,
400    tz: Option<Tz>,
401    start_offset: usize,
402) -> PolarsResult<usize> {
403    let lower = offset.add(tu, time[start_offset], tz.as_ref())?;
404    let upper = period.add(tu, lower, tz.as_ref())?;
405    let b = Bounds::new(lower, upper);
406    Ok(time[..start_offset].partition_point(|&t| !b.is_member_entry(t, closed_window)))
407}
408
409#[allow(clippy::too_many_arguments)]
410// window is completely ahead of t and t itself is not a member
411// --t-----------
412//        [---]
413pub(crate) fn group_by_values_iter_lookahead(
414    period: Duration,
415    offset: Duration,
416    time: &[i64],
417    closed_window: ClosedWindow,
418    tu: TimeUnit,
419    tz: Option<Tz>,
420    start_offset: usize,
421    upper_bound: Option<usize>,
422) -> impl TrustedLen<Item = PolarsResult<(IdxSize, IdxSize)>> + '_ {
423    let upper_bound = upper_bound.unwrap_or(time.len());
424
425    let mut start = start_offset;
426    let mut end = start;
427
428    let mut last = time[start_offset];
429    let mut started = false;
430    time[start_offset..upper_bound].iter().map(move |lower| {
431        // Fast path for duplicates.
432        if *lower == last && started {
433            let len = end - start;
434            let offset = start as IdxSize;
435            return Ok((offset, len as IdxSize));
436        }
437        started = true;
438        last = *lower;
439
440        let lower = offset.add(tu, *lower, tz.as_ref())?;
441        let upper = period.add(tu, lower, tz.as_ref())?;
442
443        let b = Bounds::new(lower, upper);
444
445        for &t in &time[start..] {
446            if b.is_member_entry(t, closed_window) {
447                break;
448            }
449            start += 1;
450        }
451
452        end = std::cmp::max(start, end);
453        for &t in &time[end..] {
454            if !b.is_member_exit(t, closed_window) {
455                break;
456            }
457            end += 1;
458        }
459
460        let len = end - start;
461        let offset = start as IdxSize;
462
463        Ok((offset, len as IdxSize))
464    })
465}
466
467#[cfg(feature = "rolling_window_by")]
468#[inline]
469pub(crate) fn group_by_values_iter(
470    period: Duration,
471    time: &[i64],
472    closed_window: ClosedWindow,
473    tu: TimeUnit,
474    tz: Option<Tz>,
475) -> PolarsResult<impl TrustedLen<Item = PolarsResult<(IdxSize, IdxSize)>> + '_> {
476    let mut offset = period;
477    offset.negative = true;
478    // t is at the right endpoint of the window
479    group_by_values_iter_lookbehind(period, offset, time, closed_window, tu, tz, 0, None)
480}
481
482/// Checks if the boundary elements don't split on duplicates.
483/// If they do we remove them
484fn prune_splits_on_duplicates(time: &[i64], thread_offsets: &mut Vec<(usize, usize)>) {
485    let is_valid = |window: &[(usize, usize)]| -> bool {
486        debug_assert_eq!(window.len(), 2);
487        let left_block_end = window[0].0 + window[0].1.saturating_sub(1);
488        let right_block_start = window[1].0;
489        time[left_block_end] != time[right_block_start]
490    };
491
492    if time.is_empty() || thread_offsets.len() <= 1 || thread_offsets.windows(2).all(is_valid) {
493        return;
494    }
495
496    let mut new = vec![];
497    for window in thread_offsets.windows(2) {
498        let this_block_is_valid = is_valid(window);
499        if this_block_is_valid {
500            // Only push left block
501            new.push(window[0])
502        }
503    }
504    // Check last block
505    if thread_offsets.len().is_multiple_of(2) {
506        let window = &thread_offsets[thread_offsets.len() - 2..];
507        if is_valid(window) {
508            new.push(thread_offsets[thread_offsets.len() - 1])
509        }
510    }
511    // We pruned invalid blocks, now we must correct the lengths.
512    if new.len() <= 1 {
513        new = vec![(0, time.len())];
514    } else {
515        let mut previous_start = time.len();
516        for window in new.iter_mut().rev() {
517            window.1 = previous_start - window.0;
518            previous_start = window.0;
519        }
520        new[0].0 = 0;
521        new[0].1 = new[1].0;
522        debug_assert_eq!(new.iter().map(|w| w.1).sum::<usize>(), time.len());
523        // Call again to check.
524        prune_splits_on_duplicates(time, &mut new)
525    }
526    std::mem::swap(thread_offsets, &mut new);
527}
528
529#[allow(clippy::too_many_arguments)]
530fn group_by_values_iter_lookbehind_collected(
531    period: Duration,
532    offset: Duration,
533    time: &[i64],
534    closed_window: ClosedWindow,
535    tu: TimeUnit,
536    tz: Option<Tz>,
537    start_offset: usize,
538    upper_bound: Option<usize>,
539) -> PolarsResult<Vec<[IdxSize; 2]>> {
540    let iter = group_by_values_iter_lookbehind(
541        period,
542        offset,
543        time,
544        closed_window,
545        tu,
546        tz,
547        start_offset,
548        upper_bound,
549    )?;
550    iter.map(|result| result.map(|(offset, len)| [offset, len]))
551        .collect::<PolarsResult<Vec<_>>>()
552}
553
554#[allow(clippy::too_many_arguments)]
555pub(crate) fn group_by_values_iter_lookahead_collected(
556    period: Duration,
557    offset: Duration,
558    time: &[i64],
559    closed_window: ClosedWindow,
560    tu: TimeUnit,
561    tz: Option<Tz>,
562    start_offset: usize,
563    upper_bound: Option<usize>,
564) -> PolarsResult<Vec<[IdxSize; 2]>> {
565    let iter = group_by_values_iter_lookahead(
566        period,
567        offset,
568        time,
569        closed_window,
570        tu,
571        tz,
572        start_offset,
573        upper_bound,
574    );
575    iter.map(|result| result.map(|(offset, len)| [offset as IdxSize, len]))
576        .collect::<PolarsResult<Vec<_>>>()
577}
578
579/// Different from `group_by_windows`, where define window buckets and search which values fit that
580/// pre-defined bucket.
581///
582/// This function defines every window based on the:
583///     - timestamp (lower bound)
584///     - timestamp + period (upper bound)
585/// where timestamps are the individual values in the array `time`
586///
587/// Only the rows in `rows` get a window; the windows still index into the whole of `time`.
588/// `rows` must not start or end inside a run of equal values.
589pub fn group_by_values(
590    period: Duration,
591    offset: Duration,
592    time: &[i64],
593    closed_window: ClosedWindow,
594    tu: TimeUnit,
595    tz: Option<Tz>,
596    rows: std::ops::Range<usize>,
597) -> PolarsResult<GroupsSlice> {
598    if rows.is_empty() {
599        return Ok(GroupsSlice::from(vec![]));
600    }
601    assert!(rows.start == 0 || time[rows.start - 1] < time[rows.start]);
602    assert!(rows.end == time.len() || time[rows.end - 1] < time[rows.end]);
603    let (start_offset, upper_bound) = (rows.start, rows.end);
604
605    // The threads split the whole of `time` and each takes the part of its split inside `rows`,
606    // so the windows do not depend on `rows`.
607    let mut thread_offsets = _split_offsets(time.len(), RAYON.current_num_threads());
608    // there are duplicates in the splits, so we opt for a single partition
609    prune_splits_on_duplicates(time, &mut thread_offsets);
610    thread_offsets.retain_mut(|(offset, len)| {
611        let start = (*offset).max(start_offset);
612        let end = (*offset + *len).min(upper_bound);
613        (*offset, *len) = (start, end.saturating_sub(start));
614        *len > 0
615    });
616
617    // If we start from within parallel work we will do this single threaded.
618    let run_parallel = !RAYON.current_thread_has_pending_tasks().unwrap_or(false);
619
620    // we have a (partial) lookbehind window
621    if offset.negative && !offset.is_zero() {
622        // lookbehind
623        if offset == -period {
624            // t is right at the end of the window
625            // ------t---
626            // [------]
627            if !run_parallel {
628                let vecs = group_by_values_iter_lookbehind_collected(
629                    period,
630                    offset,
631                    time,
632                    closed_window,
633                    tu,
634                    tz,
635                    start_offset,
636                    Some(upper_bound),
637                )?;
638                return Ok(GroupsSlice::from(vecs));
639            }
640
641            RAYON.install(|| {
642                let vals = thread_offsets
643                    .par_iter()
644                    .copied()
645                    .map(|(base_offset, len)| {
646                        let upper_bound = base_offset + len;
647                        group_by_values_iter_lookbehind_collected(
648                            period,
649                            offset,
650                            time,
651                            closed_window,
652                            tu,
653                            tz,
654                            base_offset,
655                            Some(upper_bound),
656                        )
657                    })
658                    .collect::<PolarsResult<Vec<_>>>()?;
659                Ok(flatten_par(&vals))
660            })
661        } else if ((offset.duration_ns() >= period.duration_ns())
662            && matches!(closed_window, ClosedWindow::Left | ClosedWindow::None))
663            || ((offset.duration_ns() > period.duration_ns())
664                && matches!(closed_window, ClosedWindow::Right | ClosedWindow::Both))
665        {
666            // The window is likely completely behind t. `duration_ns` is only an estimate for
667            // calendar units, but both this and the partial lookbehind below are correct for any
668            // negative offset, so it only picks the faster one.
669            // ---------------t---
670            //  [---]
671            let iter = group_by_values_iter_window_behind_t(
672                period,
673                offset,
674                time,
675                closed_window,
676                tu,
677                tz,
678                start_offset,
679                Some(upper_bound),
680            )?;
681            iter.map(|result| result.map(|(offset, len)| [offset, len]))
682                .collect::<PolarsResult<_>>()
683        }
684        // partial lookbehind
685        // this one is still single threaded
686        // can make it parallel later, it's a bit more complicated because the boundaries are unknown
687        // window is within -1 periods of t
688        // ----t---
689        //  [---]
690        else {
691            let iter = group_by_values_iter_partial_lookbehind(
692                period,
693                offset,
694                time,
695                closed_window,
696                tu,
697                tz,
698                start_offset,
699                Some(upper_bound),
700            )?;
701            iter.map(|result| result.map(|(offset, len)| [offset, len]))
702                .collect::<PolarsResult<_>>()
703        }
704    } else if !offset.is_zero()
705        || closed_window == ClosedWindow::Right
706        || closed_window == ClosedWindow::None
707    {
708        // window is completely ahead of t and t itself is not a member
709        // --t-----------
710        //        [---]
711
712        if !run_parallel {
713            let vecs = group_by_values_iter_lookahead_collected(
714                period,
715                offset,
716                time,
717                closed_window,
718                tu,
719                tz,
720                start_offset,
721                Some(upper_bound),
722            )?;
723            return Ok(GroupsSlice::from(vecs));
724        }
725
726        RAYON.install(|| {
727            let vals = thread_offsets
728                .par_iter()
729                .copied()
730                .map(|(base_offset, len)| {
731                    let lower_bound = base_offset;
732                    let upper_bound = base_offset + len;
733                    group_by_values_iter_lookahead_collected(
734                        period,
735                        offset,
736                        time,
737                        closed_window,
738                        tu,
739                        tz,
740                        lower_bound,
741                        Some(upper_bound),
742                    )
743                })
744                .collect::<PolarsResult<Vec<_>>>()?;
745            Ok(flatten_par(&vals))
746        })
747    } else {
748        if !run_parallel {
749            let vecs = group_by_values_iter_lookahead_collected(
750                period,
751                offset,
752                time,
753                closed_window,
754                tu,
755                tz,
756                start_offset,
757                Some(upper_bound),
758            )?;
759            return Ok(GroupsSlice::from(vecs));
760        }
761
762        // Offset is 0 and window is closed on the left:
763        // it must be that the window starts at t and t is a member
764        // --t-----------
765        //  [---]
766        RAYON.install(|| {
767            let vals = thread_offsets
768                .par_iter()
769                .copied()
770                .map(|(base_offset, len)| {
771                    let lower_bound = base_offset;
772                    let upper_bound = base_offset + len;
773                    group_by_values_iter_lookahead_collected(
774                        period,
775                        offset,
776                        time,
777                        closed_window,
778                        tu,
779                        tz,
780                        lower_bound,
781                        Some(upper_bound),
782                    )
783                })
784                .collect::<PolarsResult<Vec<_>>>()?;
785            Ok(flatten_par(&vals))
786        })
787    }
788}
789
790pub struct RollingWindower {
791    period: Duration,
792    offset: Duration,
793    closed: ClosedWindow,
794
795    tu: TimeUnit,
796    tz: Option<Tz>,
797
798    start: IdxSize,
799    end: IdxSize,
800    length: IdxSize,
801
802    active: VecDeque<ActiveWindow>,
803}
804
805struct ActiveWindow {
806    start: i64,
807    end: i64,
808}
809
810impl ActiveWindow {
811    #[inline(always)]
812    fn above_lower_bound(&self, t: i64, closed: ClosedWindow) -> bool {
813        (t > self.start)
814            | (matches!(closed, ClosedWindow::Left | ClosedWindow::Both) & (t == self.start))
815    }
816
817    #[inline(always)]
818    fn below_upper_bound(&self, t: i64, closed: ClosedWindow) -> bool {
819        (t < self.end)
820            | (matches!(closed, ClosedWindow::Right | ClosedWindow::Both) & (t == self.end))
821    }
822}
823
824fn skip_in_2d_list(l: &[&[i64]], mut n: usize) -> (usize, usize) {
825    let mut y = 0;
826    while y < l.len() && (n >= l[y].len() || l[y].is_empty()) {
827        n -= l[y].len();
828        y += 1;
829    }
830    assert!(n == 0 || y < l.len());
831    (n, y)
832}
833fn increment_2d(x: &mut usize, y: &mut usize, l: &[&[i64]]) {
834    *x += 1;
835    while *y < l.len() && *x == l[*y].len() {
836        *y += 1;
837        *x = 0;
838    }
839}
840
841impl RollingWindower {
842    pub fn new(
843        period: Duration,
844        offset: Duration,
845        closed: ClosedWindow,
846        tu: TimeUnit,
847        tz: Option<Tz>,
848    ) -> Self {
849        Self {
850            period,
851            offset,
852            closed,
853
854            tu,
855            tz,
856
857            start: 0,
858            end: 0,
859            length: 0,
860
861            active: Default::default(),
862        }
863    }
864
865    /// Insert new values into the windower.
866    ///
867    /// This should be given all the old values that were not processed yet.
868    pub fn insert(
869        &mut self,
870        time: &[&[i64]],
871        windows: &mut Vec<[IdxSize; 2]>,
872    ) -> PolarsResult<IdxSize> {
873        let (mut i_x, mut i_y) = skip_in_2d_list(time, (self.length - self.start) as usize);
874        let (mut s_x, mut s_y) = skip_in_2d_list(time, 0); // skip over empty lists
875        let (mut e_x, mut e_y) = skip_in_2d_list(time, (self.end - self.start) as usize);
876
877        let time_start = self.start;
878        let mut i = self.length;
879        while i_y < time.len() {
880            let t = time[i_y][i_x];
881            let window_start = self.offset.add(self.tu, t, self.tz.as_ref())?;
882            // For datetime arithmetic, it does *NOT* hold 0 + a - a == 0. Therefore, we make sure
883            // that if `offset` and `period` are inverses we keep the `t`.
884            let window_end = if self.offset == -self.period {
885                t
886            } else {
887                self.period.add(self.tu, window_start, self.tz.as_ref())?
888            };
889
890            self.active.push_back(ActiveWindow {
891                start: window_start,
892                end: window_end,
893            });
894
895            while let Some(w) = self.active.front() {
896                if w.below_upper_bound(t, self.closed) {
897                    break;
898                }
899
900                let w = self.active.pop_front().unwrap();
901                while self.start < i && !w.above_lower_bound(time[s_y][s_x], self.closed) {
902                    increment_2d(&mut s_x, &mut s_y, time);
903                    self.start += 1;
904                }
905                while self.end < i && w.below_upper_bound(time[e_y][e_x], self.closed) {
906                    increment_2d(&mut e_x, &mut e_y, time);
907                    self.end += 1;
908                }
909                windows.push([self.start, self.end - self.start]);
910            }
911
912            increment_2d(&mut i_x, &mut i_y, time);
913            i += 1;
914        }
915
916        self.length = i;
917        Ok(self.start - time_start)
918    }
919
920    /// Process all remaining items and signal that no more items are coming.
921    pub fn finalize(&mut self, time: &[&[i64]], windows: &mut Vec<[IdxSize; 2]>) {
922        assert_eq!(
923            time.iter().map(|t| t.len()).sum::<usize>() as IdxSize,
924            self.length - self.start
925        );
926
927        let (mut s_x, mut s_y) = skip_in_2d_list(time, 0);
928        let (mut e_x, mut e_y) = skip_in_2d_list(time, (self.end - self.start) as usize);
929
930        windows.extend(self.active.drain(..).map(|w| {
931            while self.start < self.length && !w.above_lower_bound(time[s_y][s_x], self.closed) {
932                increment_2d(&mut s_x, &mut s_y, time);
933                self.start += 1;
934            }
935            while self.end < self.length && w.below_upper_bound(time[e_y][e_x], self.closed) {
936                increment_2d(&mut e_x, &mut e_y, time);
937                self.end += 1;
938            }
939            [self.start, self.end - self.start]
940        }));
941
942        self.start = 0;
943        self.end = 0;
944        self.length = 0;
945    }
946
947    pub fn reset(&mut self) {
948        self.active.clear();
949        self.start = 0;
950        self.end = 0;
951        self.length = 0;
952    }
953}
954
955#[derive(Debug)]
956struct ActiveDynWindow {
957    start: IdxSize,
958    end: Option<IdxSize>,
959    lower_bound: i64,
960    upper_bound: i64,
961}
962
963#[inline(always)]
964fn is_above_lower_bound(t: i64, lb: i64, closed: ClosedWindow) -> bool {
965    (t > lb) | (matches!(closed, ClosedWindow::Left | ClosedWindow::Both) & (t == lb))
966}
967#[inline(always)]
968fn is_below_upper_bound(t: i64, ub: i64, closed: ClosedWindow) -> bool {
969    (t < ub) | (matches!(closed, ClosedWindow::Right | ClosedWindow::Both) & (t == ub))
970}
971
972pub struct GroupByDynamicWindower {
973    period: Duration,
974    offset: Duration,
975    every: Duration,
976    closed: ClosedWindow,
977
978    start_by: StartBy,
979
980    tu: TimeUnit,
981    tz: Option<Tz>,
982
983    include_lower_bound: bool,
984    include_upper_bound: bool,
985
986    placement: Option<DynamicWindowPlacement>,
987    /// No window can start in the placement's range any more.
988    past_range: bool,
989
990    num_seen: IdxSize,
991    /// Window `k` starts at `origin + every * k`.
992    origin: i64,
993    /// Index and start of the next window that may open.
994    next_k: i64,
995    next_lower_bound: i64,
996    active: VecDeque<ActiveDynWindow>,
997
998    /// Upper bound of the most recently opened window.
999    prev_upper_bound: Option<i64>,
1000    // Kept track of because of clamping/DST
1001    non_monotonic_upper_bounds: bool,
1002}
1003
1004impl GroupByDynamicWindower {
1005    #[expect(clippy::too_many_arguments)]
1006    pub fn new(
1007        period: Duration,
1008        offset: Duration,
1009        every: Duration,
1010        start_by: StartBy,
1011        closed: ClosedWindow,
1012        tu: TimeUnit,
1013        tz: Option<Tz>,
1014        include_lower_bound: bool,
1015        include_upper_bound: bool,
1016        placement: Option<DynamicWindowPlacement>,
1017    ) -> Self {
1018        Self {
1019            period,
1020            offset,
1021            every,
1022            closed,
1023
1024            start_by,
1025
1026            tu,
1027            tz,
1028
1029            include_lower_bound,
1030            include_upper_bound,
1031
1032            placement,
1033            past_range: false,
1034
1035            num_seen: 0,
1036            origin: 0,
1037            next_k: 0,
1038            next_lower_bound: 0,
1039            active: Default::default(),
1040            prev_upper_bound: None,
1041            non_monotonic_upper_bounds: false,
1042        }
1043    }
1044
1045    fn window_start(&self, k: i64) -> PolarsResult<i64> {
1046        (self.every * k).add(self.tu, self.origin, self.tz.as_ref())
1047    }
1048
1049    /// Find the first window from window `k` on whose upper bound is not behind `target`.
1050    /// Returns its index and bounds if it contains `target`, otherwise its index and start.
1051    fn find_first_window_around(&self, mut k: i64, target: i64) -> PolarsResult<FirstWindowAround> {
1052        let mut lower_bound = self.window_start(k)?;
1053        let mut upper_bound = self.period.add(self.tu, lower_bound, self.tz.as_ref())?;
1054        while !is_below_upper_bound(target, upper_bound, self.closed) {
1055            let gap = target - lower_bound;
1056            let every = self.every.nte_duration(self.tu);
1057            let period = self.period.nte_duration(self.tu);
1058            let nth = if gap > every + period {
1059                (gap - period) / every
1060            } else {
1061                1
1062            };
1063
1064            k += nth;
1065            lower_bound = self.window_start(k)?;
1066            upper_bound = self.period.add(self.tu, lower_bound, self.tz.as_ref())?;
1067        }
1068
1069        if is_above_lower_bound(target, lower_bound, self.closed) {
1070            Ok(FirstWindowAround::ContainingTarget {
1071                k,
1072                lower_bound,
1073                upper_bound,
1074            })
1075        } else {
1076            Ok(FirstWindowAround::NotContainingTarget { k, lower_bound })
1077        }
1078    }
1079
1080    fn start_lower_bound(&self, first: i64) -> PolarsResult<i64> {
1081        match self.placement {
1082            Some(placement) => Ok(placement.origin),
1083            None => Window::new(self.every, self.period, self.offset).first_window_start(
1084                first,
1085                self.closed,
1086                self.tu,
1087                self.tz.as_ref(),
1088                self.start_by,
1089            ),
1090        }
1091    }
1092
1093    pub fn insert(
1094        &mut self,
1095        time: &[i64],
1096        windows: &mut Vec<[IdxSize; 2]>,
1097        lower_bound: &mut Vec<i64>,
1098        upper_bound: &mut Vec<i64>,
1099    ) -> PolarsResult<()> {
1100        if time.is_empty() {
1101            return Ok(());
1102        }
1103
1104        if self.num_seen == 0 {
1105            debug_assert!(self.active.is_empty());
1106            self.origin = self.start_lower_bound(time[0])?;
1107            self.next_k = 0;
1108            self.next_lower_bound = self.origin;
1109        }
1110
1111        for &t in time {
1112            // Close every window that `t` has moved past.
1113            // This sets flags for monotonicity.
1114            if self.non_monotonic_upper_bounds {
1115                for w in self.active.iter_mut() {
1116                    if w.end.is_none() && !is_below_upper_bound(t, w.upper_bound, self.closed) {
1117                        w.end = Some(self.num_seen);
1118                    }
1119                }
1120
1121                while let Some(w) = self.active.front()
1122                    && w.end.is_some()
1123                {
1124                    let w = self.active.pop_front().unwrap();
1125                    Self::emit(
1126                        &w,
1127                        self.num_seen,
1128                        self.include_lower_bound,
1129                        self.include_upper_bound,
1130                        windows,
1131                        lower_bound,
1132                        upper_bound,
1133                    );
1134                }
1135            } else {
1136                while let Some(w) = self.active.front()
1137                    && !is_below_upper_bound(t, w.upper_bound, self.closed)
1138                {
1139                    let w = self.active.pop_front().unwrap();
1140                    Self::emit(
1141                        &w,
1142                        self.num_seen,
1143                        self.include_lower_bound,
1144                        self.include_upper_bound,
1145                        windows,
1146                        lower_bound,
1147                        upper_bound,
1148                    );
1149                }
1150            }
1151
1152            while !self.past_range && is_above_lower_bound(t, self.next_lower_bound, self.closed) {
1153                match self.find_first_window_around(self.next_k, t)? {
1154                    FirstWindowAround::ContainingTarget {
1155                        k,
1156                        lower_bound,
1157                        upper_bound,
1158                    } => {
1159                        self.next_k = k + 1;
1160                        self.next_lower_bound = self.window_start(self.next_k)?;
1161                        if let Some(placement) = &self.placement {
1162                            if placement.start_range.is_past(lower_bound) {
1163                                self.past_range = true;
1164                                break;
1165                            }
1166                            if placement.start_range.is_before(lower_bound) {
1167                                continue;
1168                            }
1169                        }
1170                        self.non_monotonic_upper_bounds |=
1171                            self.prev_upper_bound.is_some_and(|prev| upper_bound < prev);
1172                        self.prev_upper_bound = Some(upper_bound);
1173                        self.active.push_back(ActiveDynWindow {
1174                            start: self.num_seen,
1175                            end: None,
1176                            lower_bound,
1177                            upper_bound,
1178                        });
1179                    },
1180                    FirstWindowAround::NotContainingTarget { k, lower_bound } => {
1181                        self.next_k = k;
1182                        self.next_lower_bound = lower_bound;
1183                        break;
1184                    },
1185                }
1186            }
1187
1188            self.num_seen += 1
1189        }
1190
1191        Ok(())
1192    }
1193
1194    fn emit(
1195        w: &ActiveDynWindow,
1196        num_seen: IdxSize,
1197        include_lower_bound: bool,
1198        include_upper_bound: bool,
1199        windows: &mut Vec<[IdxSize; 2]>,
1200        lower_bound: &mut Vec<i64>,
1201        upper_bound: &mut Vec<i64>,
1202    ) {
1203        let end = w.end.unwrap_or(num_seen);
1204        windows.push([w.start, end - w.start]);
1205        if include_lower_bound {
1206            lower_bound.push(w.lower_bound);
1207        }
1208        if include_upper_bound {
1209            upper_bound.push(w.upper_bound);
1210        }
1211    }
1212
1213    pub fn lowest_needed_index(&self) -> IdxSize {
1214        self.active.front().map_or(self.num_seen, |w| w.start)
1215    }
1216
1217    /// Whether no further input can produce a window: every window that could start in the
1218    /// placement's range has been opened and closed.
1219    pub fn is_done(&self) -> bool {
1220        self.past_range && self.active.is_empty()
1221    }
1222
1223    pub fn finalize(
1224        &mut self,
1225        windows: &mut Vec<[IdxSize; 2]>,
1226        lower_bound: &mut Vec<i64>,
1227        upper_bound: &mut Vec<i64>,
1228    ) {
1229        let num_seen = self.num_seen;
1230        let (include_lower_bound, include_upper_bound) =
1231            (self.include_lower_bound, self.include_upper_bound);
1232        for w in self.active.drain(..) {
1233            Self::emit(
1234                &w,
1235                num_seen,
1236                include_lower_bound,
1237                include_upper_bound,
1238                windows,
1239                lower_bound,
1240                upper_bound,
1241            );
1242        }
1243
1244        self.origin = 0;
1245        self.next_k = 0;
1246        self.next_lower_bound = 0;
1247        self.num_seen = 0;
1248        self.prev_upper_bound = None;
1249        self.non_monotonic_upper_bounds = false;
1250        self.past_range = false;
1251    }
1252
1253    pub fn num_seen(&self) -> IdxSize {
1254        self.num_seen
1255    }
1256
1257    pub fn time_unit(&self) -> TimeUnit {
1258        self.tu
1259    }
1260}
1261
1262enum FirstWindowAround {
1263    ContainingTarget {
1264        k: i64,
1265        lower_bound: i64,
1266        upper_bound: i64,
1267    },
1268    NotContainingTarget {
1269        k: i64,
1270        lower_bound: i64,
1271    },
1272}
1273
1274#[cfg(test)]
1275mod test {
1276    use super::*;
1277
1278    #[test]
1279    fn test_prune_duplicates() {
1280        //                     |--|------------|----|---------|
1281        //                     0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10
1282        let time = &[0, 1, 1, 2, 2, 2, 3, 4, 5, 6, 5];
1283        let mut splits = vec![(0, 2), (2, 4), (6, 2), (8, 3)];
1284        prune_splits_on_duplicates(time, &mut splits);
1285        assert_eq!(splits, &[(0, 6), (6, 2), (8, 3)]);
1286    }
1287}