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