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 for &t in &time[start..time.len().saturating_sub(1)] {
41 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 start += 1;
52 }
53
54 stride = if has_member {
56 0
57 } else {
58 debug_assert!(start < time.len());
59 iter.get_stride(time[start])
60 };
61
62 let mut end = start;
64
65 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#[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 let boundary = if time.len() > 1 {
124 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#[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 let mut start = if let Some(&t) = time.get(start_offset) {
212 let lower = add(&offset, t, tz.as_ref())?;
213 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 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 if b.is_member_exit(*t, closed_window) {
253 end = i;
254 } else {
255 end = std::cmp::max(end, start);
256 }
257 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
272pub(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 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
333pub(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 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)]
390pub(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 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 group_by_values_iter_lookbehind(period, offset, time, closed_window, tu, tz, 0, None)
465}
466
467fn 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 new.push(window[0])
487 }
488 }
489 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 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 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
564pub 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 prune_splits_on_duplicates(time, &mut thread_offsets);
586
587 let run_parallel = !RAYON.current_thread_has_pending_tasks().unwrap_or(false);
589
590 if offset.negative && !offset.is_zero() {
592 if offset.duration_ns() == period.duration_ns() {
594 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 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 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 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 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 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); 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 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 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 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 prev_upper_bound: Option<i64>,
957 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 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 let start = (self.add)(
1093 &Duration::parse(&format!("{}d", self.start_by.weekday().unwrap())),
1094 start,
1095 self.tz.as_ref(),
1096 )?;
1097 let start = (self.add)(&self.offset, start, self.tz.as_ref())?;
1099 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 let start = (self.add)(
1121 &Duration::parse(&format!("{}d", self.start_by.weekday().unwrap())),
1122 start,
1123 None,
1124 )
1125 .unwrap();
1126 let start = (self.add)(&self.offset, start, None).unwrap();
1128 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 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 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}