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 months: i64,
36 weeks: i64,
38 days: i64,
40 nsecs: i64,
42 pub(crate) negative: bool,
44 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 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 pub fn parse(duration: &str) -> Self {
157 Self::try_parse(duration).unwrap()
158 }
159
160 #[doc(hidden)]
161 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 let original_string = s;
182 let s = s.as_bytes();
183 let mut pos = 0;
184
185 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_minus || leading_plus {
194 pos += 1;
195 }
196
197 if as_interval {
199 while pos < s.len() && s[pos] == b' ' {
200 pos += 1;
201 }
202 }
203
204 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 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 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 if as_interval {
254 while pos < s.len() && (s[pos] == b' ' || s[pos] == b',') {
255 pos += 1;
256 }
257 }
258
259 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 if pos < s.len() && (s[pos] == b'-' || s[pos] == b'+') {
274 error_on_second_plus_minus!(s[pos]);
275 }
276
277 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 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 _ 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 #[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 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 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 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 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 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 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 self.months == 0 && self.weeks == 0 && self.days == 0
483 }
484 }
485
486 pub fn nanoseconds(&self) -> i64 {
488 self.nsecs
489 }
490
491 pub fn negative(&self) -> bool {
493 self.negative
494 }
495
496 #[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 #[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 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 month > 12 {
564 year += 1;
565 month -= 12;
566 } else if month <= 0 {
567 year -= 1;
568 month += 12;
569 }
570
571 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 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 #[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 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 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 = 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 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 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 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 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 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 (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 (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 (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 (_, 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 #[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 #[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 #[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 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 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 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 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 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 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}