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 stride = iter.get_stride(placement.start_range.start());
40 continue 'bounds;
41 }
42 }
43
44 let mut has_member = false;
45 for &t in &time[start..time.len().saturating_sub(1)] {
47 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 start += 1;
58 }
59
60 stride = if has_member {
62 0
63 } else {
64 debug_assert!(start < time.len());
65 iter.get_stride(time[start])
66 };
67
68 let mut end = start;
70
71 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#[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 let boundary = if time.len() > 1 {
131 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#[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 let mut start = if let Some(&t) = time.get(start_offset) {
217 let lower = offset.add(tu, t, tz.as_ref())?;
218 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 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 if b.is_member_exit(*t, closed_window) {
258 end = i;
259 } else {
260 end = std::cmp::max(end, start);
261 }
262 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#[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 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#[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 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
392fn 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)]
410pub(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 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 group_by_values_iter_lookbehind(period, offset, time, closed_window, tu, tz, 0, None)
480}
481
482fn 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 new.push(window[0])
502 }
503 }
504 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 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 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
579pub 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 let mut thread_offsets = _split_offsets(time.len(), RAYON.current_num_threads());
608 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 let run_parallel = !RAYON.current_thread_has_pending_tasks().unwrap_or(false);
619
620 if offset.negative && !offset.is_zero() {
622 if offset == -period {
624 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 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 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 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 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 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); 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 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 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 past_range: bool,
989
990 num_seen: IdxSize,
991 origin: i64,
993 next_k: i64,
995 next_lower_bound: i64,
996 active: VecDeque<ActiveDynWindow>,
997
998 prev_upper_bound: Option<i64>,
1000 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 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 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 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 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}