polars_arrow/
temporal_conversions.rs

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