Skip to main content

polars_time/windows/
window.rs

1use chrono::NaiveDateTime;
2#[cfg(feature = "timezones")]
3use chrono::TimeZone;
4use now::DateTimeNow;
5use polars_arrow::legacy::time_zone::Tz;
6use polars_arrow::temporal_conversions::*;
7use polars_core::prelude::*;
8use polars_defs::time::duration::Duration;
9use polars_defs::time::group_by::{ClosedWindow, StartBy};
10
11use crate::prelude::*;
12
13/// Ensure that earliest datapoint (`t`) is in, or in front of, first window.
14///
15/// For example, if we have:
16///
17/// - first datapoint is `2020-01-01 01:00`
18/// - `every` is `'1d'`
19/// - `period` is `'2d'`
20/// - `offset` is `'6h'`
21///
22/// then truncating the earliest datapoint by `every` and adding `offset` results
23/// in the window `[2020-01-01 06:00, 2020-01-03 06:00)`. To give the earliest datapoint
24/// a chance of being included, we then shift the window back by `every` to
25/// `[2019-12-31 06:00, 2020-01-02 06:00)`.
26#[allow(clippy::too_many_arguments)]
27pub(crate) fn ensure_t_in_or_in_front_of_window(
28    mut every: Duration,
29    t: i64,
30    offset_fn: fn(&Duration, i64, Option<&Tz>) -> PolarsResult<i64>,
31    nte_duration_fn: fn(&Duration) -> i64,
32    period: Duration,
33    mut start: i64,
34    closed_window: ClosedWindow,
35    tz: Option<&Tz>,
36) -> PolarsResult<Bounds> {
37    every.negative = !every.negative;
38    let mut stop = offset_fn(&period, start, tz)?;
39
40    while Bounds::new(start, stop).is_past(t, closed_window) {
41        let mut gap = start - t;
42        if matches!(closed_window, ClosedWindow::Right | ClosedWindow::None) {
43            gap += 1;
44        }
45        debug_assert!(gap >= 1);
46
47        // Ceil division
48        let stride = (gap + nte_duration_fn(&every) - 1) / nte_duration_fn(&every);
49        debug_assert!(stride >= 1);
50        let stride = std::cmp::max(stride, 1);
51
52        start = offset_fn(&(every * stride), start, tz)?;
53        stop = offset_fn(&period, start, tz)?;
54    }
55    Ok(Bounds::new_checked(start, stop))
56}
57
58/// Represents a window in time
59#[derive(Copy, Clone)]
60pub struct Window {
61    // The ith window start is expressed via this equation:
62    //   window_start_i = zero + every * i
63    //   window_stop_i = zero + every * i + period
64    pub(crate) every: Duration,
65    pub(crate) period: Duration,
66    pub offset: Duration,
67}
68
69impl Window {
70    pub fn new(every: Duration, period: Duration, offset: Duration) -> Self {
71        debug_assert!(!every.negative);
72        Self {
73            every,
74            period,
75            offset,
76        }
77    }
78
79    /// Truncate the given ns timestamp by the window boundary.
80    pub fn truncate_ns(&self, t: i64, tz: Option<&Tz>) -> PolarsResult<i64> {
81        self.every.truncate_ns(t, tz)
82    }
83
84    /// Truncate the given us timestamp by the window boundary.
85    pub fn truncate_us(&self, t: i64, tz: Option<&Tz>) -> PolarsResult<i64> {
86        self.every.truncate_us(t, tz)
87    }
88
89    /// Truncate the given ms timestamp by the window boundary.
90    pub fn truncate_ms(&self, t: i64, tz: Option<&Tz>) -> PolarsResult<i64> {
91        self.every.truncate_ms(t, tz)
92    }
93
94    /// Round the given ns timestamp by the window boundary.
95    pub fn round_ns(&self, t: i64, tz: Option<&Tz>) -> PolarsResult<i64> {
96        let t = t + self.every.duration_ns() / 2_i64;
97        self.truncate_ns(t, tz)
98    }
99
100    /// Round the given us timestamp by the window boundary.
101    pub fn round_us(&self, t: i64, tz: Option<&Tz>) -> PolarsResult<i64> {
102        let t = t + self.every.duration_ns()
103            / (2 * timeunit_scale(ArrowTimeUnit::Nanosecond, ArrowTimeUnit::Microsecond) as i64);
104        self.truncate_us(t, tz)
105    }
106
107    /// Round the given ms timestamp by the window boundary.
108    pub fn round_ms(&self, t: i64, tz: Option<&Tz>) -> PolarsResult<i64> {
109        let t = t + self.every.duration_ns()
110            / (2 * timeunit_scale(ArrowTimeUnit::Nanosecond, ArrowTimeUnit::Millisecond) as i64);
111        self.truncate_ms(t, tz)
112    }
113
114    /// returns the bounds for the earliest window bounds
115    /// that contains the given time t.  For underlapping windows that
116    /// do not contain time t, the window directly after time t will be returned.
117    pub fn get_earliest_bounds_ns(
118        &self,
119        t: i64,
120        closed_window: ClosedWindow,
121        tz: Option<&Tz>,
122    ) -> PolarsResult<Bounds> {
123        let start = self.truncate_ns(t, tz)?;
124        let start = self.offset.add_ns(start, tz)?;
125        ensure_t_in_or_in_front_of_window(
126            self.every,
127            t,
128            Duration::add_ns,
129            Duration::nte_duration_ns,
130            self.period,
131            start,
132            closed_window,
133            tz,
134        )
135    }
136
137    pub fn get_earliest_bounds_us(
138        &self,
139        t: i64,
140        closed_window: ClosedWindow,
141        tz: Option<&Tz>,
142    ) -> PolarsResult<Bounds> {
143        let start = self.truncate_us(t, tz)?;
144        let start = self.offset.add_us(start, tz)?;
145        ensure_t_in_or_in_front_of_window(
146            self.every,
147            t,
148            Duration::add_us,
149            Duration::nte_duration_us,
150            self.period,
151            start,
152            closed_window,
153            tz,
154        )
155    }
156
157    pub fn get_earliest_bounds_ms(
158        &self,
159        t: i64,
160        closed_window: ClosedWindow,
161        tz: Option<&Tz>,
162    ) -> PolarsResult<Bounds> {
163        let start = self.truncate_ms(t, tz)?;
164        let start = self.offset.add_ms(start, tz)?;
165        ensure_t_in_or_in_front_of_window(
166            self.every,
167            t,
168            Duration::add_ms,
169            Duration::nte_duration_ms,
170            self.period,
171            start,
172            closed_window,
173            tz,
174        )
175    }
176
177    pub(crate) fn estimate_overlapping_bounds_ns(&self, boundary: Bounds) -> usize {
178        (boundary.duration() / self.every.duration_ns()
179            + self.period.duration_ns() / self.every.duration_ns()) as usize
180    }
181
182    pub(crate) fn estimate_overlapping_bounds_us(&self, boundary: Bounds) -> usize {
183        (boundary.duration() / self.every.duration_us()
184            + self.period.duration_us() / self.every.duration_us()) as usize
185    }
186
187    pub(crate) fn estimate_overlapping_bounds_ms(&self, boundary: Bounds) -> usize {
188        (boundary.duration() / self.every.duration_ms()
189            + self.period.duration_ms() / self.every.duration_ms()) as usize
190    }
191
192    pub fn get_overlapping_bounds_iter<'a>(
193        &'a self,
194        boundary: Bounds,
195        closed_window: ClosedWindow,
196        tu: TimeUnit,
197        tz: Option<&'a Tz>,
198        start_by: StartBy,
199    ) -> PolarsResult<BoundsIter<'a>> {
200        BoundsIter::new(*self, closed_window, boundary, tu, tz, start_by)
201    }
202}
203
204pub struct BoundsIter<'a> {
205    window: Window,
206    // wrapping boundary
207    boundary: Bounds,
208    // boundary per window iterator
209    bi: Bounds,
210    tu: TimeUnit,
211    tz: Option<&'a Tz>,
212}
213impl<'a> BoundsIter<'a> {
214    fn new(
215        window: Window,
216        closed_window: ClosedWindow,
217        boundary: Bounds,
218        tu: TimeUnit,
219        tz: Option<&'a Tz>,
220        start_by: StartBy,
221    ) -> PolarsResult<Self> {
222        let bi = match start_by {
223            StartBy::DataPoint => {
224                let mut boundary = boundary;
225                let offset_fn = match tu {
226                    TimeUnit::Nanoseconds => Duration::add_ns,
227                    TimeUnit::Microseconds => Duration::add_us,
228                    TimeUnit::Milliseconds => Duration::add_ms,
229                };
230                boundary.stop = offset_fn(&window.period, boundary.start, tz)?;
231                boundary
232            },
233            StartBy::WindowBound => match tu {
234                TimeUnit::Nanoseconds => {
235                    window.get_earliest_bounds_ns(boundary.start, closed_window, tz)?
236                },
237                TimeUnit::Microseconds => {
238                    window.get_earliest_bounds_us(boundary.start, closed_window, tz)?
239                },
240                TimeUnit::Milliseconds => {
241                    window.get_earliest_bounds_ms(boundary.start, closed_window, tz)?
242                },
243            },
244            _ => {
245                {
246                    #[allow(clippy::type_complexity)]
247                    let (from, to, offset_fn, nte_duration_fn): (
248                        fn(i64) -> NaiveDateTime,
249                        fn(NaiveDateTime) -> i64,
250                        fn(&Duration, i64, Option<&Tz>) -> PolarsResult<i64>,
251                        fn(&Duration) -> i64,
252                    ) = match tu {
253                        TimeUnit::Nanoseconds => (
254                            timestamp_ns_to_datetime,
255                            datetime_to_timestamp_ns,
256                            Duration::add_ns,
257                            Duration::nte_duration_ns,
258                        ),
259                        TimeUnit::Microseconds => (
260                            timestamp_us_to_datetime,
261                            datetime_to_timestamp_us,
262                            Duration::add_us,
263                            Duration::nte_duration_us,
264                        ),
265                        TimeUnit::Milliseconds => (
266                            timestamp_ms_to_datetime,
267                            datetime_to_timestamp_ms,
268                            Duration::add_ms,
269                            Duration::nte_duration_ms,
270                        ),
271                    };
272                    // find beginning of the week.
273                    let dt = from(boundary.start);
274                    match tz {
275                        #[cfg(feature = "timezones")]
276                        Some(tz) => {
277                            let dt = tz.from_utc_datetime(&dt);
278                            let dt = dt.beginning_of_week();
279                            let dt = dt.naive_utc();
280                            let start = to(dt);
281                            // adjust start of the week based on given day of the week
282                            let start = offset_fn(
283                                &Duration::parse(&format!("{}d", start_by.weekday().unwrap())),
284                                start,
285                                Some(tz),
286                            )?;
287                            // apply the 'offset'
288                            let start = offset_fn(&window.offset, start, Some(tz))?;
289                            // make sure the first datapoint has a chance to be included
290                            // and compute the end of the window defined by the 'period'
291                            ensure_t_in_or_in_front_of_window(
292                                window.every,
293                                boundary.start,
294                                offset_fn,
295                                nte_duration_fn,
296                                window.period,
297                                start,
298                                closed_window,
299                                Some(tz),
300                            )?
301                        },
302                        _ => {
303                            let tz = chrono::Utc;
304                            let dt = dt.and_local_timezone(tz).unwrap();
305                            let dt = dt.beginning_of_week();
306                            let dt = dt.naive_utc();
307                            let start = to(dt);
308                            // adjust start of the week based on given day of the week
309                            let start = offset_fn(
310                                &Duration::parse(&format!("{}d", start_by.weekday().unwrap())),
311                                start,
312                                None,
313                            )
314                            .unwrap();
315                            // apply the 'offset'
316                            let start = offset_fn(&window.offset, start, None).unwrap();
317                            // make sure the first datapoint has a chance to be included
318                            // and compute the end of the window defined by the 'period'
319                            ensure_t_in_or_in_front_of_window(
320                                window.every,
321                                boundary.start,
322                                offset_fn,
323                                nte_duration_fn,
324                                window.period,
325                                start,
326                                closed_window,
327                                None,
328                            )?
329                        },
330                    }
331                }
332            },
333        };
334        Ok(Self {
335            window,
336            boundary,
337            bi,
338            tu,
339            tz,
340        })
341    }
342}
343
344impl Iterator for BoundsIter<'_> {
345    type Item = Bounds;
346
347    fn next(&mut self) -> Option<Self::Item> {
348        if self.bi.start < self.boundary.stop {
349            let out = self.bi;
350            match self.tu {
351                // TODO: find some way to propagate error instead of unwrapping?
352                // Issue is that `next` needs to return `Option`.
353                TimeUnit::Nanoseconds => {
354                    self.bi.start = self.window.every.add_ns(self.bi.start, self.tz).unwrap();
355                    self.bi.stop = self.window.period.add_ns(self.bi.start, self.tz).unwrap();
356                },
357                TimeUnit::Microseconds => {
358                    self.bi.start = self.window.every.add_us(self.bi.start, self.tz).unwrap();
359                    self.bi.stop = self.window.period.add_us(self.bi.start, self.tz).unwrap();
360                },
361                TimeUnit::Milliseconds => {
362                    self.bi.start = self.window.every.add_ms(self.bi.start, self.tz).unwrap();
363                    self.bi.stop = self.window.period.add_ms(self.bi.start, self.tz).unwrap();
364                },
365            }
366            Some(out)
367        } else {
368            None
369        }
370    }
371
372    fn nth(&mut self, n: usize) -> Option<Self::Item> {
373        let n: i64 = n.try_into().unwrap();
374        if self.bi.start < self.boundary.stop {
375            match self.tu {
376                TimeUnit::Nanoseconds => {
377                    self.bi.start = (self.window.every * n)
378                        .add_ns(self.bi.start, self.tz)
379                        .unwrap();
380                    self.bi.stop = (self.window.period).add_ns(self.bi.start, self.tz).unwrap();
381                },
382                TimeUnit::Microseconds => {
383                    self.bi.start = (self.window.every * n)
384                        .add_us(self.bi.start, self.tz)
385                        .unwrap();
386                    self.bi.stop = (self.window.period).add_us(self.bi.start, self.tz).unwrap();
387                },
388                TimeUnit::Milliseconds => {
389                    self.bi.start = (self.window.every * n)
390                        .add_ms(self.bi.start, self.tz)
391                        .unwrap();
392                    self.bi.stop = (self.window.period).add_ms(self.bi.start, self.tz).unwrap();
393                },
394            }
395            self.next()
396        } else {
397            None
398        }
399    }
400}
401
402impl<'a> BoundsIter<'a> {
403    /// Number of iterations to advance, such that the bounds are on target; or, in
404    /// the case of non-constant duration, close to target.
405    /// Follows the `nth()` convention on Iterator indexing, i.e., a return value of 0
406    /// implies advancing 1 iteration.
407    pub fn get_stride(&self, target: i64) -> usize {
408        let mut stride = 0;
409        if self.bi.start < self.boundary.stop && target > self.bi.start {
410            let gap = target - self.bi.start;
411            match self.tu {
412                TimeUnit::Nanoseconds => {
413                    if gap
414                        > self.window.every.nte_duration_ns() + self.window.period.nte_duration_ns()
415                    {
416                        stride = ((gap - self.window.period.nte_duration_ns()) as usize)
417                            / (self.window.every.nte_duration_ns() as usize);
418                    }
419                },
420                TimeUnit::Microseconds => {
421                    if gap
422                        > self.window.every.nte_duration_us() + self.window.period.nte_duration_us()
423                    {
424                        stride = ((gap - self.window.period.nte_duration_us()) as usize)
425                            / (self.window.every.nte_duration_us() as usize);
426                    }
427                },
428                TimeUnit::Milliseconds => {
429                    if gap
430                        > self.window.every.nte_duration_ms() + self.window.period.nte_duration_ms()
431                    {
432                        stride = ((gap - self.window.period.nte_duration_ms()) as usize)
433                            / (self.window.every.nte_duration_ms() as usize);
434                    }
435                },
436            }
437        }
438        stride
439    }
440}