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