polars_time/windows/
duration.rs

1use std::cmp::Ordering;
2use std::fmt::{Display, Formatter};
3use std::ops::{Mul, Neg};
4
5#[cfg(feature = "timezones")]
6use arrow::legacy::kernels::{Ambiguous, NonExistent};
7use arrow::legacy::time_zone::Tz;
8use arrow::temporal_conversions::{
9    MICROSECONDS, MILLISECONDS, NANOSECONDS, timestamp_ms_to_datetime, timestamp_ns_to_datetime,
10    timestamp_us_to_datetime,
11};
12use chrono::{Datelike, NaiveDate, NaiveDateTime, NaiveTime, Timelike};
13use polars_core::datatypes::DataType;
14use polars_core::prelude::{
15    PolarsResult, TimeZone, datetime_to_timestamp_ms, datetime_to_timestamp_ns,
16    datetime_to_timestamp_us, polars_bail,
17};
18use polars_error::polars_ensure;
19#[cfg(feature = "serde")]
20use serde::{Deserialize, Serialize};
21
22use super::calendar::{
23    NS_DAY, NS_HOUR, NS_MICROSECOND, NS_MILLISECOND, NS_MINUTE, NS_SECOND, NS_WEEK, NTE_NS_DAY,
24    NTE_NS_WEEK,
25};
26#[cfg(feature = "timezones")]
27use crate::utils::{localize_datetime_opt, try_localize_datetime, unlocalize_datetime};
28use crate::windows::calendar::{DAYS_PER_MONTH, is_leap_year};
29
30#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
31#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
32#[cfg_attr(feature = "dsl-schema", derive(schemars::JsonSchema))]
33pub struct Duration {
34    // the number of months for the duration
35    months: i64,
36    // the number of weeks for the duration
37    weeks: i64,
38    // the number of days for the duration
39    days: i64,
40    // the number of nanoseconds for the duration
41    nsecs: i64,
42    // indicates if the duration is negative
43    pub(crate) negative: bool,
44    // indicates if an integer string was passed. e.g. "2i"
45    pub parsed_int: bool,
46}
47
48impl PartialOrd<Self> for Duration {
49    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
50        Some(self.cmp(other))
51    }
52}
53
54impl Ord for Duration {
55    fn cmp(&self, other: &Self) -> Ordering {
56        self.duration_ns().cmp(&other.duration_ns())
57    }
58}
59
60impl Neg for Duration {
61    type Output = Self;
62
63    fn neg(self) -> Self::Output {
64        Self {
65            months: self.months,
66            weeks: self.weeks,
67            days: self.days,
68            nsecs: self.nsecs,
69            negative: !self.negative,
70            parsed_int: self.parsed_int,
71        }
72    }
73}
74
75impl Display for Duration {
76    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
77        if self.is_zero() {
78            return write!(f, "0s");
79        }
80        if self.negative {
81            write!(f, "-")?
82        }
83        if self.months > 0 {
84            write!(f, "{}mo", self.months)?
85        }
86        if self.weeks > 0 {
87            write!(f, "{}w", self.weeks)?
88        }
89        if self.days > 0 {
90            write!(f, "{}d", self.days)?
91        }
92        if self.nsecs > 0 {
93            let secs = self.nsecs / NANOSECONDS;
94            if secs * NANOSECONDS == self.nsecs {
95                write!(f, "{secs}s")?
96            } else {
97                let us = self.nsecs / 1_000;
98                if us * 1_000 == self.nsecs {
99                    write!(f, "{us}us")?
100                } else {
101                    write!(f, "{}ns", self.nsecs)?
102                }
103            }
104        }
105        Ok(())
106    }
107}
108
109impl Duration {
110    /// Create a new integer size `Duration`
111    pub const fn new(fixed_slots: i64) -> Self {
112        Duration {
113            months: 0,
114            weeks: 0,
115            days: 0,
116            nsecs: fixed_slots.abs(),
117            negative: fixed_slots < 0,
118            parsed_int: true,
119        }
120    }
121
122    /// Parse a string into a `Duration`
123    ///
124    /// Strings are composed of a sequence of number-unit pairs, such as `5d` (5 days). A string may begin with a minus
125    /// sign, in which case it is interpreted as a negative duration. Some examples:
126    ///
127    /// * `"1y"`: 1 year
128    /// * `"-1w2d"`: negative 1 week, 2 days (i.e. -9 days)
129    /// * `"3d12h4m25s"`: 3 days, 12 hours, 4 minutes, and 25 seconds
130    ///
131    /// Aside from a leading minus sign, strings may not contain any characters other than numbers and letters
132    /// (including whitespace).
133    ///
134    /// The available units, in ascending order of magnitude, are as follows:
135    ///
136    /// * `ns`: nanosecond
137    /// * `us`: microsecond
138    /// * `ms`: millisecond
139    /// * `s`:  second
140    /// * `m`:  minute
141    /// * `h`:  hour
142    /// * `d`:  day
143    /// * `w`:  week
144    /// * `mo`: calendar month
145    /// * `q`: calendar quarter
146    /// * `y`:  calendar year
147    /// * `i`:  index value (only for {Int32, Int64} dtypes)
148    ///
149    /// By "calendar day", we mean the corresponding time on the next
150    /// day (which may not be 24 hours, depending on daylight savings).
151    /// Similarly for "calendar week", "calendar month", "calendar quarter",
152    /// and "calendar year".
153    ///
154    /// # Panics
155    /// If the given str is invalid for any reason.
156    pub fn parse(duration: &str) -> Self {
157        Self::try_parse(duration).unwrap()
158    }
159
160    #[doc(hidden)]
161    /// Parse SQL-style "interval" string to Duration. Handles verbose
162    /// units (such as 'year', 'minutes', etc.) and whitespace, as
163    /// well as being case-insensitive.
164    pub fn parse_interval(interval: &str) -> Self {
165        Self::try_parse_interval(interval).unwrap()
166    }
167
168    pub fn try_parse(duration: &str) -> PolarsResult<Self> {
169        Self::_parse(duration, false)
170    }
171
172    pub fn try_parse_interval(interval: &str) -> PolarsResult<Self> {
173        Self::_parse(&interval.to_ascii_lowercase(), true)
174    }
175
176    fn _parse(s: &str, as_interval: bool) -> PolarsResult<Self> {
177        let s = if as_interval { s.trim_start() } else { s };
178        let parse_type = if as_interval { "interval" } else { "duration" };
179
180        // can work on raw bytes (much faster), as valid interval/duration strings are all ASCII
181        let original_string = s;
182        let s = s.as_bytes();
183        let mut pos = 0;
184
185        // check for an initial '+'/'-' char
186        let (leading_minus, leading_plus) = match s.first() {
187            Some(&b'-') => (true, false),
188            Some(&b'+') => (false, true),
189            _ => (false, false),
190        };
191
192        // if leading '+'/'-' found, consume it
193        if leading_minus || leading_plus {
194            pos += 1;
195        }
196
197        // permissive whitespace for intervals
198        if as_interval {
199            while pos < s.len() && s[pos] == b' ' {
200                pos += 1;
201            }
202        }
203
204        // we only allow/expect a single leading '+' or '-' char
205        macro_rules! error_on_second_plus_minus {
206            ($ch:expr) => {{
207                let previously_seen = if $ch == b'-' { leading_minus } else { leading_plus };
208                if previously_seen {
209                    polars_bail!(InvalidOperation: "{} string can only have a single '{}' sign", parse_type, $ch as char);
210                }
211                let sign = if $ch == b'-' { "minus" } else { "plus" };
212                if as_interval {
213                    polars_bail!(InvalidOperation: "{} signs are not currently supported in interval strings", sign);
214                } else {
215                    polars_bail!(InvalidOperation: "only a single {} sign is allowed, at the front of the string", sign);
216                }
217            }};
218        }
219
220        // walk the byte-string, identifying number-unit pairs
221        let mut parsed_int = false;
222        let mut months = 0;
223        let mut weeks = 0;
224        let mut days = 0;
225        let mut nsecs = 0;
226
227        while pos < s.len() {
228            let ch = s[pos];
229            if !ch.is_ascii_digit() {
230                if ch == b'-' || ch == b'+' {
231                    error_on_second_plus_minus!(ch);
232                }
233                polars_bail!(InvalidOperation:
234                    "expected leading integer in the {} string, found '{}'",
235                    parse_type, ch as char
236                );
237            }
238
239            // get integer value from the raw bytes
240            let mut n = 0i64;
241            while pos < s.len() && s[pos].is_ascii_digit() {
242                n = n * 10 + (s[pos] - b'0') as i64;
243                pos += 1;
244            }
245            if pos >= s.len() {
246                polars_bail!(InvalidOperation:
247                    "expected a valid unit to follow integer in the {} string '{}'",
248                    parse_type, original_string
249                );
250            }
251
252            // skip leading whitespace/commas before unit (for intervals)
253            if as_interval {
254                while pos < s.len() && (s[pos] == b' ' || s[pos] == b',') {
255                    pos += 1;
256                }
257            }
258
259            // parse the unit associated with the given integer value
260            let unit_start = pos;
261            while pos < s.len() && s[pos].is_ascii_alphabetic() {
262                pos += 1;
263            }
264            let unit_end = pos;
265            if unit_start == unit_end {
266                polars_bail!(InvalidOperation:
267                    "expected a valid unit to follow integer in the {} string '{}'",
268                    parse_type, original_string
269                );
270            }
271
272            // only valid location for '+'/'-' chars is at the start
273            if pos < s.len() && (s[pos] == b'-' || s[pos] == b'+') {
274                error_on_second_plus_minus!(s[pos]);
275            }
276
277            // skip any whitespace/comma that follows an interval unit
278            if as_interval {
279                while pos < s.len() && (s[pos] == b' ' || s[pos] == b',') {
280                    pos += 1;
281                }
282            }
283
284            let unit = &s[unit_start..unit_end];
285            match unit {
286                // matches that are allowed for both duration and interval
287                b"ns" => nsecs += n,
288                b"us" => nsecs += n * NS_MICROSECOND,
289                b"ms" => nsecs += n * NS_MILLISECOND,
290                b"s" => nsecs += n * NS_SECOND,
291                b"m" => nsecs += n * NS_MINUTE,
292                b"h" => nsecs += n * NS_HOUR,
293                b"d" => days += n,
294                b"w" => weeks += n,
295                b"mo" => months += n,
296                b"q" => months += n * 3,
297                b"y" => months += n * 12,
298                b"i" => {
299                    nsecs += n;
300                    parsed_int = true;
301                },
302                // interval-only (verbose/sql) matches
303                _ if as_interval => match unit {
304                    b"nanosecond" | b"nanoseconds" => nsecs += n,
305                    b"microsecond" | b"microseconds" => nsecs += n * NS_MICROSECOND,
306                    b"millisecond" | b"milliseconds" => nsecs += n * NS_MILLISECOND,
307                    b"sec" | b"secs" | b"second" | b"seconds" => nsecs += n * NS_SECOND,
308                    b"min" | b"mins" | b"minute" | b"minutes" => nsecs += n * NS_MINUTE,
309                    b"hour" | b"hours" => nsecs += n * NS_HOUR,
310                    b"day" | b"days" => days += n,
311                    b"week" | b"weeks" => weeks += n,
312                    b"mon" | b"mons" | b"month" | b"months" => months += n,
313                    b"quarter" | b"quarters" => months += n * 3,
314                    b"year" | b"years" => months += n * 12,
315                    _ => {
316                        let unit_str = std::str::from_utf8(unit).unwrap_or("<invalid>");
317                        let valid_units = "'year', 'month', 'quarter', 'week', 'day', 'hour', 'minute', 'second', 'millisecond', 'microsecond', 'nanosecond'";
318                        polars_bail!(InvalidOperation: "unit: '{}' not supported; available units include: {} (and their plurals)", unit_str, valid_units);
319                    },
320                },
321                _ => {
322                    let unit_str = std::str::from_utf8(unit).unwrap_or("<invalid>");
323                    polars_bail!(InvalidOperation: "unit: '{}' not supported; available units are: 'y', 'mo', 'q', 'w', 'd', 'h', 'm', 's', 'ms', 'us', 'ns'", unit_str);
324                },
325            }
326        }
327
328        Ok(Duration {
329            months: months.abs(),
330            weeks: weeks.abs(),
331            days: days.abs(),
332            nsecs: nsecs.abs(),
333            negative: leading_minus,
334            parsed_int,
335        })
336    }
337
338    fn to_positive(v: i64) -> (bool, i64) {
339        if v < 0 { (true, -v) } else { (false, v) }
340    }
341
342    /// Normalize the duration within the interval.
343    /// It will ensure that the output duration is the smallest positive
344    /// duration that is the equivalent of the current duration.
345    #[allow(dead_code)]
346    pub(crate) fn normalize(&self, interval: &Duration) -> Self {
347        if self.months_only() && interval.months_only() {
348            let mut months = self.months() % interval.months();
349
350            match (self.negative, interval.negative) {
351                (true, true) | (true, false) => months = -months + interval.months(),
352                _ => {},
353            }
354            Duration::from_months(months)
355        } else if self.weeks_only() && interval.weeks_only() {
356            let mut weeks = self.weeks() % interval.weeks();
357
358            match (self.negative, interval.negative) {
359                (true, true) | (true, false) => weeks = -weeks + interval.weeks(),
360                _ => {},
361            }
362            Duration::from_weeks(weeks)
363        } else if self.days_only() && interval.days_only() {
364            let mut days = self.days() % interval.days();
365
366            match (self.negative, interval.negative) {
367                (true, true) | (true, false) => days = -days + interval.days(),
368                _ => {},
369            }
370            Duration::from_days(days)
371        } else {
372            let mut offset = self.duration_ns();
373            if offset == 0 {
374                return *self;
375            }
376            let every = interval.duration_ns();
377
378            if offset < 0 {
379                offset += every * ((offset / -every) + 1)
380            } else {
381                offset -= every * (offset / every)
382            }
383            Duration::from_nsecs(offset)
384        }
385    }
386
387    /// Creates a [`Duration`] that represents a fixed number of nanoseconds.
388    pub(crate) fn from_nsecs(v: i64) -> Self {
389        let (negative, nsecs) = Self::to_positive(v);
390        Self {
391            months: 0,
392            weeks: 0,
393            days: 0,
394            nsecs,
395            negative,
396            parsed_int: false,
397        }
398    }
399
400    /// Creates a [`Duration`] that represents a fixed number of months.
401    pub(crate) fn from_months(v: i64) -> Self {
402        let (negative, months) = Self::to_positive(v);
403        Self {
404            months,
405            weeks: 0,
406            days: 0,
407            nsecs: 0,
408            negative,
409            parsed_int: false,
410        }
411    }
412
413    /// Creates a [`Duration`] that represents a fixed number of weeks.
414    pub(crate) fn from_weeks(v: i64) -> Self {
415        let (negative, weeks) = Self::to_positive(v);
416        Self {
417            months: 0,
418            weeks,
419            days: 0,
420            nsecs: 0,
421            negative,
422            parsed_int: false,
423        }
424    }
425
426    /// Creates a [`Duration`] that represents a fixed number of days.
427    pub(crate) fn from_days(v: i64) -> Self {
428        let (negative, days) = Self::to_positive(v);
429        Self {
430            months: 0,
431            weeks: 0,
432            days,
433            nsecs: 0,
434            negative,
435            parsed_int: false,
436        }
437    }
438
439    /// `true` if zero duration.
440    pub fn is_zero(&self) -> bool {
441        self.months == 0 && self.weeks == 0 && self.days == 0 && self.nsecs == 0
442    }
443
444    pub fn months_only(&self) -> bool {
445        self.months != 0 && self.weeks == 0 && self.days == 0 && self.nsecs == 0
446    }
447
448    pub fn months(&self) -> i64 {
449        self.months
450    }
451
452    pub fn weeks_only(&self) -> bool {
453        self.months == 0 && self.weeks != 0 && self.days == 0 && self.nsecs == 0
454    }
455
456    pub fn weeks(&self) -> i64 {
457        self.weeks
458    }
459
460    pub fn days_only(&self) -> bool {
461        self.months == 0 && self.weeks == 0 && self.days != 0 && self.nsecs == 0
462    }
463
464    pub fn days(&self) -> i64 {
465        self.days
466    }
467
468    /// Returns whether the duration consists of full days.
469    ///
470    /// Note that 24 hours is not considered a full day due to possible
471    /// daylight savings time transitions.
472    pub fn is_full_days(&self) -> bool {
473        self.nsecs == 0
474    }
475
476    pub fn is_constant_duration(&self, time_zone: Option<&TimeZone>) -> bool {
477        if time_zone.is_none() || time_zone == Some(&TimeZone::UTC) {
478            self.months == 0
479        } else {
480            // For non-native, non-UTC time zones, 1 calendar day is not
481            // necessarily 24 hours due to daylight savings time.
482            self.months == 0 && self.weeks == 0 && self.days == 0
483        }
484    }
485
486    /// Returns the nanoseconds from the `Duration` without the weeks or months part.
487    pub fn nanoseconds(&self) -> i64 {
488        self.nsecs
489    }
490
491    /// Returns whether duration is negative.
492    pub fn negative(&self) -> bool {
493        self.negative
494    }
495
496    /// Estimated duration of the window duration. Not a very good one if not a constant duration.
497    #[doc(hidden)]
498    pub const fn duration_ns(&self) -> i64 {
499        self.months * 28 * 24 * 3600 * NANOSECONDS
500            + self.weeks * NS_WEEK
501            + self.days * NS_DAY
502            + self.nsecs
503    }
504
505    #[doc(hidden)]
506    pub const fn duration_us(&self) -> i64 {
507        self.months * 28 * 24 * 3600 * MICROSECONDS
508            + (self.weeks * NS_WEEK / 1000 + self.nsecs / 1000 + self.days * NS_DAY / 1000)
509    }
510
511    #[doc(hidden)]
512    pub const fn duration_ms(&self) -> i64 {
513        self.months * 28 * 24 * 3600 * MILLISECONDS
514            + (self.weeks * NS_WEEK / 1_000_000
515                + self.nsecs / 1_000_000
516                + self.days * NS_DAY / 1_000_000)
517    }
518
519    /// Not-to-exceed estimated duration of the window duration. The actual duration will be
520    /// less or equal than the estimate.
521    #[doc(hidden)]
522    pub const fn nte_duration_ns(&self) -> i64 {
523        self.months * (31 * 24 + 1) * 3600 * NANOSECONDS
524            + self.weeks * NTE_NS_WEEK
525            + self.days * NTE_NS_DAY
526            + self.nsecs
527    }
528
529    #[doc(hidden)]
530    pub const fn nte_duration_us(&self) -> i64 {
531        self.months * (31 * 24 + 1) * 3600 * MICROSECONDS
532            + self.weeks * NTE_NS_WEEK / 1000
533            + self.days * NTE_NS_DAY / 1000
534            + self.nsecs / 1000
535    }
536
537    #[doc(hidden)]
538    pub const fn nte_duration_ms(&self) -> i64 {
539        self.months * (31 * 24 + 1) * 3600 * MILLISECONDS
540            + self.weeks * NTE_NS_WEEK / 1_000_000
541            + self.days * NTE_NS_DAY / 1_000_000
542            + self.nsecs / 1_000_000
543    }
544
545    #[doc(hidden)]
546    fn add_month(ts: NaiveDateTime, n_months: i64, negative: bool) -> NaiveDateTime {
547        let mut months = n_months;
548        if negative {
549            months = -months;
550        }
551
552        // Retrieve the current date and increment the values
553        // based on the number of months
554        let mut year = ts.year();
555        let mut month = ts.month() as i32;
556        let mut day = ts.day();
557        year += (months / 12) as i32;
558        month += (months % 12) as i32;
559
560        // if the month overflowed or underflowed, adjust the year
561        // accordingly. Because we add the modulo for the months
562        // the year will only adjust by one
563        if month > 12 {
564            year += 1;
565            month -= 12;
566        } else if month <= 0 {
567            year -= 1;
568            month += 12;
569        }
570
571        // Normalize the day if we are past the end of the month.
572        let last_day_of_month =
573            DAYS_PER_MONTH[is_leap_year(year) as usize][(month - 1) as usize] as u32;
574
575        if day > last_day_of_month {
576            day = last_day_of_month
577        }
578
579        // Retrieve the original time and construct a data
580        // with the new year, month and day
581        let hour = ts.hour();
582        let minute = ts.minute();
583        let sec = ts.second();
584        let nsec = ts.nanosecond();
585        new_datetime(year, month as u32, day, hour, minute, sec, nsec).expect(
586            "Expected valid datetime, please open an issue at https://github.com/pola-rs/polars/issues"
587        )
588    }
589
590    /// Localize result to given time zone, respecting DST fold of original datetime.
591    /// For example, 2022-11-06 01:30:00 CST truncated by 1 hour becomes 2022-11-06 01:00:00 CST,
592    /// whereas 2022-11-06 01:30:00 CDT truncated by 1 hour becomes 2022-11-06 01:00:00 CDT.
593    ///
594    /// * `original_dt_local` - original datetime, without time zone.
595    ///   E.g. if the original datetime was 2022-11-06 01:30:00 CST, then this would
596    ///   be 2022-11-06 01:30:00.
597    /// * `original_dt_utc` - original datetime converted to UTC. E.g. if the
598    ///   original datetime was 2022-11-06 01:30:00 CST, then this would
599    ///   be 2022-11-06 07:30:00.
600    /// * `result_dt_local` - result, without time zone.
601    #[cfg(feature = "timezones")]
602    fn localize_result(
603        &self,
604        original_dt_local: NaiveDateTime,
605        original_dt_utc: NaiveDateTime,
606        result_dt_local: NaiveDateTime,
607        tz: &Tz,
608    ) -> PolarsResult<NaiveDateTime> {
609        match localize_datetime_opt(result_dt_local, tz, Ambiguous::Raise) {
610            Some(dt) => Ok(dt.expect("we didn't use Ambiguous::Null")),
611            None => {
612                if try_localize_datetime(
613                    original_dt_local,
614                    tz,
615                    Ambiguous::Earliest,
616                    NonExistent::Raise,
617                )?
618                .expect("we didn't use Ambiguous::Null or NonExistent::Null")
619                    == original_dt_utc
620                {
621                    Ok(try_localize_datetime(
622                        result_dt_local,
623                        tz,
624                        Ambiguous::Earliest,
625                        NonExistent::Raise,
626                    )?
627                    .expect("we didn't use Ambiguous::Null or NonExistent::Null"))
628                } else if try_localize_datetime(
629                    original_dt_local,
630                    tz,
631                    Ambiguous::Latest,
632                    NonExistent::Raise,
633                )?
634                .expect("we didn't use Ambiguous::Null or NonExistent::Null")
635                    == original_dt_utc
636                {
637                    Ok(try_localize_datetime(
638                        result_dt_local,
639                        tz,
640                        Ambiguous::Latest,
641                        NonExistent::Raise,
642                    )?
643                    .expect("we didn't use Ambiguous::Null or NonExistent::Null"))
644                } else {
645                    unreachable!()
646                }
647            },
648        }
649    }
650
651    fn truncate_subweekly<G, J>(
652        &self,
653        t: i64,
654        tz: Option<&Tz>,
655        duration: i64,
656        _timestamp_to_datetime: G,
657        _datetime_to_timestamp: J,
658    ) -> PolarsResult<i64>
659    where
660        G: Fn(i64) -> NaiveDateTime,
661        J: Fn(NaiveDateTime) -> i64,
662    {
663        match tz {
664            #[cfg(feature = "timezones")]
665            // for UTC, use fastpath below (same as naive)
666            Some(tz) if tz != &chrono_tz::UTC => {
667                let original_dt_utc = _timestamp_to_datetime(t);
668                let original_dt_local = unlocalize_datetime(original_dt_utc, tz);
669                let t = _datetime_to_timestamp(original_dt_local);
670                let mut remainder = t % duration;
671                if remainder < 0 {
672                    remainder += duration
673                }
674                let result_timestamp = t - remainder;
675                let result_dt_local = _timestamp_to_datetime(result_timestamp);
676                let result_dt_utc =
677                    self.localize_result(original_dt_local, original_dt_utc, result_dt_local, tz)?;
678                Ok(_datetime_to_timestamp(result_dt_utc))
679            },
680            _ => {
681                let mut remainder = t % duration;
682                if remainder < 0 {
683                    remainder += duration
684                }
685                Ok(t - remainder)
686            },
687        }
688    }
689
690    fn truncate_weekly<G, J>(
691        &self,
692        t: i64,
693        tz: Option<&Tz>,
694        _timestamp_to_datetime: G,
695        _datetime_to_timestamp: J,
696        daily_duration: i64,
697    ) -> PolarsResult<i64>
698    where
699        G: Fn(i64) -> NaiveDateTime,
700        J: Fn(NaiveDateTime) -> i64,
701    {
702        let _original_dt_utc: Option<NaiveDateTime>;
703        let _original_dt_local: Option<NaiveDateTime>;
704        let t = match tz {
705            #[cfg(feature = "timezones")]
706            // for UTC, use fastpath below (same as naive)
707            Some(tz) if tz != &chrono_tz::UTC => {
708                _original_dt_utc = Some(_timestamp_to_datetime(t));
709                _original_dt_local = Some(unlocalize_datetime(_original_dt_utc.unwrap(), tz));
710                _datetime_to_timestamp(_original_dt_local.unwrap())
711            },
712            _ => {
713                _original_dt_utc = None;
714                _original_dt_local = None;
715                t
716            },
717        };
718        // If we did
719        //   t - (t % (7 * self.weeks * daily_duration))
720        // then the timestamp would get truncated to the previous Thursday,
721        // because 1970-01-01 (timestamp 0) is a Thursday.
722        // So, we adjust by 4 days to get to Monday.
723        let mut remainder = (t - 4 * daily_duration) % (7 * self.weeks * daily_duration);
724        if remainder < 0 {
725            remainder += 7 * self.weeks * daily_duration
726        }
727        let result_t_local = t - remainder;
728        match tz {
729            #[cfg(feature = "timezones")]
730            // for UTC, use fastpath below (same as naive)
731            Some(tz) if tz != &chrono_tz::UTC => {
732                let result_dt_local = _timestamp_to_datetime(result_t_local);
733                let result_dt_utc = self.localize_result(
734                    _original_dt_local.unwrap(),
735                    _original_dt_utc.unwrap(),
736                    result_dt_local,
737                    tz,
738                )?;
739                Ok(_datetime_to_timestamp(result_dt_utc))
740            },
741            _ => Ok(result_t_local),
742        }
743    }
744    fn truncate_monthly<G, J>(
745        &self,
746        t: i64,
747        tz: Option<&Tz>,
748        timestamp_to_datetime: G,
749        datetime_to_timestamp: J,
750        daily_duration: i64,
751    ) -> PolarsResult<i64>
752    where
753        G: Fn(i64) -> NaiveDateTime,
754        J: Fn(NaiveDateTime) -> i64,
755    {
756        let original_dt_utc;
757        let original_dt_local;
758        let t = match tz {
759            #[cfg(feature = "timezones")]
760            // for UTC, use fastpath below (same as naive)
761            Some(tz) if tz != &chrono_tz::UTC => {
762                original_dt_utc = timestamp_to_datetime(t);
763                original_dt_local = unlocalize_datetime(original_dt_utc, tz);
764                datetime_to_timestamp(original_dt_local)
765            },
766            _ => {
767                original_dt_utc = timestamp_to_datetime(t);
768                original_dt_local = original_dt_utc;
769                datetime_to_timestamp(original_dt_local)
770            },
771        };
772
773        // Remove the time of day from the timestamp
774        // e.g. 2020-01-01 12:34:56 -> 2020-01-01 00:00:00
775        let mut remainder_time = t % daily_duration;
776        if remainder_time < 0 {
777            remainder_time += daily_duration
778        }
779        let t = t - remainder_time;
780
781        // Calculate how many months we need to subtract...
782        let (mut year, mut month) = (
783            original_dt_local.year() as i64,
784            original_dt_local.month() as i64,
785        );
786        let total = ((year - 1970) * 12) + (month - 1);
787        let mut remainder_months = total % self.months;
788        if remainder_months < 0 {
789            remainder_months += self.months
790        }
791
792        // ...and translate that to how many days we need to subtract.
793        let mut _is_leap_year = is_leap_year(year as i32);
794        let mut remainder_days = (original_dt_local.day() - 1) as i64;
795        while remainder_months > 12 {
796            let prev_year_is_leap_year = is_leap_year((year - 1) as i32);
797            let add_extra_day =
798                (_is_leap_year && month > 2) || (prev_year_is_leap_year && month <= 2);
799            remainder_days += 365 + add_extra_day as i64;
800            remainder_months -= 12;
801            year -= 1;
802            _is_leap_year = prev_year_is_leap_year;
803        }
804        while remainder_months > 0 {
805            month -= 1;
806            if month == 0 {
807                year -= 1;
808                _is_leap_year = is_leap_year(year as i32);
809                month = 12;
810            }
811            remainder_days += DAYS_PER_MONTH[_is_leap_year as usize][(month - 1) as usize];
812            remainder_months -= 1;
813        }
814
815        match tz {
816            #[cfg(feature = "timezones")]
817            // for UTC, use fastpath below (same as naive)
818            Some(tz) if tz != &chrono_tz::UTC => {
819                let result_dt_local = timestamp_to_datetime(t - remainder_days * daily_duration);
820                let result_dt_utc =
821                    self.localize_result(original_dt_local, original_dt_utc, result_dt_local, tz)?;
822                Ok(datetime_to_timestamp(result_dt_utc))
823            },
824            _ => Ok(t - remainder_days * daily_duration),
825        }
826    }
827
828    #[inline]
829    pub fn truncate_impl<F, G, J>(
830        &self,
831        t: i64,
832        tz: Option<&Tz>,
833        nsecs_to_unit: F,
834        timestamp_to_datetime: G,
835        datetime_to_timestamp: J,
836    ) -> PolarsResult<i64>
837    where
838        F: Fn(i64) -> i64,
839        G: Fn(i64) -> NaiveDateTime,
840        J: Fn(NaiveDateTime) -> i64,
841    {
842        match (self.months, self.weeks, self.days, self.nsecs) {
843            (0, 0, 0, 0) => polars_bail!(ComputeError: "duration cannot be zero"),
844            // truncate by ns/us/ms
845            (0, 0, 0, _) => {
846                let duration = nsecs_to_unit(self.nsecs);
847                if duration == 0 {
848                    return Ok(t);
849                }
850                self.truncate_subweekly(
851                    t,
852                    tz,
853                    duration,
854                    timestamp_to_datetime,
855                    datetime_to_timestamp,
856                )
857            },
858            // truncate by days
859            (0, 0, _, 0) => {
860                let duration = self.days * nsecs_to_unit(NS_DAY);
861                self.truncate_subweekly(
862                    t,
863                    tz,
864                    duration,
865                    timestamp_to_datetime,
866                    datetime_to_timestamp,
867                )
868            },
869            // truncate by weeks
870            (0, _, 0, 0) => {
871                let duration = nsecs_to_unit(NS_DAY);
872                self.truncate_weekly(
873                    t,
874                    tz,
875                    timestamp_to_datetime,
876                    datetime_to_timestamp,
877                    duration,
878                )
879            },
880            // truncate by months
881            (_, 0, 0, 0) => {
882                let duration = nsecs_to_unit(NS_DAY);
883                self.truncate_monthly(
884                    t,
885                    tz,
886                    timestamp_to_datetime,
887                    datetime_to_timestamp,
888                    duration,
889                )
890            },
891            _ => {
892                polars_bail!(ComputeError: "cannot mix month, week, day, and sub-daily units for this operation")
893            },
894        }
895    }
896
897    // Truncate the given ns timestamp by the window boundary.
898    #[inline]
899    pub fn truncate_ns(&self, t: i64, tz: Option<&Tz>) -> PolarsResult<i64> {
900        self.truncate_impl(
901            t,
902            tz,
903            |nsecs| nsecs,
904            timestamp_ns_to_datetime,
905            datetime_to_timestamp_ns,
906        )
907    }
908
909    // Truncate the given ns timestamp by the window boundary.
910    #[inline]
911    pub fn truncate_us(&self, t: i64, tz: Option<&Tz>) -> PolarsResult<i64> {
912        self.truncate_impl(
913            t,
914            tz,
915            |nsecs| nsecs / 1000,
916            timestamp_us_to_datetime,
917            datetime_to_timestamp_us,
918        )
919    }
920
921    // Truncate the given ms timestamp by the window boundary.
922    #[inline]
923    pub fn truncate_ms(&self, t: i64, tz: Option<&Tz>) -> PolarsResult<i64> {
924        self.truncate_impl(
925            t,
926            tz,
927            |nsecs| nsecs / 1_000_000,
928            timestamp_ms_to_datetime,
929            datetime_to_timestamp_ms,
930        )
931    }
932
933    fn add_impl_month_week_or_day<F, G, J>(
934        &self,
935        mut t: i64,
936        tz: Option<&Tz>,
937        nsecs_to_unit: F,
938        timestamp_to_datetime: G,
939        datetime_to_timestamp: J,
940    ) -> PolarsResult<i64>
941    where
942        F: Fn(i64) -> i64,
943        G: Fn(i64) -> NaiveDateTime,
944        J: Fn(NaiveDateTime) -> i64,
945    {
946        let d = self;
947
948        if d.months > 0 {
949            let ts = match tz {
950                #[cfg(feature = "timezones")]
951                // for UTC, use fastpath below (same as naive)
952                Some(tz) if tz != &chrono_tz::UTC => {
953                    unlocalize_datetime(timestamp_to_datetime(t), tz)
954                },
955                _ => timestamp_to_datetime(t),
956            };
957            let dt = Self::add_month(ts, d.months, d.negative);
958            t = match tz {
959                #[cfg(feature = "timezones")]
960                // for UTC, use fastpath below (same as naive)
961                Some(tz) if tz != &chrono_tz::UTC => datetime_to_timestamp(
962                    try_localize_datetime(dt, tz, Ambiguous::Raise, NonExistent::Raise)?
963                        .expect("we didn't use Ambiguous::Null or NonExistent::Null"),
964                ),
965                _ => datetime_to_timestamp(dt),
966            };
967        }
968
969        if d.weeks > 0 {
970            let t_weeks = nsecs_to_unit(NS_WEEK) * self.weeks;
971            match tz {
972                #[cfg(feature = "timezones")]
973                // for UTC, use fastpath below (same as naive)
974                Some(tz) if tz != &chrono_tz::UTC => {
975                    t = datetime_to_timestamp(unlocalize_datetime(timestamp_to_datetime(t), tz));
976                    t += if d.negative { -t_weeks } else { t_weeks };
977                    t = datetime_to_timestamp(
978                        try_localize_datetime(
979                            timestamp_to_datetime(t),
980                            tz,
981                            Ambiguous::Raise,
982                            NonExistent::Raise,
983                        )?
984                        .expect("we didn't use Ambiguous::Null or NonExistent::Null"),
985                    );
986                },
987                _ => t += if d.negative { -t_weeks } else { t_weeks },
988            };
989        }
990
991        if d.days > 0 {
992            let t_days = nsecs_to_unit(NS_DAY) * self.days;
993            match tz {
994                #[cfg(feature = "timezones")]
995                // for UTC, use fastpath below (same as naive)
996                Some(tz) if tz != &chrono_tz::UTC => {
997                    t = datetime_to_timestamp(unlocalize_datetime(timestamp_to_datetime(t), tz));
998                    t += if d.negative { -t_days } else { t_days };
999                    t = datetime_to_timestamp(
1000                        try_localize_datetime(
1001                            timestamp_to_datetime(t),
1002                            tz,
1003                            Ambiguous::Raise,
1004                            NonExistent::Raise,
1005                        )?
1006                        .expect("we didn't use Ambiguous::Null or NonExistent::Null"),
1007                    );
1008                },
1009                _ => t += if d.negative { -t_days } else { t_days },
1010            };
1011        }
1012
1013        Ok(t)
1014    }
1015
1016    pub fn add_ns(&self, t: i64, tz: Option<&Tz>) -> PolarsResult<i64> {
1017        let d = self;
1018        let new_t = self.add_impl_month_week_or_day(
1019            t,
1020            tz,
1021            |nsecs| nsecs,
1022            timestamp_ns_to_datetime,
1023            datetime_to_timestamp_ns,
1024        );
1025        let nsecs = if d.negative { -d.nsecs } else { d.nsecs };
1026        Ok(new_t? + nsecs)
1027    }
1028
1029    pub fn add_us(&self, t: i64, tz: Option<&Tz>) -> PolarsResult<i64> {
1030        let d = self;
1031        let new_t = self.add_impl_month_week_or_day(
1032            t,
1033            tz,
1034            |nsecs| nsecs / 1000,
1035            timestamp_us_to_datetime,
1036            datetime_to_timestamp_us,
1037        );
1038        let nsecs = if d.negative { -d.nsecs } else { d.nsecs };
1039        Ok(new_t? + nsecs / 1_000)
1040    }
1041
1042    pub fn add_ms(&self, t: i64, tz: Option<&Tz>) -> PolarsResult<i64> {
1043        let d = self;
1044        let new_t = self.add_impl_month_week_or_day(
1045            t,
1046            tz,
1047            |nsecs| nsecs / 1_000_000,
1048            timestamp_ms_to_datetime,
1049            datetime_to_timestamp_ms,
1050        );
1051        let nsecs = if d.negative { -d.nsecs } else { d.nsecs };
1052        Ok(new_t? + nsecs / 1_000_000)
1053    }
1054}
1055
1056impl Mul<i64> for Duration {
1057    type Output = Self;
1058
1059    fn mul(mut self, mut rhs: i64) -> Self {
1060        if rhs < 0 {
1061            rhs = -rhs;
1062            self.negative = !self.negative
1063        }
1064        self.months *= rhs;
1065        self.weeks *= rhs;
1066        self.days *= rhs;
1067        self.nsecs *= rhs;
1068        self
1069    }
1070}
1071
1072fn new_datetime(
1073    year: i32,
1074    month: u32,
1075    days: u32,
1076    hour: u32,
1077    min: u32,
1078    sec: u32,
1079    nano: u32,
1080) -> Option<NaiveDateTime> {
1081    let date = NaiveDate::from_ymd_opt(year, month, days)?;
1082    let time = NaiveTime::from_hms_nano_opt(hour, min, sec, nano)?;
1083    Some(NaiveDateTime::new(date, time))
1084}
1085
1086pub fn ensure_is_constant_duration(
1087    duration: Duration,
1088    time_zone: Option<&TimeZone>,
1089    variable_name: &str,
1090) -> PolarsResult<()> {
1091    polars_ensure!(duration.is_constant_duration(time_zone),
1092        InvalidOperation: "expected `{}` to be a constant duration \
1093            (i.e. one independent of differing month durations or of daylight savings time), got {}.\n\
1094            \n\
1095            You may want to try:\n\
1096            - using `'730h'` instead of `'1mo'`\n\
1097            - using `'24h'` instead of `'1d'` if your series is time-zone-aware", variable_name, duration);
1098    Ok(())
1099}
1100
1101pub fn ensure_duration_matches_dtype(
1102    duration: Duration,
1103    dtype: &DataType,
1104    variable_name: &str,
1105) -> PolarsResult<()> {
1106    match dtype {
1107        DataType::Int64 | DataType::UInt64 | DataType::Int32 | DataType::UInt32 => {
1108            polars_ensure!(duration.parsed_int || duration.is_zero(),
1109                InvalidOperation: "`{}` duration must be a parsed integer (i.e. use '2i', not '2d') when working with a numeric column", variable_name);
1110        },
1111        DataType::Datetime(_, _) | DataType::Date | DataType::Duration(_) | DataType::Time => {
1112            polars_ensure!(!duration.parsed_int,
1113                InvalidOperation: "`{}` duration may not be a parsed integer (i.e. use '2d', not '2i') when working with a temporal column", variable_name);
1114        },
1115        _ => {
1116            polars_bail!(InvalidOperation: "unsupported data type: {} for temporal/index column, expected UInt64, UInt32, Int64, Int32, Datetime, Date, Duration, or Time", dtype)
1117        },
1118    }
1119    Ok(())
1120}
1121
1122#[cfg(test)]
1123mod test {
1124    use super::*;
1125
1126    #[test]
1127    fn test_parse() {
1128        let out = Duration::parse("1ns");
1129        assert_eq!(out.nsecs, 1);
1130        let out = Duration::parse("1ns1ms");
1131        assert_eq!(out.nsecs, NS_MILLISECOND + 1);
1132        let out = Duration::parse("123ns40ms");
1133        assert_eq!(out.nsecs, 40 * NS_MILLISECOND + 123);
1134        let out = Duration::parse("123ns40ms1w");
1135        assert_eq!(out.nsecs, 40 * NS_MILLISECOND + 123);
1136        assert_eq!(out.duration_ns(), 40 * NS_MILLISECOND + 123 + NS_WEEK);
1137        let out = Duration::parse("-123ns40ms1w");
1138        assert!(out.negative);
1139        let out = Duration::parse("5w");
1140        assert_eq!(out.weeks(), 5);
1141    }
1142
1143    #[test]
1144    fn test_parse_interval() {
1145        let d = Duration::try_parse_interval("3 DAYS").unwrap();
1146        assert_eq!(d.days(), 3);
1147
1148        let d = Duration::try_parse_interval("1 year, 2 months, 1 week").unwrap();
1149        assert_eq!(d.months(), 14);
1150        assert_eq!(d.weeks(), 1);
1151
1152        let d = Duration::try_parse_interval("100ms 100us").unwrap();
1153        assert_eq!(d.duration_us(), 100_100);
1154    }
1155
1156    #[test]
1157    fn test_add_ns() {
1158        let t = 1;
1159        let seven_days = Duration::parse("7d");
1160        let one_week = Duration::parse("1w");
1161
1162        // add_ns can only error if a time zone is passed, so it's
1163        // safe to unwrap here
1164        assert_eq!(
1165            seven_days.add_ns(t, None).unwrap(),
1166            one_week.add_ns(t, None).unwrap()
1167        );
1168
1169        let seven_days_negative = Duration::parse("-7d");
1170        let one_week_negative = Duration::parse("-1w");
1171
1172        // add_ns can only error if a time zone is passed, so it's
1173        // safe to unwrap here
1174        assert_eq!(
1175            seven_days_negative.add_ns(t, None).unwrap(),
1176            one_week_negative.add_ns(t, None).unwrap()
1177        );
1178    }
1179
1180    #[test]
1181    fn test_display() {
1182        let duration = Duration::parse("1h");
1183        let expected = "3600s";
1184        assert_eq!(format!("{duration}"), expected);
1185        let duration = Duration::parse("1h5ns");
1186        let expected = "3600000000005ns";
1187        assert_eq!(format!("{duration}"), expected);
1188        let duration = Duration::parse("1h5000ns");
1189        let expected = "3600000005us";
1190        assert_eq!(format!("{duration}"), expected);
1191        let duration = Duration::parse("3mo");
1192        let expected = "3mo";
1193        assert_eq!(format!("{duration}"), expected);
1194        let duration = Duration::parse_interval("4 weeks");
1195        let expected = "4w";
1196        assert_eq!(format!("{duration}"), expected);
1197    }
1198}