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
8424 views
1
use std::cmp::Ordering;
2
use std::fmt::{Display, Formatter};
3
use std::ops::{Add, Mul, Neg};
4
5
use arrow::legacy::time_zone::Tz;
6
use arrow::temporal_conversions::{
7
MICROSECONDS, MILLISECONDS, NANOSECONDS, timestamp_ms_to_datetime, timestamp_ns_to_datetime,
8
timestamp_us_to_datetime,
9
};
10
#[cfg(feature = "timezones")]
11
use chrono::TimeZone as ChronoTimeZone;
12
#[cfg(feature = "timezones")]
13
use chrono::offset::LocalResult;
14
use chrono::{Datelike, NaiveDate, NaiveDateTime, NaiveTime, TimeDelta, Timelike};
15
#[cfg(feature = "timezones")]
16
use chrono_tz::OffsetComponents;
17
use polars_core::datatypes::DataType;
18
use polars_core::prelude::{
19
Ambiguous, NonExistent, PolarsResult, TimeZone, datetime_to_timestamp_ms,
20
datetime_to_timestamp_ns, datetime_to_timestamp_us, polars_bail,
21
};
22
use polars_error::polars_ensure;
23
#[cfg(feature = "serde")]
24
use serde::{Deserialize, Serialize};
25
26
use super::calendar::{
27
NS_DAY, NS_HOUR, NS_MICROSECOND, NS_MILLISECOND, NS_MINUTE, NS_SECOND, NS_WEEK, NTE_NS_DAY,
28
NTE_NS_WEEK,
29
};
30
#[cfg(feature = "timezones")]
31
use crate::utils::try_localize_datetime;
32
#[cfg(feature = "timezones")]
33
use crate::utils::unlocalize_datetime;
34
use crate::windows::calendar::{DAYS_PER_MONTH, is_leap_year};
35
36
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
37
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
38
#[cfg_attr(feature = "dsl-schema", derive(schemars::JsonSchema))]
39
pub struct Duration {
40
// the number of months for the duration
41
months: i64,
42
// the number of weeks for the duration
43
weeks: i64,
44
// the number of days for the duration
45
days: i64,
46
// the number of nanoseconds for the duration
47
nsecs: i64,
48
// indicates if the duration is negative
49
pub(crate) negative: bool,
50
// indicates if an integer string was passed. e.g. "2i"
51
pub parsed_int: bool,
52
}
53
54
impl PartialOrd<Self> for Duration {
55
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
56
Some(self.cmp(other))
57
}
58
}
59
60
impl Ord for Duration {
61
fn cmp(&self, other: &Self) -> Ordering {
62
self.duration_ns().cmp(&other.duration_ns())
63
}
64
}
65
66
impl Neg for Duration {
67
type Output = Self;
68
69
fn neg(self) -> Self::Output {
70
Self {
71
months: self.months,
72
weeks: self.weeks,
73
days: self.days,
74
nsecs: self.nsecs,
75
negative: !self.negative,
76
parsed_int: self.parsed_int,
77
}
78
}
79
}
80
81
impl Display for Duration {
82
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
83
if self.is_zero() {
84
return write!(f, "0s");
85
}
86
if self.negative {
87
write!(f, "-")?
88
}
89
if self.months > 0 {
90
write!(f, "{}mo", self.months)?
91
}
92
if self.weeks > 0 {
93
write!(f, "{}w", self.weeks)?
94
}
95
if self.days > 0 {
96
write!(f, "{}d", self.days)?
97
}
98
if self.nsecs > 0 {
99
let secs = self.nsecs / NANOSECONDS;
100
if secs * NANOSECONDS == self.nsecs {
101
write!(f, "{secs}s")?
102
} else {
103
let us = self.nsecs / 1_000;
104
if us * 1_000 == self.nsecs {
105
write!(f, "{us}us")?
106
} else {
107
write!(f, "{}ns", self.nsecs)?
108
}
109
}
110
}
111
Ok(())
112
}
113
}
114
115
impl Duration {
116
/// Create a new integer size `Duration`
117
pub const fn new(fixed_slots: i64) -> Self {
118
Duration {
119
months: 0,
120
weeks: 0,
121
days: 0,
122
nsecs: fixed_slots.abs(),
123
negative: fixed_slots < 0,
124
parsed_int: true,
125
}
126
}
127
128
/// Parse a string into a `Duration`
129
///
130
/// Strings are composed of a sequence of number-unit pairs, such as `5d` (5 days). A string may begin with a minus
131
/// sign, in which case it is interpreted as a negative duration. Some examples:
132
///
133
/// * `"1y"`: 1 year
134
/// * `"-1w2d"`: negative 1 week, 2 days (i.e. -9 days)
135
/// * `"3d12h4m25s"`: 3 days, 12 hours, 4 minutes, and 25 seconds
136
///
137
/// Aside from a leading minus sign, strings may not contain any characters other than numbers and letters
138
/// (including whitespace).
139
///
140
/// The available units, in ascending order of magnitude, are as follows:
141
///
142
/// * `ns`: nanosecond
143
/// * `us`: microsecond
144
/// * `ms`: millisecond
145
/// * `s`: second
146
/// * `m`: minute
147
/// * `h`: hour
148
/// * `d`: day
149
/// * `w`: week
150
/// * `mo`: calendar month
151
/// * `q`: calendar quarter
152
/// * `y`: calendar year
153
/// * `i`: index value (only for {Int32, Int64} dtypes)
154
///
155
/// By "calendar day", we mean the corresponding time on the next
156
/// day (which may not be 24 hours, depending on daylight savings).
157
/// Similarly for "calendar week", "calendar month", "calendar quarter",
158
/// and "calendar year".
159
///
160
/// # Panics
161
/// If the given str is invalid for any reason.
162
pub fn parse(duration: &str) -> Self {
163
Self::try_parse(duration).unwrap()
164
}
165
166
#[doc(hidden)]
167
/// Parse SQL-style "interval" string to Duration. Handles verbose
168
/// units (such as 'year', 'minutes', etc.) and whitespace, as
169
/// well as being case-insensitive.
170
pub fn parse_interval(interval: &str) -> Self {
171
Self::try_parse_interval(interval).unwrap()
172
}
173
174
pub fn try_parse(duration: &str) -> PolarsResult<Self> {
175
Self::_parse(duration, false)
176
}
177
178
pub fn try_parse_interval(interval: &str) -> PolarsResult<Self> {
179
Self::_parse(&interval.to_ascii_lowercase(), true)
180
}
181
182
fn _parse(s: &str, as_interval: bool) -> PolarsResult<Self> {
183
let s = if as_interval { s.trim_start() } else { s };
184
let parse_type = if as_interval { "interval" } else { "duration" };
185
186
// can work on raw bytes (much faster), as valid interval/duration strings are all ASCII
187
let original_string = s;
188
let s = s.as_bytes();
189
let mut pos = 0;
190
191
// check for an initial '+'/'-' char
192
let (leading_minus, leading_plus) = match s.first() {
193
Some(&b'-') => (true, false),
194
Some(&b'+') => (false, true),
195
_ => (false, false),
196
};
197
198
// if leading '+'/'-' found, consume it
199
if leading_minus || leading_plus {
200
pos += 1;
201
}
202
203
// permissive whitespace for intervals
204
if as_interval {
205
while pos < s.len() && s[pos] == b' ' {
206
pos += 1;
207
}
208
}
209
210
// we only allow/expect a single leading '+' or '-' char
211
macro_rules! error_on_second_plus_minus {
212
($ch:expr) => {{
213
let previously_seen = if $ch == b'-' { leading_minus } else { leading_plus };
214
if previously_seen {
215
polars_bail!(InvalidOperation: "{} string can only have a single '{}' sign", parse_type, $ch as char);
216
}
217
let sign = if $ch == b'-' { "minus" } else { "plus" };
218
if as_interval {
219
polars_bail!(InvalidOperation: "{} signs are not currently supported in interval strings", sign);
220
} else {
221
polars_bail!(InvalidOperation: "only a single {} sign is allowed, at the front of the string", sign);
222
}
223
}};
224
}
225
226
// walk the byte-string, identifying number-unit pairs
227
let mut parsed_int = false;
228
let mut months = 0;
229
let mut weeks = 0;
230
let mut days = 0;
231
let mut nsecs = 0;
232
233
while pos < s.len() {
234
let ch = s[pos];
235
if !ch.is_ascii_digit() {
236
if ch == b'-' || ch == b'+' {
237
error_on_second_plus_minus!(ch);
238
}
239
polars_bail!(InvalidOperation:
240
"expected leading integer in the {} string, found '{}'",
241
parse_type, ch as char
242
);
243
}
244
245
// get integer value from the raw bytes
246
let mut n = 0i64;
247
while pos < s.len() && s[pos].is_ascii_digit() {
248
n = n * 10 + (s[pos] - b'0') as i64;
249
pos += 1;
250
}
251
if pos >= s.len() {
252
polars_bail!(InvalidOperation:
253
"expected a valid unit to follow integer in the {} string '{}'",
254
parse_type, original_string
255
);
256
}
257
258
// skip leading whitespace/commas before unit (for intervals)
259
if as_interval {
260
while pos < s.len() && (s[pos] == b' ' || s[pos] == b',') {
261
pos += 1;
262
}
263
}
264
265
// parse the unit associated with the given integer value
266
let unit_start = pos;
267
while pos < s.len() && s[pos].is_ascii_alphabetic() {
268
pos += 1;
269
}
270
let unit_end = pos;
271
if unit_start == unit_end {
272
polars_bail!(InvalidOperation:
273
"expected a valid unit to follow integer in the {} string '{}'",
274
parse_type, original_string
275
);
276
}
277
278
// only valid location for '+'/'-' chars is at the start
279
if pos < s.len() && (s[pos] == b'-' || s[pos] == b'+') {
280
error_on_second_plus_minus!(s[pos]);
281
}
282
283
// skip any whitespace/comma that follows an interval unit
284
if as_interval {
285
while pos < s.len() && (s[pos] == b' ' || s[pos] == b',') {
286
pos += 1;
287
}
288
}
289
290
let unit = &s[unit_start..unit_end];
291
match unit {
292
// matches that are allowed for both duration and interval
293
b"ns" => nsecs += n,
294
b"us" => nsecs += n * NS_MICROSECOND,
295
b"ms" => nsecs += n * NS_MILLISECOND,
296
b"s" => nsecs += n * NS_SECOND,
297
b"m" => nsecs += n * NS_MINUTE,
298
b"h" => nsecs += n * NS_HOUR,
299
b"d" => days += n,
300
b"w" => weeks += n,
301
b"mo" => months += n,
302
b"q" => months += n * 3,
303
b"y" => months += n * 12,
304
b"i" => {
305
nsecs += n;
306
parsed_int = true;
307
},
308
// interval-only (verbose/sql) matches
309
_ if as_interval => match unit {
310
b"nanosecond" | b"nanoseconds" => nsecs += n,
311
b"microsecond" | b"microseconds" => nsecs += n * NS_MICROSECOND,
312
b"millisecond" | b"milliseconds" => nsecs += n * NS_MILLISECOND,
313
b"sec" | b"secs" | b"second" | b"seconds" => nsecs += n * NS_SECOND,
314
b"min" | b"mins" | b"minute" | b"minutes" => nsecs += n * NS_MINUTE,
315
b"hour" | b"hours" => nsecs += n * NS_HOUR,
316
b"day" | b"days" => days += n,
317
b"week" | b"weeks" => weeks += n,
318
b"mon" | b"mons" | b"month" | b"months" => months += n,
319
b"quarter" | b"quarters" => months += n * 3,
320
b"year" | b"years" => months += n * 12,
321
_ => {
322
let unit_str = std::str::from_utf8(unit).unwrap_or("<invalid>");
323
let valid_units = "'year', 'month', 'quarter', 'week', 'day', 'hour', 'minute', 'second', 'millisecond', 'microsecond', 'nanosecond'";
324
polars_bail!(InvalidOperation: "unit: '{}' not supported; available units include: {} (and their plurals)", unit_str, valid_units);
325
},
326
},
327
_ => {
328
let unit_str = std::str::from_utf8(unit).unwrap_or("<invalid>");
329
polars_bail!(InvalidOperation: "unit: '{}' not supported; available units are: 'y', 'mo', 'q', 'w', 'd', 'h', 'm', 's', 'ms', 'us', 'ns'", unit_str);
330
},
331
}
332
}
333
334
Ok(Duration {
335
months: months.abs(),
336
weeks: weeks.abs(),
337
days: days.abs(),
338
nsecs: nsecs.abs(),
339
negative: leading_minus,
340
parsed_int,
341
})
342
}
343
344
fn to_positive(v: i64) -> (bool, i64) {
345
if v < 0 { (true, -v) } else { (false, v) }
346
}
347
348
/// Normalize the duration within the interval.
349
/// It will ensure that the output duration is the smallest positive
350
/// duration that is the equivalent of the current duration.
351
#[allow(dead_code)]
352
pub(crate) fn normalize(&self, interval: &Duration) -> Self {
353
if self.months_only() && interval.months_only() {
354
let mut months = self.months() % interval.months();
355
356
match (self.negative, interval.negative) {
357
(true, true) | (true, false) => months = -months + interval.months(),
358
_ => {},
359
}
360
Duration::from_months(months)
361
} else if self.weeks_only() && interval.weeks_only() {
362
let mut weeks = self.weeks() % interval.weeks();
363
364
match (self.negative, interval.negative) {
365
(true, true) | (true, false) => weeks = -weeks + interval.weeks(),
366
_ => {},
367
}
368
Duration::from_weeks(weeks)
369
} else if self.days_only() && interval.days_only() {
370
let mut days = self.days() % interval.days();
371
372
match (self.negative, interval.negative) {
373
(true, true) | (true, false) => days = -days + interval.days(),
374
_ => {},
375
}
376
Duration::from_days(days)
377
} else {
378
let mut offset = self.duration_ns();
379
if offset == 0 {
380
return *self;
381
}
382
let every = interval.duration_ns();
383
384
if offset < 0 {
385
offset += every * ((offset / -every) + 1)
386
} else {
387
offset -= every * (offset / every)
388
}
389
Duration::from_nsecs(offset)
390
}
391
}
392
393
/// Creates a [`Duration`] that represents a fixed number of nanoseconds.
394
pub(crate) fn from_nsecs(v: i64) -> Self {
395
let (negative, nsecs) = Self::to_positive(v);
396
Self {
397
months: 0,
398
weeks: 0,
399
days: 0,
400
nsecs,
401
negative,
402
parsed_int: false,
403
}
404
}
405
406
/// Creates a [`Duration`] that represents a fixed number of months.
407
pub(crate) fn from_months(v: i64) -> Self {
408
let (negative, months) = Self::to_positive(v);
409
Self {
410
months,
411
weeks: 0,
412
days: 0,
413
nsecs: 0,
414
negative,
415
parsed_int: false,
416
}
417
}
418
419
/// Creates a [`Duration`] that represents a fixed number of weeks.
420
pub(crate) fn from_weeks(v: i64) -> Self {
421
let (negative, weeks) = Self::to_positive(v);
422
Self {
423
months: 0,
424
weeks,
425
days: 0,
426
nsecs: 0,
427
negative,
428
parsed_int: false,
429
}
430
}
431
432
/// Creates a [`Duration`] that represents a fixed number of days.
433
pub(crate) fn from_days(v: i64) -> Self {
434
let (negative, days) = Self::to_positive(v);
435
Self {
436
months: 0,
437
weeks: 0,
438
days,
439
nsecs: 0,
440
negative,
441
parsed_int: false,
442
}
443
}
444
445
/// `true` if zero duration.
446
pub fn is_zero(&self) -> bool {
447
self.months == 0 && self.weeks == 0 && self.days == 0 && self.nsecs == 0
448
}
449
450
pub fn months_only(&self) -> bool {
451
self.months != 0 && self.weeks == 0 && self.days == 0 && self.nsecs == 0
452
}
453
454
pub fn months(&self) -> i64 {
455
self.months
456
}
457
458
pub fn weeks_only(&self) -> bool {
459
self.months == 0 && self.weeks != 0 && self.days == 0 && self.nsecs == 0
460
}
461
462
pub fn weeks(&self) -> i64 {
463
self.weeks
464
}
465
466
pub fn days_only(&self) -> bool {
467
self.months == 0 && self.weeks == 0 && self.days != 0 && self.nsecs == 0
468
}
469
470
pub fn days(&self) -> i64 {
471
self.days
472
}
473
474
/// Returns whether the duration consists of full days.
475
///
476
/// Note that 24 hours is not considered a full day due to possible
477
/// daylight savings time transitions.
478
pub fn is_full_days(&self) -> bool {
479
self.nsecs == 0
480
}
481
482
pub fn is_constant_duration(&self, time_zone: Option<&TimeZone>) -> bool {
483
if time_zone.is_none() || time_zone == Some(&TimeZone::UTC) {
484
self.months == 0
485
} else {
486
// For non-native, non-UTC time zones, 1 calendar day is not
487
// necessarily 24 hours due to daylight savings time.
488
self.months == 0 && self.weeks == 0 && self.days == 0
489
}
490
}
491
492
/// Returns the nanoseconds from the `Duration` without the weeks or months part.
493
pub fn nanoseconds(&self) -> i64 {
494
self.nsecs
495
}
496
497
/// Returns whether duration is negative.
498
pub fn negative(&self) -> bool {
499
self.negative
500
}
501
502
/// Estimated duration of the window duration. Not a very good one if not a constant duration.
503
#[doc(hidden)]
504
pub const fn duration_ns(&self) -> i64 {
505
self.months * 28 * 24 * 3600 * NANOSECONDS
506
+ self.weeks * NS_WEEK
507
+ self.days * NS_DAY
508
+ self.nsecs
509
}
510
511
#[doc(hidden)]
512
pub const fn duration_us(&self) -> i64 {
513
self.months * 28 * 24 * 3600 * MICROSECONDS
514
+ (self.weeks * NS_WEEK / 1000 + self.nsecs / 1000 + self.days * NS_DAY / 1000)
515
}
516
517
#[doc(hidden)]
518
pub const fn duration_ms(&self) -> i64 {
519
self.months * 28 * 24 * 3600 * MILLISECONDS
520
+ (self.weeks * NS_WEEK / 1_000_000
521
+ self.nsecs / 1_000_000
522
+ self.days * NS_DAY / 1_000_000)
523
}
524
525
/// Not-to-exceed estimated duration of the window duration. The actual duration will be
526
/// less or equal than the estimate.
527
#[doc(hidden)]
528
pub const fn nte_duration_ns(&self) -> i64 {
529
self.months * (31 * 24 + 1) * 3600 * NANOSECONDS
530
+ self.weeks * NTE_NS_WEEK
531
+ self.days * NTE_NS_DAY
532
+ self.nsecs
533
}
534
535
#[doc(hidden)]
536
pub const fn nte_duration_us(&self) -> i64 {
537
self.months * (31 * 24 + 1) * 3600 * MICROSECONDS
538
+ self.weeks * NTE_NS_WEEK / 1000
539
+ self.days * NTE_NS_DAY / 1000
540
+ self.nsecs / 1000
541
}
542
543
#[doc(hidden)]
544
pub const fn nte_duration_ms(&self) -> i64 {
545
self.months * (31 * 24 + 1) * 3600 * MILLISECONDS
546
+ self.weeks * NTE_NS_WEEK / 1_000_000
547
+ self.days * NTE_NS_DAY / 1_000_000
548
+ self.nsecs / 1_000_000
549
}
550
551
#[doc(hidden)]
552
fn add_month(ts: NaiveDateTime, n_months: i64, negative: bool) -> NaiveDateTime {
553
let mut months = n_months;
554
if negative {
555
months = -months;
556
}
557
558
// Retrieve the current date and increment the values
559
// based on the number of months
560
let mut year = ts.year();
561
let mut month = ts.month() as i32;
562
let mut day = ts.day();
563
year += (months / 12) as i32;
564
month += (months % 12) as i32;
565
566
// if the month overflowed or underflowed, adjust the year
567
// accordingly. Because we add the modulo for the months
568
// the year will only adjust by one
569
if month > 12 {
570
year += 1;
571
month -= 12;
572
} else if month <= 0 {
573
year -= 1;
574
month += 12;
575
}
576
577
// Normalize the day if we are past the end of the month.
578
let last_day_of_month =
579
DAYS_PER_MONTH[is_leap_year(year) as usize][(month - 1) as usize] as u32;
580
581
if day > last_day_of_month {
582
day = last_day_of_month
583
}
584
585
// Retrieve the original time and construct a data
586
// with the new year, month and day
587
let hour = ts.hour();
588
let minute = ts.minute();
589
let sec = ts.second();
590
let nsec = ts.nanosecond();
591
new_datetime(year, month as u32, day, hour, minute, sec, nsec).expect(
592
"Expected valid datetime, please open an issue at https://github.com/pola-rs/polars/issues"
593
)
594
}
595
596
/// Localize result to given time zone, respecting RFC5545 to deal with non-existent or
597
/// ambiguous results.
598
///
599
/// For ambiguous and non-existent results, we preserve the DST fold of the original datetime.
600
///
601
/// * `original_dt_utc` - original datetime converted to UTC. E.g. if the
602
/// original datetime was 2022-11-06 01:30:00 CST, then this would
603
/// be 2022-11-06 07:30:00.
604
/// * `result_dt_local` - result, without time zone.
605
/// * `tz` - time zone.
606
#[cfg(feature = "timezones")]
607
fn localize_result_rfc_5545(
608
&self,
609
original_dt_utc: NaiveDateTime,
610
result_dt_local: NaiveDateTime,
611
tz: &Tz,
612
) -> PolarsResult<NaiveDateTime> {
613
let result_localized = tz.from_local_datetime(&result_dt_local);
614
match result_localized {
615
LocalResult::Single(result) => Ok(result.naive_utc()),
616
LocalResult::Ambiguous(result_earliest, result_latest) => {
617
let original_localized = tz.from_utc_datetime(&original_dt_utc);
618
let original_dst_offset = original_localized.offset().dst_offset();
619
if result_earliest.offset().dst_offset() == original_dst_offset {
620
return Ok(result_earliest.naive_utc());
621
}
622
if result_latest.offset().dst_offset() == original_dst_offset {
623
return Ok(result_latest.naive_utc());
624
}
625
polars_bail!(ComputeError: "Could not localize datetime '{}' to time zone '{}'", result_dt_local, tz);
626
},
627
LocalResult::None => {
628
let original_localized = tz.from_utc_datetime(&original_dt_utc);
629
let original_dst_offset = original_localized.offset().dst_offset();
630
let shifted: NaiveDateTime;
631
if original_dst_offset.num_minutes() != 0 {
632
shifted = result_dt_local.add(original_dst_offset);
633
} else if let Some(next_hour) = tz
634
.from_local_datetime(&result_dt_local.add(TimeDelta::hours(1)))
635
.earliest()
636
{
637
// Try shifting forwards to get the DST offset of the would-be-result.
638
let result_dst_offset = next_hour.offset().dst_offset();
639
shifted = result_dt_local.add(-result_dst_offset);
640
} else {
641
polars_bail!(ComputeError: "Could not localize datetime '{}' to time zone '{}'", result_dt_local, tz);
642
}
643
Ok(
644
try_localize_datetime(shifted, tz, Ambiguous::Raise, NonExistent::Raise)?
645
.expect("we didn't use Ambiguous::Null or NonExistent::Null"),
646
)
647
},
648
}
649
}
650
651
fn truncate_subweekly<G, J>(
652
&self,
653
t: i64,
654
tz: Option<&Tz>,
655
duration: i64,
656
_timestamp_to_datetime: G,
657
_datetime_to_timestamp: J,
658
) -> PolarsResult<i64>
659
where
660
G: Fn(i64) -> NaiveDateTime,
661
J: Fn(NaiveDateTime) -> i64,
662
{
663
match tz {
664
#[cfg(feature = "timezones")]
665
// for UTC, use fastpath below (same as naive)
666
Some(tz) if tz != &chrono_tz::UTC => {
667
let original_dt_utc = _timestamp_to_datetime(t);
668
let original_dt_local = unlocalize_datetime(original_dt_utc, tz);
669
let t = _datetime_to_timestamp(original_dt_local);
670
let mut remainder = t % duration;
671
if remainder < 0 {
672
remainder += duration
673
}
674
let result_timestamp = t - remainder;
675
let result_dt_local = _timestamp_to_datetime(result_timestamp);
676
let result_dt_utc =
677
self.localize_result_rfc_5545(original_dt_utc, result_dt_local, tz)?;
678
Ok(_datetime_to_timestamp(result_dt_utc))
679
},
680
_ => {
681
let mut remainder = t % duration;
682
if remainder < 0 {
683
remainder += duration
684
}
685
Ok(t - remainder)
686
},
687
}
688
}
689
690
fn truncate_weekly<G, J>(
691
&self,
692
t: i64,
693
tz: Option<&Tz>,
694
_timestamp_to_datetime: G,
695
_datetime_to_timestamp: J,
696
daily_duration: i64,
697
) -> PolarsResult<i64>
698
where
699
G: Fn(i64) -> NaiveDateTime,
700
J: Fn(NaiveDateTime) -> i64,
701
{
702
let _original_dt_utc: Option<NaiveDateTime>;
703
let _original_dt_local: Option<NaiveDateTime>;
704
let t = match tz {
705
#[cfg(feature = "timezones")]
706
// for UTC, use fastpath below (same as naive)
707
Some(tz) if tz != &chrono_tz::UTC => {
708
_original_dt_utc = Some(_timestamp_to_datetime(t));
709
_original_dt_local = Some(unlocalize_datetime(_original_dt_utc.unwrap(), tz));
710
_datetime_to_timestamp(_original_dt_local.unwrap())
711
},
712
_ => {
713
_original_dt_utc = None;
714
_original_dt_local = None;
715
t
716
},
717
};
718
// If we did
719
// t - (t % (7 * self.weeks * daily_duration))
720
// then the timestamp would get truncated to the previous Thursday,
721
// because 1970-01-01 (timestamp 0) is a Thursday.
722
// So, we adjust by 4 days to get to Monday.
723
let mut remainder = (t - 4 * daily_duration) % (7 * self.weeks * daily_duration);
724
if remainder < 0 {
725
remainder += 7 * self.weeks * daily_duration
726
}
727
let result_t_local = t - remainder;
728
match tz {
729
#[cfg(feature = "timezones")]
730
// for UTC, use fastpath below (same as naive)
731
Some(tz) if tz != &chrono_tz::UTC => {
732
let result_dt_local = _timestamp_to_datetime(result_t_local);
733
let result_dt_utc =
734
self.localize_result_rfc_5545(_original_dt_utc.unwrap(), result_dt_local, tz)?;
735
Ok(_datetime_to_timestamp(result_dt_utc))
736
},
737
_ => Ok(result_t_local),
738
}
739
}
740
fn truncate_monthly<G, J>(
741
&self,
742
t: i64,
743
tz: Option<&Tz>,
744
timestamp_to_datetime: G,
745
datetime_to_timestamp: J,
746
daily_duration: i64,
747
) -> PolarsResult<i64>
748
where
749
G: Fn(i64) -> NaiveDateTime,
750
J: Fn(NaiveDateTime) -> i64,
751
{
752
let original_dt_utc;
753
let original_dt_local;
754
let t = match tz {
755
#[cfg(feature = "timezones")]
756
// for UTC, use fastpath below (same as naive)
757
Some(tz) if tz != &chrono_tz::UTC => {
758
original_dt_utc = timestamp_to_datetime(t);
759
original_dt_local = unlocalize_datetime(original_dt_utc, tz);
760
datetime_to_timestamp(original_dt_local)
761
},
762
_ => {
763
original_dt_utc = timestamp_to_datetime(t);
764
original_dt_local = original_dt_utc;
765
datetime_to_timestamp(original_dt_local)
766
},
767
};
768
769
// Remove the time of day from the timestamp
770
// e.g. 2020-01-01 12:34:56 -> 2020-01-01 00:00:00
771
let mut remainder_time = t % daily_duration;
772
if remainder_time < 0 {
773
remainder_time += daily_duration
774
}
775
let t = t - remainder_time;
776
777
// Calculate how many months we need to subtract...
778
let (mut year, mut month) = (
779
original_dt_local.year() as i64,
780
original_dt_local.month() as i64,
781
);
782
let total = ((year - 1970) * 12) + (month - 1);
783
let mut remainder_months = total % self.months;
784
if remainder_months < 0 {
785
remainder_months += self.months
786
}
787
788
// ...and translate that to how many days we need to subtract.
789
let mut _is_leap_year = is_leap_year(year as i32);
790
let mut remainder_days = (original_dt_local.day() - 1) as i64;
791
while remainder_months > 12 {
792
let prev_year_is_leap_year = is_leap_year((year - 1) as i32);
793
let add_extra_day =
794
(_is_leap_year && month > 2) || (prev_year_is_leap_year && month <= 2);
795
remainder_days += 365 + add_extra_day as i64;
796
remainder_months -= 12;
797
year -= 1;
798
_is_leap_year = prev_year_is_leap_year;
799
}
800
while remainder_months > 0 {
801
month -= 1;
802
if month == 0 {
803
year -= 1;
804
_is_leap_year = is_leap_year(year as i32);
805
month = 12;
806
}
807
remainder_days += DAYS_PER_MONTH[_is_leap_year as usize][(month - 1) as usize];
808
remainder_months -= 1;
809
}
810
811
match tz {
812
#[cfg(feature = "timezones")]
813
// for UTC, use fastpath below (same as naive)
814
Some(tz) if tz != &chrono_tz::UTC => {
815
let result_dt_local = timestamp_to_datetime(t - remainder_days * daily_duration);
816
let result_dt_utc =
817
self.localize_result_rfc_5545(original_dt_utc, result_dt_local, tz)?;
818
Ok(datetime_to_timestamp(result_dt_utc))
819
},
820
_ => Ok(t - remainder_days * daily_duration),
821
}
822
}
823
824
#[inline]
825
pub fn truncate_impl<F, G, J>(
826
&self,
827
t: i64,
828
tz: Option<&Tz>,
829
nsecs_to_unit: F,
830
timestamp_to_datetime: G,
831
datetime_to_timestamp: J,
832
) -> PolarsResult<i64>
833
where
834
F: Fn(i64) -> i64,
835
G: Fn(i64) -> NaiveDateTime,
836
J: Fn(NaiveDateTime) -> i64,
837
{
838
match (self.months, self.weeks, self.days, self.nsecs) {
839
(0, 0, 0, 0) => polars_bail!(ComputeError: "duration cannot be zero"),
840
// truncate by ns/us/ms
841
(0, 0, 0, _) => {
842
let duration = nsecs_to_unit(self.nsecs);
843
if duration == 0 {
844
return Ok(t);
845
}
846
self.truncate_subweekly(
847
t,
848
tz,
849
duration,
850
timestamp_to_datetime,
851
datetime_to_timestamp,
852
)
853
},
854
// truncate by days
855
(0, 0, _, 0) => {
856
let duration = self.days * nsecs_to_unit(NS_DAY);
857
self.truncate_subweekly(
858
t,
859
tz,
860
duration,
861
timestamp_to_datetime,
862
datetime_to_timestamp,
863
)
864
},
865
// truncate by weeks
866
(0, _, 0, 0) => {
867
let duration = nsecs_to_unit(NS_DAY);
868
self.truncate_weekly(
869
t,
870
tz,
871
timestamp_to_datetime,
872
datetime_to_timestamp,
873
duration,
874
)
875
},
876
// truncate by months
877
(_, 0, 0, 0) => {
878
let duration = nsecs_to_unit(NS_DAY);
879
self.truncate_monthly(
880
t,
881
tz,
882
timestamp_to_datetime,
883
datetime_to_timestamp,
884
duration,
885
)
886
},
887
_ => {
888
polars_bail!(ComputeError: "cannot mix month, week, day, and sub-daily units for this operation")
889
},
890
}
891
}
892
893
// Truncate the given ns timestamp by the window boundary.
894
#[inline]
895
pub fn truncate_ns(&self, t: i64, tz: Option<&Tz>) -> PolarsResult<i64> {
896
self.truncate_impl(
897
t,
898
tz,
899
|nsecs| nsecs,
900
timestamp_ns_to_datetime,
901
datetime_to_timestamp_ns,
902
)
903
}
904
905
// Truncate the given ns timestamp by the window boundary.
906
#[inline]
907
pub fn truncate_us(&self, t: i64, tz: Option<&Tz>) -> PolarsResult<i64> {
908
self.truncate_impl(
909
t,
910
tz,
911
|nsecs| nsecs / 1000,
912
timestamp_us_to_datetime,
913
datetime_to_timestamp_us,
914
)
915
}
916
917
// Truncate the given ms timestamp by the window boundary.
918
#[inline]
919
pub fn truncate_ms(&self, t: i64, tz: Option<&Tz>) -> PolarsResult<i64> {
920
self.truncate_impl(
921
t,
922
tz,
923
|nsecs| nsecs / 1_000_000,
924
timestamp_ms_to_datetime,
925
datetime_to_timestamp_ms,
926
)
927
}
928
929
fn add_impl_month_week_or_day<F, G, J>(
930
&self,
931
mut t: i64,
932
tz: Option<&Tz>,
933
nsecs_to_unit: F,
934
timestamp_to_datetime: G,
935
datetime_to_timestamp: J,
936
) -> PolarsResult<i64>
937
where
938
F: Fn(i64) -> i64,
939
G: Fn(i64) -> NaiveDateTime,
940
J: Fn(NaiveDateTime) -> i64,
941
{
942
let d = self;
943
944
if d.months > 0 {
945
t = match tz {
946
#[cfg(feature = "timezones")]
947
// for UTC, use fastpath below (same as naive)
948
Some(tz) if tz != &chrono_tz::UTC => {
949
let original_dt_utc = timestamp_to_datetime(t);
950
let original_dt_local = unlocalize_datetime(original_dt_utc, tz);
951
let result_dt_local = Self::add_month(original_dt_local, d.months, d.negative);
952
datetime_to_timestamp(self.localize_result_rfc_5545(
953
original_dt_utc,
954
result_dt_local,
955
tz,
956
)?)
957
},
958
_ => datetime_to_timestamp(Self::add_month(
959
timestamp_to_datetime(t),
960
d.months,
961
d.negative,
962
)),
963
};
964
}
965
966
if d.weeks > 0 {
967
let t_weeks = nsecs_to_unit(NS_WEEK) * self.weeks;
968
t = match tz {
969
#[cfg(feature = "timezones")]
970
// for UTC, use fastpath below (same as naive)
971
Some(tz) if tz != &chrono_tz::UTC => {
972
let original_dt_utc = timestamp_to_datetime(t);
973
let original_dt_local = unlocalize_datetime(original_dt_utc, tz);
974
let mut result_timestamp_local = datetime_to_timestamp(original_dt_local);
975
result_timestamp_local += if d.negative { -t_weeks } else { t_weeks };
976
let result_dt_local = timestamp_to_datetime(result_timestamp_local);
977
datetime_to_timestamp(self.localize_result_rfc_5545(
978
original_dt_utc,
979
result_dt_local,
980
tz,
981
)?)
982
},
983
_ => {
984
if d.negative {
985
t - t_weeks
986
} else {
987
t + t_weeks
988
}
989
},
990
};
991
}
992
993
if d.days > 0 {
994
let t_days = nsecs_to_unit(NS_DAY) * self.days;
995
t = match tz {
996
#[cfg(feature = "timezones")]
997
// for UTC, use fastpath below (same as naive)
998
Some(tz) if tz != &chrono_tz::UTC => {
999
let original_dt_utc = timestamp_to_datetime(t);
1000
let original_dt_local = unlocalize_datetime(original_dt_utc, tz);
1001
t = datetime_to_timestamp(original_dt_local);
1002
t += if d.negative { -t_days } else { t_days };
1003
let result_dt_local = timestamp_to_datetime(t);
1004
let result_dt_utc =
1005
self.localize_result_rfc_5545(original_dt_utc, result_dt_local, tz)?;
1006
datetime_to_timestamp(result_dt_utc)
1007
},
1008
_ => {
1009
if d.negative {
1010
t - t_days
1011
} else {
1012
t + t_days
1013
}
1014
},
1015
};
1016
}
1017
1018
Ok(t)
1019
}
1020
1021
pub fn add_ns(&self, t: i64, tz: Option<&Tz>) -> PolarsResult<i64> {
1022
let d = self;
1023
let new_t = self.add_impl_month_week_or_day(
1024
t,
1025
tz,
1026
|nsecs| nsecs,
1027
timestamp_ns_to_datetime,
1028
datetime_to_timestamp_ns,
1029
);
1030
let nsecs = if d.negative { -d.nsecs } else { d.nsecs };
1031
Ok(new_t? + nsecs)
1032
}
1033
1034
pub fn add_us(&self, t: i64, tz: Option<&Tz>) -> PolarsResult<i64> {
1035
let d = self;
1036
let new_t = self.add_impl_month_week_or_day(
1037
t,
1038
tz,
1039
|nsecs| nsecs / 1000,
1040
timestamp_us_to_datetime,
1041
datetime_to_timestamp_us,
1042
);
1043
let nsecs = if d.negative { -d.nsecs } else { d.nsecs };
1044
Ok(new_t? + nsecs / 1_000)
1045
}
1046
1047
pub fn add_ms(&self, t: i64, tz: Option<&Tz>) -> PolarsResult<i64> {
1048
let d = self;
1049
let new_t = self.add_impl_month_week_or_day(
1050
t,
1051
tz,
1052
|nsecs| nsecs / 1_000_000,
1053
timestamp_ms_to_datetime,
1054
datetime_to_timestamp_ms,
1055
);
1056
let nsecs = if d.negative { -d.nsecs } else { d.nsecs };
1057
Ok(new_t? + nsecs / 1_000_000)
1058
}
1059
}
1060
1061
impl Mul<i64> for Duration {
1062
type Output = Self;
1063
1064
fn mul(mut self, mut rhs: i64) -> Self {
1065
if rhs < 0 {
1066
rhs = -rhs;
1067
self.negative = !self.negative
1068
}
1069
self.months *= rhs;
1070
self.weeks *= rhs;
1071
self.days *= rhs;
1072
self.nsecs *= rhs;
1073
self
1074
}
1075
}
1076
1077
fn new_datetime(
1078
year: i32,
1079
month: u32,
1080
days: u32,
1081
hour: u32,
1082
min: u32,
1083
sec: u32,
1084
nano: u32,
1085
) -> Option<NaiveDateTime> {
1086
let date = NaiveDate::from_ymd_opt(year, month, days)?;
1087
let time = NaiveTime::from_hms_nano_opt(hour, min, sec, nano)?;
1088
Some(NaiveDateTime::new(date, time))
1089
}
1090
1091
pub fn ensure_is_constant_duration(
1092
duration: Duration,
1093
time_zone: Option<&TimeZone>,
1094
variable_name: &str,
1095
) -> PolarsResult<()> {
1096
polars_ensure!(duration.is_constant_duration(time_zone),
1097
InvalidOperation: "expected `{}` to be a constant duration \
1098
(i.e. one independent of differing month durations or of daylight savings time), got {}.\n\
1099
\n\
1100
You may want to try:\n\
1101
- using `'730h'` instead of `'1mo'`\n\
1102
- using `'24h'` instead of `'1d'` if your series is time-zone-aware", variable_name, duration);
1103
Ok(())
1104
}
1105
1106
pub fn ensure_duration_matches_dtype(
1107
duration: Duration,
1108
dtype: &DataType,
1109
variable_name: &str,
1110
) -> PolarsResult<()> {
1111
match dtype {
1112
DataType::Int64 | DataType::UInt64 | DataType::Int32 | DataType::UInt32 => {
1113
polars_ensure!(duration.parsed_int || duration.is_zero(),
1114
InvalidOperation: "`{}` duration must be a parsed integer (i.e. use '2i', not '2d') when working with a numeric column", variable_name);
1115
},
1116
DataType::Datetime(_, _) | DataType::Date | DataType::Duration(_) | DataType::Time => {
1117
polars_ensure!(!duration.parsed_int,
1118
InvalidOperation: "`{}` duration may not be a parsed integer (i.e. use '2d', not '2i') when working with a temporal column", variable_name);
1119
},
1120
_ => {
1121
polars_bail!(InvalidOperation: "unsupported data type: {} for temporal/index column, expected UInt64, UInt32, Int64, Int32, Datetime, Date, Duration, or Time", dtype)
1122
},
1123
}
1124
Ok(())
1125
}
1126
1127
#[cfg(test)]
1128
mod test {
1129
use super::*;
1130
1131
#[test]
1132
fn test_parse() {
1133
let out = Duration::parse("1ns");
1134
assert_eq!(out.nsecs, 1);
1135
let out = Duration::parse("1ns1ms");
1136
assert_eq!(out.nsecs, NS_MILLISECOND + 1);
1137
let out = Duration::parse("123ns40ms");
1138
assert_eq!(out.nsecs, 40 * NS_MILLISECOND + 123);
1139
let out = Duration::parse("123ns40ms1w");
1140
assert_eq!(out.nsecs, 40 * NS_MILLISECOND + 123);
1141
assert_eq!(out.duration_ns(), 40 * NS_MILLISECOND + 123 + NS_WEEK);
1142
let out = Duration::parse("-123ns40ms1w");
1143
assert!(out.negative);
1144
let out = Duration::parse("5w");
1145
assert_eq!(out.weeks(), 5);
1146
}
1147
1148
#[test]
1149
fn test_parse_interval() {
1150
let d = Duration::try_parse_interval("3 DAYS").unwrap();
1151
assert_eq!(d.days(), 3);
1152
1153
let d = Duration::try_parse_interval("1 year, 2 months, 1 week").unwrap();
1154
assert_eq!(d.months(), 14);
1155
assert_eq!(d.weeks(), 1);
1156
1157
let d = Duration::try_parse_interval("100ms 100us").unwrap();
1158
assert_eq!(d.duration_us(), 100_100);
1159
}
1160
1161
#[test]
1162
fn test_add_ns() {
1163
let t = 1;
1164
let seven_days = Duration::parse("7d");
1165
let one_week = Duration::parse("1w");
1166
1167
// add_ns can only error if a time zone is passed, so it's
1168
// safe to unwrap here
1169
assert_eq!(
1170
seven_days.add_ns(t, None).unwrap(),
1171
one_week.add_ns(t, None).unwrap()
1172
);
1173
1174
let seven_days_negative = Duration::parse("-7d");
1175
let one_week_negative = Duration::parse("-1w");
1176
1177
// add_ns can only error if a time zone is passed, so it's
1178
// safe to unwrap here
1179
assert_eq!(
1180
seven_days_negative.add_ns(t, None).unwrap(),
1181
one_week_negative.add_ns(t, None).unwrap()
1182
);
1183
}
1184
1185
#[test]
1186
fn test_display() {
1187
let duration = Duration::parse("1h");
1188
let expected = "3600s";
1189
assert_eq!(format!("{duration}"), expected);
1190
let duration = Duration::parse("1h5ns");
1191
let expected = "3600000000005ns";
1192
assert_eq!(format!("{duration}"), expected);
1193
let duration = Duration::parse("1h5000ns");
1194
let expected = "3600000005us";
1195
assert_eq!(format!("{duration}"), expected);
1196
let duration = Duration::parse("3mo");
1197
let expected = "3mo";
1198
assert_eq!(format!("{duration}"), expected);
1199
let duration = Duration::parse_interval("4 weeks");
1200
let expected = "4w";
1201
assert_eq!(format!("{duration}"), expected);
1202
}
1203
}
1204
1205