Skip to main content

polars_time/windows/
group_by.rs

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