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