Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
pola-rs
GitHub Repository: pola-rs/polars
Path: blob/main/crates/polars-arrow/src/temporal_conversions.rs
6939 views
1
//! Conversion methods for dates and times.
2
3
use chrono::format::{Parsed, StrftimeItems, parse};
4
use chrono::{DateTime, Duration, FixedOffset, NaiveDate, NaiveDateTime, NaiveTime, TimeDelta};
5
use polars_error::{PolarsResult, polars_err};
6
7
use crate::datatypes::TimeUnit;
8
9
/// Number of seconds in a day
10
pub const SECONDS_IN_DAY: i64 = 86_400;
11
/// Number of milliseconds in a second
12
pub const MILLISECONDS: i64 = 1_000;
13
/// Number of microseconds in a second
14
pub const MICROSECONDS: i64 = 1_000_000;
15
/// Number of nanoseconds in a second
16
pub const NANOSECONDS: i64 = 1_000_000_000;
17
/// Number of milliseconds in a day
18
pub const MILLISECONDS_IN_DAY: i64 = SECONDS_IN_DAY * MILLISECONDS;
19
/// Number of microseconds in a day
20
pub const MICROSECONDS_IN_DAY: i64 = SECONDS_IN_DAY * MICROSECONDS;
21
/// Number of nanoseconds in a day
22
pub const NANOSECONDS_IN_DAY: i64 = SECONDS_IN_DAY * NANOSECONDS;
23
/// Number of days between 0001-01-01 and 1970-01-01
24
pub const EPOCH_DAYS_FROM_CE: i32 = 719_163;
25
26
#[inline]
27
fn unix_epoch() -> NaiveDateTime {
28
DateTime::UNIX_EPOCH.naive_utc()
29
}
30
31
/// converts a `i32` representing a `date32` to [`NaiveDateTime`]
32
#[inline]
33
pub fn date32_to_datetime(v: i32) -> NaiveDateTime {
34
date32_to_datetime_opt(v).expect("invalid or out-of-range datetime")
35
}
36
37
/// converts a `i32` representing a `date32` to [`NaiveDateTime`]
38
#[inline]
39
pub fn date32_to_datetime_opt(v: i32) -> Option<NaiveDateTime> {
40
let delta = TimeDelta::try_days(v.into())?;
41
unix_epoch().checked_add_signed(delta)
42
}
43
44
/// converts a `i32` representing a `date32` to [`NaiveDate`]
45
#[inline]
46
pub fn date32_to_date(days: i32) -> NaiveDate {
47
date32_to_date_opt(days).expect("out-of-range date")
48
}
49
50
/// converts a `i32` representing a `date32` to [`NaiveDate`]
51
#[inline]
52
pub fn date32_to_date_opt(days: i32) -> Option<NaiveDate> {
53
NaiveDate::from_num_days_from_ce_opt(EPOCH_DAYS_FROM_CE + days)
54
}
55
56
/// converts a `i64` representing a `date64` to [`NaiveDateTime`]
57
#[inline]
58
pub fn date64_to_datetime(v: i64) -> NaiveDateTime {
59
TimeDelta::try_milliseconds(v)
60
.and_then(|delta| unix_epoch().checked_add_signed(delta))
61
.expect("invalid or out-of-range datetime")
62
}
63
64
/// converts a `i64` representing a `date64` to [`NaiveDate`]
65
#[inline]
66
pub fn date64_to_date(milliseconds: i64) -> NaiveDate {
67
date64_to_datetime(milliseconds).date()
68
}
69
70
/// converts a `i32` representing a `time32(s)` to [`NaiveTime`]
71
#[inline]
72
pub fn time32s_to_time(v: i32) -> NaiveTime {
73
NaiveTime::from_num_seconds_from_midnight_opt(v as u32, 0).expect("invalid time")
74
}
75
76
/// converts a `i64` representing a `duration(s)` to [`Duration`]
77
#[inline]
78
pub fn duration_s_to_duration(v: i64) -> Duration {
79
Duration::try_seconds(v).expect("out-of-range duration")
80
}
81
82
/// converts a `i64` representing a `duration(ms)` to [`Duration`]
83
#[inline]
84
pub fn duration_ms_to_duration(v: i64) -> Duration {
85
Duration::try_milliseconds(v).expect("out-of-range in duration conversion")
86
}
87
88
/// converts a `i64` representing a `duration(us)` to [`Duration`]
89
#[inline]
90
pub fn duration_us_to_duration(v: i64) -> Duration {
91
Duration::microseconds(v)
92
}
93
94
/// converts a `i64` representing a `duration(ns)` to [`Duration`]
95
#[inline]
96
pub fn duration_ns_to_duration(v: i64) -> Duration {
97
Duration::nanoseconds(v)
98
}
99
100
/// converts a `i32` representing a `time32(ms)` to [`NaiveTime`]
101
#[inline]
102
pub fn time32ms_to_time(v: i32) -> NaiveTime {
103
let v = v as i64;
104
let seconds = v / MILLISECONDS;
105
106
let milli_to_nano = 1_000_000;
107
let nano = (v - seconds * MILLISECONDS) * milli_to_nano;
108
NaiveTime::from_num_seconds_from_midnight_opt(seconds as u32, nano as u32)
109
.expect("invalid time")
110
}
111
112
/// converts a `i64` representing a `time64(us)` to [`NaiveTime`]
113
#[inline]
114
pub fn time64us_to_time(v: i64) -> NaiveTime {
115
time64us_to_time_opt(v).expect("invalid time")
116
}
117
118
/// converts a `i64` representing a `time64(us)` to [`NaiveTime`]
119
#[inline]
120
pub fn time64us_to_time_opt(v: i64) -> Option<NaiveTime> {
121
NaiveTime::from_num_seconds_from_midnight_opt(
122
// extract seconds from microseconds
123
(v / MICROSECONDS) as u32,
124
// discard extracted seconds and convert microseconds to
125
// nanoseconds
126
(v % MICROSECONDS * MILLISECONDS) as u32,
127
)
128
}
129
130
/// converts a `i64` representing a `time64(ns)` to [`NaiveTime`]
131
#[inline]
132
pub fn time64ns_to_time(v: i64) -> NaiveTime {
133
time64ns_to_time_opt(v).expect("invalid time")
134
}
135
136
/// converts a `i64` representing a `time64(ns)` to [`NaiveTime`]
137
#[inline]
138
pub fn time64ns_to_time_opt(v: i64) -> Option<NaiveTime> {
139
NaiveTime::from_num_seconds_from_midnight_opt(
140
// extract seconds from nanoseconds
141
(v / NANOSECONDS) as u32,
142
// discard extracted seconds
143
(v % NANOSECONDS) as u32,
144
)
145
}
146
147
/// converts a `i64` representing a `timestamp(s)` to [`NaiveDateTime`]
148
#[inline]
149
pub fn timestamp_s_to_datetime(seconds: i64) -> NaiveDateTime {
150
timestamp_s_to_datetime_opt(seconds).expect("invalid or out-of-range datetime")
151
}
152
153
/// converts a `i64` representing a `timestamp(s)` to [`NaiveDateTime`]
154
#[inline]
155
pub fn timestamp_s_to_datetime_opt(seconds: i64) -> Option<NaiveDateTime> {
156
Some(DateTime::from_timestamp(seconds, 0)?.naive_utc())
157
}
158
159
/// converts a `i64` representing a `timestamp(ms)` to [`NaiveDateTime`]
160
#[inline]
161
pub fn timestamp_ms_to_datetime(v: i64) -> NaiveDateTime {
162
timestamp_ms_to_datetime_opt(v).expect("invalid or out-of-range datetime")
163
}
164
165
/// converts a `i64` representing a `timestamp(ms)` to [`NaiveDateTime`]
166
#[inline]
167
pub fn timestamp_ms_to_datetime_opt(v: i64) -> Option<NaiveDateTime> {
168
let delta = TimeDelta::try_milliseconds(v)?;
169
unix_epoch().checked_add_signed(delta)
170
}
171
172
/// converts a `i64` representing a `timestamp(us)` to [`NaiveDateTime`]
173
#[inline]
174
pub fn timestamp_us_to_datetime(v: i64) -> NaiveDateTime {
175
timestamp_us_to_datetime_opt(v).expect("invalid or out-of-range datetime")
176
}
177
178
/// converts a `i64` representing a `timestamp(us)` to [`NaiveDateTime`]
179
#[inline]
180
pub fn timestamp_us_to_datetime_opt(v: i64) -> Option<NaiveDateTime> {
181
let delta = TimeDelta::microseconds(v);
182
unix_epoch().checked_add_signed(delta)
183
}
184
185
/// converts a `i64` representing a `timestamp(ns)` to [`NaiveDateTime`]
186
#[inline]
187
pub fn timestamp_ns_to_datetime(v: i64) -> NaiveDateTime {
188
timestamp_ns_to_datetime_opt(v).expect("invalid or out-of-range datetime")
189
}
190
191
/// converts a `i64` representing a `timestamp(ns)` to [`NaiveDateTime`]
192
#[inline]
193
pub fn timestamp_ns_to_datetime_opt(v: i64) -> Option<NaiveDateTime> {
194
let delta = TimeDelta::nanoseconds(v);
195
unix_epoch().checked_add_signed(delta)
196
}
197
198
/// Converts a timestamp in `time_unit` and `timezone` into [`chrono::DateTime`].
199
#[inline]
200
pub(crate) fn timestamp_to_naive_datetime(
201
timestamp: i64,
202
time_unit: TimeUnit,
203
) -> chrono::NaiveDateTime {
204
match time_unit {
205
TimeUnit::Second => timestamp_s_to_datetime(timestamp),
206
TimeUnit::Millisecond => timestamp_ms_to_datetime(timestamp),
207
TimeUnit::Microsecond => timestamp_us_to_datetime(timestamp),
208
TimeUnit::Nanosecond => timestamp_ns_to_datetime(timestamp),
209
}
210
}
211
212
/// Converts a timestamp in `time_unit` and `timezone` into [`chrono::DateTime`].
213
#[inline]
214
pub fn timestamp_to_datetime<T: chrono::TimeZone>(
215
timestamp: i64,
216
time_unit: TimeUnit,
217
timezone: &T,
218
) -> chrono::DateTime<T> {
219
timezone.from_utc_datetime(&timestamp_to_naive_datetime(timestamp, time_unit))
220
}
221
222
/// Calculates the scale factor between two TimeUnits. The function returns the
223
/// scale that should multiply the TimeUnit "b" to have the same time scale as
224
/// the TimeUnit "a".
225
pub fn timeunit_scale(a: TimeUnit, b: TimeUnit) -> f64 {
226
match (a, b) {
227
(TimeUnit::Second, TimeUnit::Second) => 1.0,
228
(TimeUnit::Second, TimeUnit::Millisecond) => 0.001,
229
(TimeUnit::Second, TimeUnit::Microsecond) => 0.000_001,
230
(TimeUnit::Second, TimeUnit::Nanosecond) => 0.000_000_001,
231
(TimeUnit::Millisecond, TimeUnit::Second) => 1_000.0,
232
(TimeUnit::Millisecond, TimeUnit::Millisecond) => 1.0,
233
(TimeUnit::Millisecond, TimeUnit::Microsecond) => 0.001,
234
(TimeUnit::Millisecond, TimeUnit::Nanosecond) => 0.000_001,
235
(TimeUnit::Microsecond, TimeUnit::Second) => 1_000_000.0,
236
(TimeUnit::Microsecond, TimeUnit::Millisecond) => 1_000.0,
237
(TimeUnit::Microsecond, TimeUnit::Microsecond) => 1.0,
238
(TimeUnit::Microsecond, TimeUnit::Nanosecond) => 0.001,
239
(TimeUnit::Nanosecond, TimeUnit::Second) => 1_000_000_000.0,
240
(TimeUnit::Nanosecond, TimeUnit::Millisecond) => 1_000_000.0,
241
(TimeUnit::Nanosecond, TimeUnit::Microsecond) => 1_000.0,
242
(TimeUnit::Nanosecond, TimeUnit::Nanosecond) => 1.0,
243
}
244
}
245
246
/// Parses `value` to `Option<i64>` consistent with the Arrow's definition of timestamp with timezone.
247
///
248
/// `tz` must be built from `timezone` (either via [`parse_offset`] or `chrono-tz`).
249
/// Returns in scale `tz` of `TimeUnit`.
250
#[inline]
251
pub fn utf8_to_timestamp_scalar<T: chrono::TimeZone>(
252
value: &str,
253
fmt: &str,
254
tz: &T,
255
tu: &TimeUnit,
256
) -> Option<i64> {
257
let mut parsed = Parsed::new();
258
let fmt = StrftimeItems::new(fmt);
259
let r = parse(&mut parsed, value, fmt).ok();
260
if r.is_some() {
261
parsed
262
.to_datetime()
263
.map(|x| x.naive_utc())
264
.map(|x| tz.from_utc_datetime(&x))
265
.map(|x| match tu {
266
TimeUnit::Second => x.timestamp(),
267
TimeUnit::Millisecond => x.timestamp_millis(),
268
TimeUnit::Microsecond => x.timestamp_micros(),
269
TimeUnit::Nanosecond => x.timestamp_nanos_opt().unwrap(),
270
})
271
.ok()
272
} else {
273
None
274
}
275
}
276
277
/// Parses an offset of the form `"+WX:YZ"` or `"UTC"` into [`FixedOffset`].
278
/// # Errors
279
/// If the offset is not in any of the allowed forms.
280
pub fn parse_offset(offset: &str) -> PolarsResult<FixedOffset> {
281
if offset == "UTC" {
282
return Ok(FixedOffset::east_opt(0).expect("FixedOffset::east out of bounds"));
283
}
284
let error = "timezone offset must be of the form [-]00:00";
285
286
let mut a = offset.split(':');
287
let first: &str = a
288
.next()
289
.ok_or_else(|| polars_err!(InvalidOperation: error))?;
290
let last = a
291
.next()
292
.ok_or_else(|| polars_err!(InvalidOperation: error))?;
293
let hours: i32 = first
294
.parse()
295
.map_err(|_| polars_err!(InvalidOperation: error))?;
296
let minutes: i32 = last
297
.parse()
298
.map_err(|_| polars_err!(InvalidOperation: error))?;
299
300
Ok(FixedOffset::east_opt(hours * 60 * 60 + minutes * 60)
301
.expect("FixedOffset::east out of bounds"))
302
}
303
304
/// Parses `value` to a [`chrono_tz::Tz`] with the Arrow's definition of timestamp with a timezone.
305
#[cfg(feature = "chrono-tz")]
306
#[cfg_attr(docsrs, doc(cfg(feature = "chrono-tz")))]
307
pub fn parse_offset_tz(timezone: &str) -> PolarsResult<chrono_tz::Tz> {
308
timezone
309
.parse::<chrono_tz::Tz>()
310
.map_err(|_| polars_err!(InvalidOperation: "timezone \"{timezone}\" cannot be parsed"))
311
}
312
313