polars_arrow/
temporal_conversions.rs

1//! Conversion methods for dates and times.
2
3use chrono::format::{Parsed, StrftimeItems, parse};
4use chrono::{DateTime, Duration, FixedOffset, NaiveDate, NaiveDateTime, NaiveTime, TimeDelta};
5use polars_error::{PolarsResult, polars_err};
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#[inline]
27fn unix_epoch() -> NaiveDateTime {
28    DateTime::UNIX_EPOCH.naive_utc()
29}
30
31/// converts a `i32` representing a `date32` to [`NaiveDateTime`]
32#[inline]
33pub 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]
39pub 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]
46pub 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]
52pub 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]
58pub 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]
66pub 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]
72pub 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]
78pub 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]
84pub 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]
90pub 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]
96pub 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]
102pub 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]
114pub 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]
120pub 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]
132pub 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]
138pub 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]
149pub 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]
155pub 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]
161pub 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]
167pub 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]
174pub 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]
180pub 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]
187pub 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]
193pub 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]
200pub(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]
214pub 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".
225pub 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]
251pub 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.
280pub 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")))]
307pub 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}