Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
pola-rs
GitHub Repository: pola-rs/polars
Path: blob/main/crates/polars-time/src/windows/duration.rs
6939 views
1
use std::cmp::Ordering;
2
use std::fmt::{Display, Formatter};
3
use std::ops::{Mul, Neg};
4
5
#[cfg(feature = "timezones")]
6
use arrow::legacy::kernels::{Ambiguous, NonExistent};
7
use arrow::legacy::time_zone::Tz;
8
use arrow::temporal_conversions::{
9
MICROSECONDS, MILLISECONDS, NANOSECONDS, timestamp_ms_to_datetime, timestamp_ns_to_datetime,
10
timestamp_us_to_datetime,
11
};
12
use chrono::{Datelike, NaiveDate, NaiveDateTime, NaiveTime, Timelike};
13
use polars_core::datatypes::DataType;
14
use polars_core::prelude::{
15
PolarsResult, TimeZone, datetime_to_timestamp_ms, datetime_to_timestamp_ns,
16
datetime_to_timestamp_us, polars_bail,
17
};
18
use polars_error::polars_ensure;
19
#[cfg(feature = "serde")]
20
use serde::{Deserialize, Serialize};
21
22
use super::calendar::{
23
NS_DAY, NS_HOUR, NS_MICROSECOND, NS_MILLISECOND, NS_MINUTE, NS_SECOND, NS_WEEK,
24
};
25
#[cfg(feature = "timezones")]
26
use crate::utils::{localize_datetime_opt, try_localize_datetime, unlocalize_datetime};
27
use 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))]
32
pub struct Duration {
33
// the number of months for the duration
34
months: i64,
35
// the number of weeks for the duration
36
weeks: i64,
37
// the number of days for the duration
38
days: i64,
39
// the number of nanoseconds for the duration
40
nsecs: i64,
41
// indicates if the duration is negative
42
pub(crate) negative: bool,
43
// indicates if an integer string was passed. e.g. "2i"
44
pub parsed_int: bool,
45
}
46
47
impl PartialOrd<Self> for Duration {
48
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
49
Some(self.cmp(other))
50
}
51
}
52
53
impl Ord for Duration {
54
fn cmp(&self, other: &Self) -> Ordering {
55
self.duration_ns().cmp(&other.duration_ns())
56
}
57
}
58
59
impl 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
74
impl 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
108
impl Duration {
109
/// Create a new integer size `Duration`
110
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
/// Parse a string into a `Duration`
122
///
123
/// Strings are composed of a sequence of number-unit pairs, such as `5d` (5 days). A string may begin with a minus
124
/// sign, in which case it is interpreted as a negative duration. Some examples:
125
///
126
/// * `"1y"`: 1 year
127
/// * `"-1w2d"`: negative 1 week, 2 days (i.e. -9 days)
128
/// * `"3d12h4m25s"`: 3 days, 12 hours, 4 minutes, and 25 seconds
129
///
130
/// Aside from a leading minus sign, strings may not contain any characters other than numbers and letters
131
/// (including whitespace).
132
///
133
/// The available units, in ascending order of magnitude, are as follows:
134
///
135
/// * `ns`: nanosecond
136
/// * `us`: microsecond
137
/// * `ms`: millisecond
138
/// * `s`: second
139
/// * `m`: minute
140
/// * `h`: hour
141
/// * `d`: day
142
/// * `w`: week
143
/// * `mo`: calendar month
144
/// * `q`: calendar quarter
145
/// * `y`: calendar year
146
/// * `i`: index value (only for {Int32, Int64} dtypes)
147
///
148
/// By "calendar day", we mean the corresponding time on the next
149
/// day (which may not be 24 hours, depending on daylight savings).
150
/// Similarly for "calendar week", "calendar month", "calendar quarter",
151
/// and "calendar year".
152
///
153
/// # Panics
154
/// If the given str is invalid for any reason.
155
pub fn parse(duration: &str) -> Self {
156
Self::try_parse(duration).unwrap()
157
}
158
159
#[doc(hidden)]
160
/// Parse SQL-style "interval" string to Duration. Handles verbose
161
/// units (such as 'year', 'minutes', etc.) and whitespace, as
162
/// well as being case-insensitive.
163
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
// TODO: intervals need to support per-element minus signs
186
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
// skip the '-' char
201
if negative {
202
start += 1;
203
iter.next().unwrap();
204
}
205
// permissive whitespace for intervals
206
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
// reserve capacity for the longest valid unit ("microseconds")
217
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
// matches that are allowed for both duration/interval
252
"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
// interval-only (verbose/sql) matches
269
"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
/// Normalize the duration within the interval.
317
/// It will ensure that the output duration is the smallest positive
318
/// duration that is the equivalent of the current duration.
319
#[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
/// Creates a [`Duration`] that represents a fixed number of nanoseconds.
362
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
/// Creates a [`Duration`] that represents a fixed number of months.
375
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
/// Creates a [`Duration`] that represents a fixed number of weeks.
388
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
/// Creates a [`Duration`] that represents a fixed number of days.
401
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
/// `true` if zero duration.
414
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
/// Returns whether the duration consists of full days.
443
///
444
/// Note that 24 hours is not considered a full day due to possible
445
/// daylight savings time transitions.
446
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
// For non-native, non-UTC time zones, 1 calendar day is not
455
// necessarily 24 hours due to daylight savings time.
456
self.months == 0 && self.weeks == 0 && self.days == 0
457
}
458
}
459
460
/// Returns the nanoseconds from the `Duration` without the weeks or months part.
461
pub fn nanoseconds(&self) -> i64 {
462
self.nsecs
463
}
464
465
/// Returns whether duration is negative.
466
pub fn negative(&self) -> bool {
467
self.negative
468
}
469
470
/// Estimated duration of the window duration. Not a very good one if not a constant duration.
471
#[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
// Retrieve the current date and increment the values
501
// based on the number of months
502
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 the month overflowed or underflowed, adjust the year
509
// accordingly. Because we add the modulo for the months
510
// the year will only adjust by one
511
if month > 12 {
512
year += 1;
513
month -= 12;
514
} else if month <= 0 {
515
year -= 1;
516
month += 12;
517
}
518
519
// Normalize the day if we are past the end of the month.
520
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
// Retrieve the original time and construct a data
528
// with the new year, month and day
529
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
/// Localize result to given time zone, respecting DST fold of original datetime.
539
/// For example, 2022-11-06 01:30:00 CST truncated by 1 hour becomes 2022-11-06 01:00:00 CST,
540
/// whereas 2022-11-06 01:30:00 CDT truncated by 1 hour becomes 2022-11-06 01:00:00 CDT.
541
///
542
/// * `original_dt_local` - original datetime, without time zone.
543
/// E.g. if the original datetime was 2022-11-06 01:30:00 CST, then this would
544
/// be 2022-11-06 01:30:00.
545
/// * `original_dt_utc` - original datetime converted to UTC. E.g. if the
546
/// original datetime was 2022-11-06 01:30:00 CST, then this would
547
/// be 2022-11-06 07:30:00.
548
/// * `result_dt_local` - result, without time zone.
549
#[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
// for UTC, use fastpath below (same as naive)
614
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
// for UTC, use fastpath below (same as naive)
655
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
// If we did
667
// t - (t % (7 * self.weeks * daily_duration))
668
// then the timestamp would get truncated to the previous Thursday,
669
// because 1970-01-01 (timestamp 0) is a Thursday.
670
// So, we adjust by 4 days to get to Monday.
671
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
// for UTC, use fastpath below (same as naive)
679
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
// for UTC, use fastpath below (same as naive)
709
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
// Remove the time of day from the timestamp
722
// e.g. 2020-01-01 12:34:56 -> 2020-01-01 00:00:00
723
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
// Calculate how many months we need to subtract...
730
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
// ...and translate that to how many days we need to subtract.
741
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
// for UTC, use fastpath below (same as naive)
766
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
// truncate by ns/us/ms
793
(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
// truncate by days
804
(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
// truncate by weeks
815
(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
// truncate by months
826
(_, 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
// Truncate the given ns timestamp by the window boundary.
843
#[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
// Truncate the given ns timestamp by the window boundary.
855
#[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
// Truncate the given ms timestamp by the window boundary.
867
#[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
// for UTC, use fastpath below (same as naive)
897
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
// for UTC, use fastpath below (same as naive)
906
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
// for UTC, use fastpath below (same as naive)
919
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
// for UTC, use fastpath below (same as naive)
941
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
1001
impl 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
1017
fn 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
1031
pub 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
1046
pub 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)]
1068
mod 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
// add_ns can only error if a time zone is passed, so it's
1095
// safe to unwrap here
1096
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
// add_ns can only error if a time zone is passed, so it's
1105
// safe to unwrap here
1106
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
}
1131
1132