1use std::cmp::Ordering;
2use std::fmt::{Display, Formatter};
3use std::ops::{Add, Mul, Neg};
4
5use arrow::legacy::time_zone::Tz;
6use arrow::temporal_conversions::{
7 MICROSECONDS, MILLISECONDS, NANOSECONDS, timestamp_ms_to_datetime, timestamp_ns_to_datetime,
8 timestamp_us_to_datetime,
9};
10#[cfg(feature = "timezones")]
11use chrono::TimeZone as ChronoTimeZone;
12#[cfg(feature = "timezones")]
13use chrono::offset::LocalResult;
14use chrono::{Datelike, NaiveDate, NaiveDateTime, NaiveTime, TimeDelta, Timelike};
15#[cfg(feature = "timezones")]
16use chrono_tz::OffsetComponents;
17use polars_core::datatypes::DataType;
18use polars_core::prelude::{
19 Ambiguous, NonExistent, PolarsResult, TimeZone, datetime_to_timestamp_ms,
20 datetime_to_timestamp_ns, datetime_to_timestamp_us, polars_bail,
21};
22use polars_error::polars_ensure;
23#[cfg(feature = "serde")]
24use serde::{Deserialize, Serialize};
25
26use super::calendar::{
27 NS_DAY, NS_HOUR, NS_MICROSECOND, NS_MILLISECOND, NS_MINUTE, NS_SECOND, NS_WEEK, NTE_NS_DAY,
28 NTE_NS_WEEK,
29};
30#[cfg(feature = "timezones")]
31use crate::utils::try_localize_datetime;
32#[cfg(feature = "timezones")]
33use crate::utils::unlocalize_datetime;
34use crate::windows::calendar::{DAYS_PER_MONTH, is_leap_year};
35
36#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
37#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
38#[cfg_attr(feature = "dsl-schema", derive(schemars::JsonSchema))]
39pub struct Duration {
40 months: i64,
42 weeks: i64,
44 days: i64,
46 nsecs: i64,
48 pub(crate) negative: bool,
50 pub parsed_int: bool,
52}
53
54impl PartialOrd<Self> for Duration {
55 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
56 Some(self.cmp(other))
57 }
58}
59
60impl Ord for Duration {
61 fn cmp(&self, other: &Self) -> Ordering {
62 self.duration_ns().cmp(&other.duration_ns())
63 }
64}
65
66impl Neg for Duration {
67 type Output = Self;
68
69 fn neg(self) -> Self::Output {
70 Self {
71 months: self.months,
72 weeks: self.weeks,
73 days: self.days,
74 nsecs: self.nsecs,
75 negative: !self.negative,
76 parsed_int: self.parsed_int,
77 }
78 }
79}
80
81impl Display for Duration {
82 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
83 if self.is_zero() {
84 return write!(f, "0s");
85 }
86 if self.negative {
87 write!(f, "-")?
88 }
89 if self.months > 0 {
90 write!(f, "{}mo", self.months)?
91 }
92 if self.weeks > 0 {
93 write!(f, "{}w", self.weeks)?
94 }
95 if self.days > 0 {
96 write!(f, "{}d", self.days)?
97 }
98 if self.nsecs > 0 {
99 let secs = self.nsecs / NANOSECONDS;
100 if secs * NANOSECONDS == self.nsecs {
101 write!(f, "{secs}s")?
102 } else {
103 let us = self.nsecs / 1_000;
104 if us * 1_000 == self.nsecs {
105 write!(f, "{us}us")?
106 } else {
107 write!(f, "{}ns", self.nsecs)?
108 }
109 }
110 }
111 Ok(())
112 }
113}
114
115impl Duration {
116 pub const fn new(fixed_slots: i64) -> Self {
118 Duration {
119 months: 0,
120 weeks: 0,
121 days: 0,
122 nsecs: fixed_slots.abs(),
123 negative: fixed_slots < 0,
124 parsed_int: true,
125 }
126 }
127
128 pub fn parse(duration: &str) -> Self {
163 Self::try_parse(duration).unwrap()
164 }
165
166 #[doc(hidden)]
167 pub fn parse_interval(interval: &str) -> Self {
171 Self::try_parse_interval(interval).unwrap()
172 }
173
174 pub fn try_parse(duration: &str) -> PolarsResult<Self> {
175 Self::_parse(duration, false)
176 }
177
178 pub fn try_parse_interval(interval: &str) -> PolarsResult<Self> {
179 Self::_parse(&interval.to_ascii_lowercase(), true)
180 }
181
182 fn _parse(s: &str, as_interval: bool) -> PolarsResult<Self> {
183 let s = if as_interval { s.trim_start() } else { s };
184 let parse_type = if as_interval { "interval" } else { "duration" };
185
186 let original_string = s;
188 let s = s.as_bytes();
189 let mut pos = 0;
190
191 let (leading_minus, leading_plus) = match s.first() {
193 Some(&b'-') => (true, false),
194 Some(&b'+') => (false, true),
195 _ => (false, false),
196 };
197
198 if leading_minus || leading_plus {
200 pos += 1;
201 }
202
203 if as_interval {
205 while pos < s.len() && s[pos] == b' ' {
206 pos += 1;
207 }
208 }
209
210 macro_rules! error_on_second_plus_minus {
212 ($ch:expr) => {{
213 let previously_seen = if $ch == b'-' { leading_minus } else { leading_plus };
214 if previously_seen {
215 polars_bail!(InvalidOperation: "{} string can only have a single '{}' sign", parse_type, $ch as char);
216 }
217 let sign = if $ch == b'-' { "minus" } else { "plus" };
218 if as_interval {
219 polars_bail!(InvalidOperation: "{} signs are not currently supported in interval strings", sign);
220 } else {
221 polars_bail!(InvalidOperation: "only a single {} sign is allowed, at the front of the string", sign);
222 }
223 }};
224 }
225
226 let mut parsed_int = false;
228 let mut months = 0;
229 let mut weeks = 0;
230 let mut days = 0;
231 let mut nsecs = 0;
232
233 while pos < s.len() {
234 let ch = s[pos];
235 if !ch.is_ascii_digit() {
236 if ch == b'-' || ch == b'+' {
237 error_on_second_plus_minus!(ch);
238 }
239 polars_bail!(InvalidOperation:
240 "expected leading integer in the {} string, found '{}'",
241 parse_type, ch as char
242 );
243 }
244
245 let mut n = 0i64;
247 while pos < s.len() && s[pos].is_ascii_digit() {
248 n = n * 10 + (s[pos] - b'0') as i64;
249 pos += 1;
250 }
251 if pos >= s.len() {
252 polars_bail!(InvalidOperation:
253 "expected a valid unit to follow integer in the {} string '{}'",
254 parse_type, original_string
255 );
256 }
257
258 if as_interval {
260 while pos < s.len() && (s[pos] == b' ' || s[pos] == b',') {
261 pos += 1;
262 }
263 }
264
265 let unit_start = pos;
267 while pos < s.len() && s[pos].is_ascii_alphabetic() {
268 pos += 1;
269 }
270 let unit_end = pos;
271 if unit_start == unit_end {
272 polars_bail!(InvalidOperation:
273 "expected a valid unit to follow integer in the {} string '{}'",
274 parse_type, original_string
275 );
276 }
277
278 if pos < s.len() && (s[pos] == b'-' || s[pos] == b'+') {
280 error_on_second_plus_minus!(s[pos]);
281 }
282
283 if as_interval {
285 while pos < s.len() && (s[pos] == b' ' || s[pos] == b',') {
286 pos += 1;
287 }
288 }
289
290 let unit = &s[unit_start..unit_end];
291 match unit {
292 b"ns" => nsecs += n,
294 b"us" => nsecs += n * NS_MICROSECOND,
295 b"ms" => nsecs += n * NS_MILLISECOND,
296 b"s" => nsecs += n * NS_SECOND,
297 b"m" => nsecs += n * NS_MINUTE,
298 b"h" => nsecs += n * NS_HOUR,
299 b"d" => days += n,
300 b"w" => weeks += n,
301 b"mo" => months += n,
302 b"q" => months += n * 3,
303 b"y" => months += n * 12,
304 b"i" => {
305 nsecs += n;
306 parsed_int = true;
307 },
308 _ if as_interval => match unit {
310 b"nanosecond" | b"nanoseconds" => nsecs += n,
311 b"microsecond" | b"microseconds" => nsecs += n * NS_MICROSECOND,
312 b"millisecond" | b"milliseconds" => nsecs += n * NS_MILLISECOND,
313 b"sec" | b"secs" | b"second" | b"seconds" => nsecs += n * NS_SECOND,
314 b"min" | b"mins" | b"minute" | b"minutes" => nsecs += n * NS_MINUTE,
315 b"hour" | b"hours" => nsecs += n * NS_HOUR,
316 b"day" | b"days" => days += n,
317 b"week" | b"weeks" => weeks += n,
318 b"mon" | b"mons" | b"month" | b"months" => months += n,
319 b"quarter" | b"quarters" => months += n * 3,
320 b"year" | b"years" => months += n * 12,
321 _ => {
322 let unit_str = std::str::from_utf8(unit).unwrap_or("<invalid>");
323 let valid_units = "'year', 'month', 'quarter', 'week', 'day', 'hour', 'minute', 'second', 'millisecond', 'microsecond', 'nanosecond'";
324 polars_bail!(InvalidOperation: "unit: '{}' not supported; available units include: {} (and their plurals)", unit_str, valid_units);
325 },
326 },
327 _ => {
328 let unit_str = std::str::from_utf8(unit).unwrap_or("<invalid>");
329 polars_bail!(InvalidOperation: "unit: '{}' not supported; available units are: 'y', 'mo', 'q', 'w', 'd', 'h', 'm', 's', 'ms', 'us', 'ns'", unit_str);
330 },
331 }
332 }
333
334 Ok(Duration {
335 months: months.abs(),
336 weeks: weeks.abs(),
337 days: days.abs(),
338 nsecs: nsecs.abs(),
339 negative: leading_minus,
340 parsed_int,
341 })
342 }
343
344 fn to_positive(v: i64) -> (bool, i64) {
345 if v < 0 { (true, -v) } else { (false, v) }
346 }
347
348 #[allow(dead_code)]
352 pub(crate) fn normalize(&self, interval: &Duration) -> Self {
353 if self.months_only() && interval.months_only() {
354 let mut months = self.months() % interval.months();
355
356 match (self.negative, interval.negative) {
357 (true, true) | (true, false) => months = -months + interval.months(),
358 _ => {},
359 }
360 Duration::from_months(months)
361 } else if self.weeks_only() && interval.weeks_only() {
362 let mut weeks = self.weeks() % interval.weeks();
363
364 match (self.negative, interval.negative) {
365 (true, true) | (true, false) => weeks = -weeks + interval.weeks(),
366 _ => {},
367 }
368 Duration::from_weeks(weeks)
369 } else if self.days_only() && interval.days_only() {
370 let mut days = self.days() % interval.days();
371
372 match (self.negative, interval.negative) {
373 (true, true) | (true, false) => days = -days + interval.days(),
374 _ => {},
375 }
376 Duration::from_days(days)
377 } else {
378 let mut offset = self.duration_ns();
379 if offset == 0 {
380 return *self;
381 }
382 let every = interval.duration_ns();
383
384 if offset < 0 {
385 offset += every * ((offset / -every) + 1)
386 } else {
387 offset -= every * (offset / every)
388 }
389 Duration::from_nsecs(offset)
390 }
391 }
392
393 pub(crate) fn from_nsecs(v: i64) -> Self {
395 let (negative, nsecs) = Self::to_positive(v);
396 Self {
397 months: 0,
398 weeks: 0,
399 days: 0,
400 nsecs,
401 negative,
402 parsed_int: false,
403 }
404 }
405
406 pub(crate) fn from_months(v: i64) -> Self {
408 let (negative, months) = Self::to_positive(v);
409 Self {
410 months,
411 weeks: 0,
412 days: 0,
413 nsecs: 0,
414 negative,
415 parsed_int: false,
416 }
417 }
418
419 pub(crate) fn from_weeks(v: i64) -> Self {
421 let (negative, weeks) = Self::to_positive(v);
422 Self {
423 months: 0,
424 weeks,
425 days: 0,
426 nsecs: 0,
427 negative,
428 parsed_int: false,
429 }
430 }
431
432 pub(crate) fn from_days(v: i64) -> Self {
434 let (negative, days) = Self::to_positive(v);
435 Self {
436 months: 0,
437 weeks: 0,
438 days,
439 nsecs: 0,
440 negative,
441 parsed_int: false,
442 }
443 }
444
445 pub fn is_zero(&self) -> bool {
447 self.months == 0 && self.weeks == 0 && self.days == 0 && self.nsecs == 0
448 }
449
450 pub fn months_only(&self) -> bool {
451 self.months != 0 && self.weeks == 0 && self.days == 0 && self.nsecs == 0
452 }
453
454 pub fn months(&self) -> i64 {
455 self.months
456 }
457
458 pub fn weeks_only(&self) -> bool {
459 self.months == 0 && self.weeks != 0 && self.days == 0 && self.nsecs == 0
460 }
461
462 pub fn weeks(&self) -> i64 {
463 self.weeks
464 }
465
466 pub fn days_only(&self) -> bool {
467 self.months == 0 && self.weeks == 0 && self.days != 0 && self.nsecs == 0
468 }
469
470 pub fn days(&self) -> i64 {
471 self.days
472 }
473
474 pub fn is_full_days(&self) -> bool {
479 self.nsecs == 0
480 }
481
482 pub fn is_constant_duration(&self, time_zone: Option<&TimeZone>) -> bool {
483 if time_zone.is_none() || time_zone == Some(&TimeZone::UTC) {
484 self.months == 0
485 } else {
486 self.months == 0 && self.weeks == 0 && self.days == 0
489 }
490 }
491
492 pub fn nanoseconds(&self) -> i64 {
494 self.nsecs
495 }
496
497 pub fn negative(&self) -> bool {
499 self.negative
500 }
501
502 #[doc(hidden)]
504 pub const fn duration_ns(&self) -> i64 {
505 self.months * 28 * 24 * 3600 * NANOSECONDS
506 + self.weeks * NS_WEEK
507 + self.days * NS_DAY
508 + self.nsecs
509 }
510
511 #[doc(hidden)]
512 pub const fn duration_us(&self) -> i64 {
513 self.months * 28 * 24 * 3600 * MICROSECONDS
514 + (self.weeks * NS_WEEK / 1000 + self.nsecs / 1000 + self.days * NS_DAY / 1000)
515 }
516
517 #[doc(hidden)]
518 pub const fn duration_ms(&self) -> i64 {
519 self.months * 28 * 24 * 3600 * MILLISECONDS
520 + (self.weeks * NS_WEEK / 1_000_000
521 + self.nsecs / 1_000_000
522 + self.days * NS_DAY / 1_000_000)
523 }
524
525 #[doc(hidden)]
528 pub const fn nte_duration_ns(&self) -> i64 {
529 self.months * (31 * 24 + 1) * 3600 * NANOSECONDS
530 + self.weeks * NTE_NS_WEEK
531 + self.days * NTE_NS_DAY
532 + self.nsecs
533 }
534
535 #[doc(hidden)]
536 pub const fn nte_duration_us(&self) -> i64 {
537 self.months * (31 * 24 + 1) * 3600 * MICROSECONDS
538 + self.weeks * NTE_NS_WEEK / 1000
539 + self.days * NTE_NS_DAY / 1000
540 + self.nsecs / 1000
541 }
542
543 #[doc(hidden)]
544 pub const fn nte_duration_ms(&self) -> i64 {
545 self.months * (31 * 24 + 1) * 3600 * MILLISECONDS
546 + self.weeks * NTE_NS_WEEK / 1_000_000
547 + self.days * NTE_NS_DAY / 1_000_000
548 + self.nsecs / 1_000_000
549 }
550
551 #[doc(hidden)]
552 fn add_month(ts: NaiveDateTime, n_months: i64, negative: bool) -> NaiveDateTime {
553 let mut months = n_months;
554 if negative {
555 months = -months;
556 }
557
558 let mut year = ts.year();
561 let mut month = ts.month() as i32;
562 let mut day = ts.day();
563 year += (months / 12) as i32;
564 month += (months % 12) as i32;
565
566 if month > 12 {
570 year += 1;
571 month -= 12;
572 } else if month <= 0 {
573 year -= 1;
574 month += 12;
575 }
576
577 let last_day_of_month =
579 DAYS_PER_MONTH[is_leap_year(year) as usize][(month - 1) as usize] as u32;
580
581 if day > last_day_of_month {
582 day = last_day_of_month
583 }
584
585 let hour = ts.hour();
588 let minute = ts.minute();
589 let sec = ts.second();
590 let nsec = ts.nanosecond();
591 new_datetime(year, month as u32, day, hour, minute, sec, nsec).expect(
592 "Expected valid datetime, please open an issue at https://github.com/pola-rs/polars/issues"
593 )
594 }
595
596 #[cfg(feature = "timezones")]
607 fn localize_result_rfc_5545(
608 &self,
609 original_dt_utc: NaiveDateTime,
610 result_dt_local: NaiveDateTime,
611 tz: &Tz,
612 ) -> PolarsResult<NaiveDateTime> {
613 let result_localized = tz.from_local_datetime(&result_dt_local);
614 match result_localized {
615 LocalResult::Single(result) => Ok(result.naive_utc()),
616 LocalResult::Ambiguous(result_earliest, result_latest) => {
617 let original_localized = tz.from_utc_datetime(&original_dt_utc);
618 let original_dst_offset = original_localized.offset().dst_offset();
619 if result_earliest.offset().dst_offset() == original_dst_offset {
620 return Ok(result_earliest.naive_utc());
621 }
622 if result_latest.offset().dst_offset() == original_dst_offset {
623 return Ok(result_latest.naive_utc());
624 }
625 polars_bail!(ComputeError: "Could not localize datetime '{}' to time zone '{}'", result_dt_local, tz);
626 },
627 LocalResult::None => {
628 let original_localized = tz.from_utc_datetime(&original_dt_utc);
629 let original_dst_offset = original_localized.offset().dst_offset();
630 let shifted: NaiveDateTime;
631 if original_dst_offset.num_minutes() != 0 {
632 shifted = result_dt_local.add(original_dst_offset);
633 } else if let Some(next_hour) = tz
634 .from_local_datetime(&result_dt_local.add(TimeDelta::hours(1)))
635 .earliest()
636 {
637 let result_dst_offset = next_hour.offset().dst_offset();
639 shifted = result_dt_local.add(-result_dst_offset);
640 } else {
641 polars_bail!(ComputeError: "Could not localize datetime '{}' to time zone '{}'", result_dt_local, tz);
642 }
643 Ok(
644 try_localize_datetime(shifted, tz, Ambiguous::Raise, NonExistent::Raise)?
645 .expect("we didn't use Ambiguous::Null or NonExistent::Null"),
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 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_rfc_5545(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 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 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 Some(tz) if tz != &chrono_tz::UTC => {
732 let result_dt_local = _timestamp_to_datetime(result_t_local);
733 let result_dt_utc =
734 self.localize_result_rfc_5545(_original_dt_utc.unwrap(), result_dt_local, tz)?;
735 Ok(_datetime_to_timestamp(result_dt_utc))
736 },
737 _ => Ok(result_t_local),
738 }
739 }
740 fn truncate_monthly<G, J>(
741 &self,
742 t: i64,
743 tz: Option<&Tz>,
744 timestamp_to_datetime: G,
745 datetime_to_timestamp: J,
746 daily_duration: i64,
747 ) -> PolarsResult<i64>
748 where
749 G: Fn(i64) -> NaiveDateTime,
750 J: Fn(NaiveDateTime) -> i64,
751 {
752 let original_dt_utc;
753 let original_dt_local;
754 let t = match tz {
755 #[cfg(feature = "timezones")]
756 Some(tz) if tz != &chrono_tz::UTC => {
758 original_dt_utc = timestamp_to_datetime(t);
759 original_dt_local = unlocalize_datetime(original_dt_utc, tz);
760 datetime_to_timestamp(original_dt_local)
761 },
762 _ => {
763 original_dt_utc = timestamp_to_datetime(t);
764 original_dt_local = original_dt_utc;
765 datetime_to_timestamp(original_dt_local)
766 },
767 };
768
769 let mut remainder_time = t % daily_duration;
772 if remainder_time < 0 {
773 remainder_time += daily_duration
774 }
775 let t = t - remainder_time;
776
777 let (mut year, mut month) = (
779 original_dt_local.year() as i64,
780 original_dt_local.month() as i64,
781 );
782 let total = ((year - 1970) * 12) + (month - 1);
783 let mut remainder_months = total % self.months;
784 if remainder_months < 0 {
785 remainder_months += self.months
786 }
787
788 let mut _is_leap_year = is_leap_year(year as i32);
790 let mut remainder_days = (original_dt_local.day() - 1) as i64;
791 while remainder_months > 12 {
792 let prev_year_is_leap_year = is_leap_year((year - 1) as i32);
793 let add_extra_day =
794 (_is_leap_year && month > 2) || (prev_year_is_leap_year && month <= 2);
795 remainder_days += 365 + add_extra_day as i64;
796 remainder_months -= 12;
797 year -= 1;
798 _is_leap_year = prev_year_is_leap_year;
799 }
800 while remainder_months > 0 {
801 month -= 1;
802 if month == 0 {
803 year -= 1;
804 _is_leap_year = is_leap_year(year as i32);
805 month = 12;
806 }
807 remainder_days += DAYS_PER_MONTH[_is_leap_year as usize][(month - 1) as usize];
808 remainder_months -= 1;
809 }
810
811 match tz {
812 #[cfg(feature = "timezones")]
813 Some(tz) if tz != &chrono_tz::UTC => {
815 let result_dt_local = timestamp_to_datetime(t - remainder_days * daily_duration);
816 let result_dt_utc =
817 self.localize_result_rfc_5545(original_dt_utc, result_dt_local, tz)?;
818 Ok(datetime_to_timestamp(result_dt_utc))
819 },
820 _ => Ok(t - remainder_days * daily_duration),
821 }
822 }
823
824 #[inline]
825 pub fn truncate_impl<F, G, J>(
826 &self,
827 t: i64,
828 tz: Option<&Tz>,
829 nsecs_to_unit: F,
830 timestamp_to_datetime: G,
831 datetime_to_timestamp: J,
832 ) -> PolarsResult<i64>
833 where
834 F: Fn(i64) -> i64,
835 G: Fn(i64) -> NaiveDateTime,
836 J: Fn(NaiveDateTime) -> i64,
837 {
838 match (self.months, self.weeks, self.days, self.nsecs) {
839 (0, 0, 0, 0) => polars_bail!(ComputeError: "duration cannot be zero"),
840 (0, 0, 0, _) => {
842 let duration = nsecs_to_unit(self.nsecs);
843 if duration == 0 {
844 return Ok(t);
845 }
846 self.truncate_subweekly(
847 t,
848 tz,
849 duration,
850 timestamp_to_datetime,
851 datetime_to_timestamp,
852 )
853 },
854 (0, 0, _, 0) => {
856 let duration = self.days * nsecs_to_unit(NS_DAY);
857 self.truncate_subweekly(
858 t,
859 tz,
860 duration,
861 timestamp_to_datetime,
862 datetime_to_timestamp,
863 )
864 },
865 (0, _, 0, 0) => {
867 let duration = nsecs_to_unit(NS_DAY);
868 self.truncate_weekly(
869 t,
870 tz,
871 timestamp_to_datetime,
872 datetime_to_timestamp,
873 duration,
874 )
875 },
876 (_, 0, 0, 0) => {
878 let duration = nsecs_to_unit(NS_DAY);
879 self.truncate_monthly(
880 t,
881 tz,
882 timestamp_to_datetime,
883 datetime_to_timestamp,
884 duration,
885 )
886 },
887 _ => {
888 polars_bail!(ComputeError: "cannot mix month, week, day, and sub-daily units for this operation")
889 },
890 }
891 }
892
893 #[inline]
895 pub fn truncate_ns(&self, t: i64, tz: Option<&Tz>) -> PolarsResult<i64> {
896 self.truncate_impl(
897 t,
898 tz,
899 |nsecs| nsecs,
900 timestamp_ns_to_datetime,
901 datetime_to_timestamp_ns,
902 )
903 }
904
905 #[inline]
907 pub fn truncate_us(&self, t: i64, tz: Option<&Tz>) -> PolarsResult<i64> {
908 self.truncate_impl(
909 t,
910 tz,
911 |nsecs| nsecs / 1000,
912 timestamp_us_to_datetime,
913 datetime_to_timestamp_us,
914 )
915 }
916
917 #[inline]
919 pub fn truncate_ms(&self, t: i64, tz: Option<&Tz>) -> PolarsResult<i64> {
920 self.truncate_impl(
921 t,
922 tz,
923 |nsecs| nsecs / 1_000_000,
924 timestamp_ms_to_datetime,
925 datetime_to_timestamp_ms,
926 )
927 }
928
929 fn add_impl_month_week_or_day<F, G, J>(
930 &self,
931 mut t: i64,
932 tz: Option<&Tz>,
933 nsecs_to_unit: F,
934 timestamp_to_datetime: G,
935 datetime_to_timestamp: J,
936 ) -> PolarsResult<i64>
937 where
938 F: Fn(i64) -> i64,
939 G: Fn(i64) -> NaiveDateTime,
940 J: Fn(NaiveDateTime) -> i64,
941 {
942 let d = self;
943
944 if d.months > 0 {
945 t = match tz {
946 #[cfg(feature = "timezones")]
947 Some(tz) if tz != &chrono_tz::UTC => {
949 let original_dt_utc = timestamp_to_datetime(t);
950 let original_dt_local = unlocalize_datetime(original_dt_utc, tz);
951 let result_dt_local = Self::add_month(original_dt_local, d.months, d.negative);
952 datetime_to_timestamp(self.localize_result_rfc_5545(
953 original_dt_utc,
954 result_dt_local,
955 tz,
956 )?)
957 },
958 _ => datetime_to_timestamp(Self::add_month(
959 timestamp_to_datetime(t),
960 d.months,
961 d.negative,
962 )),
963 };
964 }
965
966 if d.weeks > 0 {
967 let t_weeks = nsecs_to_unit(NS_WEEK) * self.weeks;
968 t = match tz {
969 #[cfg(feature = "timezones")]
970 Some(tz) if tz != &chrono_tz::UTC => {
972 let original_dt_utc = timestamp_to_datetime(t);
973 let original_dt_local = unlocalize_datetime(original_dt_utc, tz);
974 let mut result_timestamp_local = datetime_to_timestamp(original_dt_local);
975 result_timestamp_local += if d.negative { -t_weeks } else { t_weeks };
976 let result_dt_local = timestamp_to_datetime(result_timestamp_local);
977 datetime_to_timestamp(self.localize_result_rfc_5545(
978 original_dt_utc,
979 result_dt_local,
980 tz,
981 )?)
982 },
983 _ => {
984 if d.negative {
985 t - t_weeks
986 } else {
987 t + t_weeks
988 }
989 },
990 };
991 }
992
993 if d.days > 0 {
994 let t_days = nsecs_to_unit(NS_DAY) * self.days;
995 t = match tz {
996 #[cfg(feature = "timezones")]
997 Some(tz) if tz != &chrono_tz::UTC => {
999 let original_dt_utc = timestamp_to_datetime(t);
1000 let original_dt_local = unlocalize_datetime(original_dt_utc, tz);
1001 t = datetime_to_timestamp(original_dt_local);
1002 t += if d.negative { -t_days } else { t_days };
1003 let result_dt_local = timestamp_to_datetime(t);
1004 let result_dt_utc =
1005 self.localize_result_rfc_5545(original_dt_utc, result_dt_local, tz)?;
1006 datetime_to_timestamp(result_dt_utc)
1007 },
1008 _ => {
1009 if d.negative {
1010 t - t_days
1011 } else {
1012 t + t_days
1013 }
1014 },
1015 };
1016 }
1017
1018 Ok(t)
1019 }
1020
1021 pub fn add_ns(&self, t: i64, tz: Option<&Tz>) -> PolarsResult<i64> {
1022 let d = self;
1023 let new_t = self.add_impl_month_week_or_day(
1024 t,
1025 tz,
1026 |nsecs| nsecs,
1027 timestamp_ns_to_datetime,
1028 datetime_to_timestamp_ns,
1029 );
1030 let nsecs = if d.negative { -d.nsecs } else { d.nsecs };
1031 Ok(new_t? + nsecs)
1032 }
1033
1034 pub fn add_us(&self, t: i64, tz: Option<&Tz>) -> PolarsResult<i64> {
1035 let d = self;
1036 let new_t = self.add_impl_month_week_or_day(
1037 t,
1038 tz,
1039 |nsecs| nsecs / 1000,
1040 timestamp_us_to_datetime,
1041 datetime_to_timestamp_us,
1042 );
1043 let nsecs = if d.negative { -d.nsecs } else { d.nsecs };
1044 Ok(new_t? + nsecs / 1_000)
1045 }
1046
1047 pub fn add_ms(&self, t: i64, tz: Option<&Tz>) -> PolarsResult<i64> {
1048 let d = self;
1049 let new_t = self.add_impl_month_week_or_day(
1050 t,
1051 tz,
1052 |nsecs| nsecs / 1_000_000,
1053 timestamp_ms_to_datetime,
1054 datetime_to_timestamp_ms,
1055 );
1056 let nsecs = if d.negative { -d.nsecs } else { d.nsecs };
1057 Ok(new_t? + nsecs / 1_000_000)
1058 }
1059}
1060
1061impl Mul<i64> for Duration {
1062 type Output = Self;
1063
1064 fn mul(mut self, mut rhs: i64) -> Self {
1065 if rhs < 0 {
1066 rhs = -rhs;
1067 self.negative = !self.negative
1068 }
1069 self.months *= rhs;
1070 self.weeks *= rhs;
1071 self.days *= rhs;
1072 self.nsecs *= rhs;
1073 self
1074 }
1075}
1076
1077fn new_datetime(
1078 year: i32,
1079 month: u32,
1080 days: u32,
1081 hour: u32,
1082 min: u32,
1083 sec: u32,
1084 nano: u32,
1085) -> Option<NaiveDateTime> {
1086 let date = NaiveDate::from_ymd_opt(year, month, days)?;
1087 let time = NaiveTime::from_hms_nano_opt(hour, min, sec, nano)?;
1088 Some(NaiveDateTime::new(date, time))
1089}
1090
1091pub fn ensure_is_constant_duration(
1092 duration: Duration,
1093 time_zone: Option<&TimeZone>,
1094 variable_name: &str,
1095) -> PolarsResult<()> {
1096 polars_ensure!(duration.is_constant_duration(time_zone),
1097 InvalidOperation: "expected `{}` to be a constant duration \
1098 (i.e. one independent of differing month durations or of daylight savings time), got {}.\n\
1099 \n\
1100 You may want to try:\n\
1101 - using `'730h'` instead of `'1mo'`\n\
1102 - using `'24h'` instead of `'1d'` if your series is time-zone-aware", variable_name, duration);
1103 Ok(())
1104}
1105
1106pub fn ensure_duration_matches_dtype(
1107 duration: Duration,
1108 dtype: &DataType,
1109 variable_name: &str,
1110) -> PolarsResult<()> {
1111 match dtype {
1112 DataType::Int64 | DataType::UInt64 | DataType::Int32 | DataType::UInt32 => {
1113 polars_ensure!(duration.parsed_int || duration.is_zero(),
1114 InvalidOperation: "`{}` duration must be a parsed integer (i.e. use '2i', not '2d') when working with a numeric column", variable_name);
1115 },
1116 DataType::Datetime(_, _) | DataType::Date | DataType::Duration(_) | DataType::Time => {
1117 polars_ensure!(!duration.parsed_int,
1118 InvalidOperation: "`{}` duration may not be a parsed integer (i.e. use '2d', not '2i') when working with a temporal column", variable_name);
1119 },
1120 _ => {
1121 polars_bail!(InvalidOperation: "unsupported data type: {} for temporal/index column, expected UInt64, UInt32, Int64, Int32, Datetime, Date, Duration, or Time", dtype)
1122 },
1123 }
1124 Ok(())
1125}
1126
1127#[cfg(test)]
1128mod test {
1129 use super::*;
1130
1131 #[test]
1132 fn test_parse() {
1133 let out = Duration::parse("1ns");
1134 assert_eq!(out.nsecs, 1);
1135 let out = Duration::parse("1ns1ms");
1136 assert_eq!(out.nsecs, NS_MILLISECOND + 1);
1137 let out = Duration::parse("123ns40ms");
1138 assert_eq!(out.nsecs, 40 * NS_MILLISECOND + 123);
1139 let out = Duration::parse("123ns40ms1w");
1140 assert_eq!(out.nsecs, 40 * NS_MILLISECOND + 123);
1141 assert_eq!(out.duration_ns(), 40 * NS_MILLISECOND + 123 + NS_WEEK);
1142 let out = Duration::parse("-123ns40ms1w");
1143 assert!(out.negative);
1144 let out = Duration::parse("5w");
1145 assert_eq!(out.weeks(), 5);
1146 }
1147
1148 #[test]
1149 fn test_parse_interval() {
1150 let d = Duration::try_parse_interval("3 DAYS").unwrap();
1151 assert_eq!(d.days(), 3);
1152
1153 let d = Duration::try_parse_interval("1 year, 2 months, 1 week").unwrap();
1154 assert_eq!(d.months(), 14);
1155 assert_eq!(d.weeks(), 1);
1156
1157 let d = Duration::try_parse_interval("100ms 100us").unwrap();
1158 assert_eq!(d.duration_us(), 100_100);
1159 }
1160
1161 #[test]
1162 fn test_add_ns() {
1163 let t = 1;
1164 let seven_days = Duration::parse("7d");
1165 let one_week = Duration::parse("1w");
1166
1167 assert_eq!(
1170 seven_days.add_ns(t, None).unwrap(),
1171 one_week.add_ns(t, None).unwrap()
1172 );
1173
1174 let seven_days_negative = Duration::parse("-7d");
1175 let one_week_negative = Duration::parse("-1w");
1176
1177 assert_eq!(
1180 seven_days_negative.add_ns(t, None).unwrap(),
1181 one_week_negative.add_ns(t, None).unwrap()
1182 );
1183 }
1184
1185 #[test]
1186 fn test_display() {
1187 let duration = Duration::parse("1h");
1188 let expected = "3600s";
1189 assert_eq!(format!("{duration}"), expected);
1190 let duration = Duration::parse("1h5ns");
1191 let expected = "3600000000005ns";
1192 assert_eq!(format!("{duration}"), expected);
1193 let duration = Duration::parse("1h5000ns");
1194 let expected = "3600000005us";
1195 assert_eq!(format!("{duration}"), expected);
1196 let duration = Duration::parse("3mo");
1197 let expected = "3mo";
1198 assert_eq!(format!("{duration}"), expected);
1199 let duration = Duration::parse_interval("4 weeks");
1200 let expected = "4w";
1201 assert_eq!(format!("{duration}"), expected);
1202 }
1203}