polars_arrow/compute/
temporal.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18//! Defines temporal kernels for time and date related functions.
19
20use chrono::{Datelike, Timelike};
21use polars_error::PolarsResult;
22
23use super::arity::unary;
24use crate::array::*;
25use crate::datatypes::*;
26use crate::temporal_conversions::*;
27use crate::types::NativeType;
28
29// Create and implement a trait that converts chrono's `Weekday`
30// type into `i8`
31trait Int8Weekday: Datelike {
32    fn i8_weekday(&self) -> i8 {
33        self.weekday().number_from_monday().try_into().unwrap()
34    }
35}
36
37impl Int8Weekday for chrono::NaiveDateTime {}
38impl<T: chrono::TimeZone> Int8Weekday for chrono::DateTime<T> {}
39
40// Create and implement a trait that converts chrono's `IsoWeek`
41// type into `i8`
42trait Int8IsoWeek: Datelike {
43    fn i8_iso_week(&self) -> i8 {
44        self.iso_week().week().try_into().unwrap()
45    }
46}
47
48impl Int8IsoWeek for chrono::NaiveDateTime {}
49impl<T: chrono::TimeZone> Int8IsoWeek for chrono::DateTime<T> {}
50
51// Macro to avoid repetition in functions, that apply
52// `chrono::Datelike` methods on Arrays
53macro_rules! date_like {
54    ($extract:ident, $array:ident, $dtype:path) => {
55        match $array.dtype().to_logical_type() {
56            ArrowDataType::Date32 | ArrowDataType::Date64 | ArrowDataType::Timestamp(_, None) => {
57                date_variants($array, $dtype, |x| x.$extract().try_into().unwrap())
58            },
59            ArrowDataType::Timestamp(time_unit, Some(timezone_str)) => {
60                let array = $array.as_any().downcast_ref().unwrap();
61
62                if let Ok(timezone) = parse_offset(timezone_str.as_str()) {
63                    Ok(extract_impl(array, *time_unit, timezone, |x| {
64                        x.$extract().try_into().unwrap()
65                    }))
66                } else {
67                    chrono_tz(array, *time_unit, timezone_str.as_str(), |x| {
68                        x.$extract().try_into().unwrap()
69                    })
70                }
71            },
72            _ => unimplemented!(),
73        }
74    };
75}
76
77/// Extracts the years of a temporal array as [`PrimitiveArray<i32>`].
78pub fn year(array: &dyn Array) -> PolarsResult<PrimitiveArray<i32>> {
79    date_like!(year, array, ArrowDataType::Int32)
80}
81
82/// Extracts the months of a temporal array as [`PrimitiveArray<i8>`].
83///
84/// Value ranges from 1 to 12.
85pub fn month(array: &dyn Array) -> PolarsResult<PrimitiveArray<i8>> {
86    date_like!(month, array, ArrowDataType::Int8)
87}
88
89/// Extracts the days of a temporal array as [`PrimitiveArray<i8>`].
90///
91/// Value ranges from 1 to 32 (Last day depends on month).
92pub fn day(array: &dyn Array) -> PolarsResult<PrimitiveArray<i8>> {
93    date_like!(day, array, ArrowDataType::Int8)
94}
95
96/// Extracts weekday of a temporal array as [`PrimitiveArray<i8>`].
97///
98/// Monday is 1, Tuesday is 2, ..., Sunday is 7.
99pub fn weekday(array: &dyn Array) -> PolarsResult<PrimitiveArray<i8>> {
100    date_like!(i8_weekday, array, ArrowDataType::Int8)
101}
102
103/// Extracts ISO week of a temporal array as [`PrimitiveArray<i8>`].
104///
105/// Value ranges from 1 to 53 (Last week depends on the year).
106pub fn iso_week(array: &dyn Array) -> PolarsResult<PrimitiveArray<i8>> {
107    date_like!(i8_iso_week, array, ArrowDataType::Int8)
108}
109
110// Macro to avoid repetition in functions, that apply
111// `chrono::Timelike` methods on Arrays
112macro_rules! time_like {
113    ($extract:ident, $array:ident, $dtype:path) => {
114        match $array.dtype().to_logical_type() {
115            ArrowDataType::Date32 | ArrowDataType::Date64 | ArrowDataType::Timestamp(_, None) => {
116                date_variants($array, $dtype, |x| x.$extract().try_into().unwrap())
117            },
118            ArrowDataType::Time32(_) | ArrowDataType::Time64(_) => {
119                time_variants($array, ArrowDataType::UInt32, |x| {
120                    x.$extract().try_into().unwrap()
121                })
122            },
123            ArrowDataType::Timestamp(time_unit, Some(timezone_str)) => {
124                let array = $array.as_any().downcast_ref().unwrap();
125
126                if let Ok(timezone) = parse_offset(timezone_str.as_str()) {
127                    Ok(extract_impl(array, *time_unit, timezone, |x| {
128                        x.$extract().try_into().unwrap()
129                    }))
130                } else {
131                    chrono_tz(array, *time_unit, timezone_str.as_str(), |x| {
132                        x.$extract().try_into().unwrap()
133                    })
134                }
135            },
136            _ => unimplemented!(),
137        }
138    };
139}
140
141/// Extracts the hours of a temporal array as [`PrimitiveArray<i8>`].
142/// Value ranges from 0 to 23.
143/// Use [`can_hour`] to check if this operation is supported for the target [`ArrowDataType`].
144pub fn hour(array: &dyn Array) -> PolarsResult<PrimitiveArray<i8>> {
145    time_like!(hour, array, ArrowDataType::Int8)
146}
147
148/// Extracts the minutes of a temporal array as [`PrimitiveArray<i8>`].
149/// Value ranges from 0 to 59.
150/// Use [`can_minute`] to check if this operation is supported for the target [`ArrowDataType`].
151pub fn minute(array: &dyn Array) -> PolarsResult<PrimitiveArray<i8>> {
152    time_like!(minute, array, ArrowDataType::Int8)
153}
154
155/// Extracts the seconds of a temporal array as [`PrimitiveArray<i8>`].
156/// Value ranges from 0 to 59.
157/// Use [`can_second`] to check if this operation is supported for the target [`ArrowDataType`].
158pub fn second(array: &dyn Array) -> PolarsResult<PrimitiveArray<i8>> {
159    time_like!(second, array, ArrowDataType::Int8)
160}
161
162/// Extracts the nanoseconds of a temporal array as [`PrimitiveArray<i32>`].
163///
164/// Value ranges from 0 to 1_999_999_999.
165/// The range from 1_000_000_000 to 1_999_999_999 represents the leap second.
166/// Use [`can_nanosecond`] to check if this operation is supported for the target [`ArrowDataType`].
167pub fn nanosecond(array: &dyn Array) -> PolarsResult<PrimitiveArray<i32>> {
168    time_like!(nanosecond, array, ArrowDataType::Int32)
169}
170
171fn date_variants<F, O>(
172    array: &dyn Array,
173    dtype: ArrowDataType,
174    op: F,
175) -> PolarsResult<PrimitiveArray<O>>
176where
177    O: NativeType,
178    F: Fn(chrono::NaiveDateTime) -> O,
179{
180    match array.dtype().to_logical_type() {
181        ArrowDataType::Date32 => {
182            let array = array
183                .as_any()
184                .downcast_ref::<PrimitiveArray<i32>>()
185                .unwrap();
186            Ok(unary(array, |x| op(date32_to_datetime(x)), dtype))
187        },
188        ArrowDataType::Date64 => {
189            let array = array
190                .as_any()
191                .downcast_ref::<PrimitiveArray<i64>>()
192                .unwrap();
193            Ok(unary(array, |x| op(date64_to_datetime(x)), dtype))
194        },
195        ArrowDataType::Timestamp(time_unit, None) => {
196            let array = array
197                .as_any()
198                .downcast_ref::<PrimitiveArray<i64>>()
199                .unwrap();
200            let func = match time_unit {
201                TimeUnit::Second => timestamp_s_to_datetime,
202                TimeUnit::Millisecond => timestamp_ms_to_datetime,
203                TimeUnit::Microsecond => timestamp_us_to_datetime,
204                TimeUnit::Nanosecond => timestamp_ns_to_datetime,
205            };
206            Ok(PrimitiveArray::<O>::from_trusted_len_iter(
207                array.iter().map(|v| v.map(|x| op(func(*x)))),
208            ))
209        },
210        _ => unreachable!(),
211    }
212}
213
214fn time_variants<F, O>(
215    array: &dyn Array,
216    dtype: ArrowDataType,
217    op: F,
218) -> PolarsResult<PrimitiveArray<O>>
219where
220    O: NativeType,
221    F: Fn(chrono::NaiveTime) -> O,
222{
223    match array.dtype().to_logical_type() {
224        ArrowDataType::Time32(TimeUnit::Second) => {
225            let array = array
226                .as_any()
227                .downcast_ref::<PrimitiveArray<i32>>()
228                .unwrap();
229            Ok(unary(array, |x| op(time32s_to_time(x)), dtype))
230        },
231        ArrowDataType::Time32(TimeUnit::Millisecond) => {
232            let array = array
233                .as_any()
234                .downcast_ref::<PrimitiveArray<i32>>()
235                .unwrap();
236            Ok(unary(array, |x| op(time32ms_to_time(x)), dtype))
237        },
238        ArrowDataType::Time64(TimeUnit::Microsecond) => {
239            let array = array
240                .as_any()
241                .downcast_ref::<PrimitiveArray<i64>>()
242                .unwrap();
243            Ok(unary(array, |x| op(time64us_to_time(x)), dtype))
244        },
245        ArrowDataType::Time64(TimeUnit::Nanosecond) => {
246            let array = array
247                .as_any()
248                .downcast_ref::<PrimitiveArray<i64>>()
249                .unwrap();
250            Ok(unary(array, |x| op(time64ns_to_time(x)), dtype))
251        },
252        _ => unreachable!(),
253    }
254}
255
256#[cfg(feature = "chrono-tz")]
257fn chrono_tz<F, O>(
258    array: &PrimitiveArray<i64>,
259    time_unit: TimeUnit,
260    timezone_str: &str,
261    op: F,
262) -> PolarsResult<PrimitiveArray<O>>
263where
264    O: NativeType,
265    F: Fn(chrono::DateTime<chrono_tz::Tz>) -> O,
266{
267    let timezone = parse_offset_tz(timezone_str)?;
268    Ok(extract_impl(array, time_unit, timezone, op))
269}
270
271#[cfg(not(feature = "chrono-tz"))]
272fn chrono_tz<F, O>(
273    _: &PrimitiveArray<i64>,
274    _: TimeUnit,
275    timezone_str: &str,
276    _: F,
277) -> PolarsResult<PrimitiveArray<O>>
278where
279    O: NativeType,
280    F: Fn(chrono::DateTime<chrono::FixedOffset>) -> O,
281{
282    panic!(
283        "timezone \"{}\" cannot be parsed (feature chrono-tz is not active)",
284        timezone_str
285    )
286}
287
288fn extract_impl<T, A, F>(
289    array: &PrimitiveArray<i64>,
290    time_unit: TimeUnit,
291    timezone: T,
292    extract: F,
293) -> PrimitiveArray<A>
294where
295    T: chrono::TimeZone,
296    A: NativeType,
297    F: Fn(chrono::DateTime<T>) -> A,
298{
299    match time_unit {
300        TimeUnit::Second => {
301            let op = |x| {
302                let datetime = timestamp_s_to_datetime(x);
303                let offset = timezone.offset_from_utc_datetime(&datetime);
304                extract(chrono::DateTime::<T>::from_naive_utc_and_offset(
305                    datetime, offset,
306                ))
307            };
308            unary(array, op, A::PRIMITIVE.into())
309        },
310        TimeUnit::Millisecond => {
311            let op = |x| {
312                let datetime = timestamp_ms_to_datetime(x);
313                let offset = timezone.offset_from_utc_datetime(&datetime);
314                extract(chrono::DateTime::<T>::from_naive_utc_and_offset(
315                    datetime, offset,
316                ))
317            };
318            unary(array, op, A::PRIMITIVE.into())
319        },
320        TimeUnit::Microsecond => {
321            let op = |x| {
322                let datetime = timestamp_us_to_datetime(x);
323                let offset = timezone.offset_from_utc_datetime(&datetime);
324                extract(chrono::DateTime::<T>::from_naive_utc_and_offset(
325                    datetime, offset,
326                ))
327            };
328            unary(array, op, A::PRIMITIVE.into())
329        },
330        TimeUnit::Nanosecond => {
331            let op = |x| {
332                let datetime = timestamp_ns_to_datetime(x);
333                let offset = timezone.offset_from_utc_datetime(&datetime);
334                extract(chrono::DateTime::<T>::from_naive_utc_and_offset(
335                    datetime, offset,
336                ))
337            };
338            unary(array, op, A::PRIMITIVE.into())
339        },
340    }
341}