Skip to main content

arrow_cast/cast/
temporal.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: See the NOTICE file distributed with this work for additional information regarding copyright ownership
3// SPDX-FileContributor: Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements
4
5//! Cast support for temporal arrays: timestamps, dates, durations,
6//! intervals, and timezones.
7
8use arrow_array::{cast::*, temporal_conversions::*, timezone::Tz, types::*, *};
9use arrow_buffer::IntervalMonthDayNano;
10use arrow_schema::{ArrowError, DataType, TimeUnit};
11use chrono::{NaiveTime, Offset};
12use std::sync::Arc;
13
14use num_traits::AsPrimitive;
15
16use crate::local_time::resolve_local_offset;
17
18use super::{CastOptions, time_unit_multiple, units_per_day};
19
20/// Cast the array from interval year month to month day nano
21pub(crate) fn cast_interval_year_month_to_interval_month_day_nano(
22    array: &dyn Array,
23    _cast_options: &CastOptions,
24) -> Result<ArrayRef, ArrowError> {
25    let array = array.as_primitive::<IntervalYearMonthType>();
26
27    Ok(Arc::new(array.unary::<_, IntervalMonthDayNanoType>(|v| {
28        let months = IntervalYearMonthType::to_months(v);
29        IntervalMonthDayNanoType::make_value(months, 0, 0)
30    })))
31}
32
33/// Cast the array from interval day time to month day nano
34pub(crate) fn cast_interval_day_time_to_interval_month_day_nano(
35    array: &dyn Array,
36    _cast_options: &CastOptions,
37) -> Result<ArrayRef, ArrowError> {
38    let array = array.as_primitive::<IntervalDayTimeType>();
39    let mul = 1_000_000;
40
41    Ok(Arc::new(array.unary::<_, IntervalMonthDayNanoType>(|v| {
42        let (days, ms) = IntervalDayTimeType::to_parts(v);
43        IntervalMonthDayNanoType::make_value(0, days, ms as i64 * mul)
44    })))
45}
46
47/// Cast the array from interval to duration
48pub(crate) fn cast_month_day_nano_to_duration<D: ArrowTemporalType<Native = i64>>(
49    array: &dyn Array,
50    cast_options: &CastOptions,
51) -> Result<ArrayRef, ArrowError> {
52    let array = array.as_primitive::<IntervalMonthDayNanoType>();
53    let scale = match D::DATA_TYPE {
54        DataType::Duration(TimeUnit::Second) => 1_000_000_000,
55        DataType::Duration(TimeUnit::Millisecond) => 1_000_000,
56        DataType::Duration(TimeUnit::Microsecond) => 1_000,
57        DataType::Duration(TimeUnit::Nanosecond) => 1,
58        _ => unreachable!(),
59    };
60
61    if cast_options.safe {
62        let iter = array.iter().map(|v| {
63            let v = v?;
64            (v.days == 0 && v.months == 0).then_some(v.nanoseconds / scale)
65        });
66        Ok(Arc::new(unsafe {
67            PrimitiveArray::<D>::from_trusted_len_iter(iter)
68        }))
69    } else {
70        let vec = array
71            .iter()
72            .map(|v| {
73                v.map(|v| match v.days == 0 && v.months == 0 {
74                    true => Ok((v.nanoseconds) / scale),
75                    _ => Err(ArrowError::ComputeError(
76                        "Cannot convert interval containing non-zero months or days to duration"
77                            .to_string(),
78                    )),
79                })
80                .transpose()
81            })
82            .collect::<Result<Vec<_>, _>>()?;
83        Ok(Arc::new(unsafe {
84            PrimitiveArray::<D>::from_trusted_len_iter(vec.iter())
85        }))
86    }
87}
88
89/// Cast the array from duration and interval
90pub(crate) fn cast_duration_to_interval<D: ArrowTemporalType<Native = i64>>(
91    array: &dyn Array,
92    cast_options: &CastOptions,
93) -> Result<ArrayRef, ArrowError> {
94    let array = array.as_primitive::<D>();
95
96    let scale = match array.data_type() {
97        DataType::Duration(TimeUnit::Second) => 1_000_000_000,
98        DataType::Duration(TimeUnit::Millisecond) => 1_000_000,
99        DataType::Duration(TimeUnit::Microsecond) => 1_000,
100        DataType::Duration(TimeUnit::Nanosecond) => 1,
101        _ => unreachable!(),
102    };
103
104    if cast_options.safe {
105        let iter = array.iter().map(|v| {
106            v?.checked_mul(scale)
107                .map(|v| IntervalMonthDayNano::new(0, 0, v))
108        });
109        Ok(Arc::new(unsafe {
110            PrimitiveArray::<IntervalMonthDayNanoType>::from_trusted_len_iter(iter)
111        }))
112    } else {
113        let vec = array
114            .iter()
115            .map(|v| {
116                v.map(|v| {
117                    if let Ok(v) = v.mul_checked(scale) {
118                        Ok(IntervalMonthDayNano::new(0, 0, v))
119                    } else {
120                        Err(ArrowError::ComputeError(format!(
121                            "Cannot cast to {:?}. Overflowing on {:?}",
122                            IntervalMonthDayNanoType::DATA_TYPE,
123                            v
124                        )))
125                    }
126                })
127                .transpose()
128            })
129            .collect::<Result<Vec<_>, _>>()?;
130        Ok(Arc::new(unsafe {
131            PrimitiveArray::<IntervalMonthDayNanoType>::from_trusted_len_iter(vec.iter())
132        }))
133    }
134}
135
136pub(crate) fn make_timestamp_array(
137    array: &PrimitiveArray<Int64Type>,
138    unit: TimeUnit,
139    tz: Option<Arc<str>>,
140) -> ArrayRef {
141    match unit {
142        TimeUnit::Second => Arc::new(
143            array
144                .reinterpret_cast::<TimestampSecondType>()
145                .with_timezone_opt(tz),
146        ),
147        TimeUnit::Millisecond => Arc::new(
148            array
149                .reinterpret_cast::<TimestampMillisecondType>()
150                .with_timezone_opt(tz),
151        ),
152        TimeUnit::Microsecond => Arc::new(
153            array
154                .reinterpret_cast::<TimestampMicrosecondType>()
155                .with_timezone_opt(tz),
156        ),
157        TimeUnit::Nanosecond => Arc::new(
158            array
159                .reinterpret_cast::<TimestampNanosecondType>()
160                .with_timezone_opt(tz),
161        ),
162    }
163}
164
165pub(crate) fn make_duration_array(array: &PrimitiveArray<Int64Type>, unit: TimeUnit) -> ArrayRef {
166    match unit {
167        TimeUnit::Second => Arc::new(array.reinterpret_cast::<DurationSecondType>()),
168        TimeUnit::Millisecond => Arc::new(array.reinterpret_cast::<DurationMillisecondType>()),
169        TimeUnit::Microsecond => Arc::new(array.reinterpret_cast::<DurationMicrosecondType>()),
170        TimeUnit::Nanosecond => Arc::new(array.reinterpret_cast::<DurationNanosecondType>()),
171    }
172}
173
174pub(crate) fn cast_timestamp_to_time<T: ArrowTimestampType>(
175    array: &dyn Array,
176    to_type: &DataType,
177    cast_options: &CastOptions,
178) -> Result<ArrayRef, ArrowError> {
179    let array = array.as_primitive::<T>();
180    match to_type {
181        DataType::Time32(TimeUnit::Second) => {
182            timestamp_to_time::<T, Time32SecondType>(array, cast_options)
183        }
184        DataType::Time32(TimeUnit::Millisecond) => {
185            timestamp_to_time::<T, Time32MillisecondType>(array, cast_options)
186        }
187        DataType::Time64(TimeUnit::Microsecond) => {
188            timestamp_to_time::<T, Time64MicrosecondType>(array, cast_options)
189        }
190        DataType::Time64(TimeUnit::Nanosecond) => {
191            timestamp_to_time::<T, Time64NanosecondType>(array, cast_options)
192        }
193        _ => Err(ArrowError::InvalidArgumentError(format!(
194            "Casting from {} to {to_type} not supported",
195            array.data_type()
196        ))),
197    }
198}
199
200trait TimeType: ArrowTemporalType {
201    /// Number of units in one second.
202    const UNIT_MULTIPLE: i64;
203
204    fn from_naive_time(time: NaiveTime) -> Self::Native;
205}
206
207impl TimeType for Time32SecondType {
208    const UNIT_MULTIPLE: i64 = 1;
209
210    fn from_naive_time(time: NaiveTime) -> Self::Native {
211        time_to_time32s(time)
212    }
213}
214
215impl TimeType for Time32MillisecondType {
216    const UNIT_MULTIPLE: i64 = MILLISECONDS;
217
218    fn from_naive_time(time: NaiveTime) -> Self::Native {
219        time_to_time32ms(time)
220    }
221}
222
223impl TimeType for Time64MicrosecondType {
224    const UNIT_MULTIPLE: i64 = MICROSECONDS;
225
226    fn from_naive_time(time: NaiveTime) -> Self::Native {
227        time_to_time64us(time)
228    }
229}
230
231impl TimeType for Time64NanosecondType {
232    const UNIT_MULTIPLE: i64 = NANOSECONDS;
233
234    fn from_naive_time(time: NaiveTime) -> Self::Native {
235        time_to_time64ns(time)
236    }
237}
238
239fn timestamp_to_time<T, O>(
240    array: &PrimitiveArray<T>,
241    cast_options: &CastOptions,
242) -> Result<ArrayRef, ArrowError>
243where
244    T: ArrowTimestampType,
245    O: TimeType,
246    i64: AsPrimitive<O::Native>,
247{
248    // A time within one day fits its Time32 or Time64 representation.
249    let array = match array.timezone() {
250        Some(tz) => {
251            let tz: Tz = tz.parse()?;
252            let time = |v: i64| {
253                as_datetime_with_timezone::<T>(v, tz).map(|d| O::from_naive_time(d.time()))
254            };
255            if cast_options.safe {
256                array.unary_opt::<_, O>(time)
257            } else {
258                array.try_unary::<_, O, _>(|v| {
259                    time(v).ok_or_else(|| {
260                        ArrowError::CastError(format!(
261                            "Failed to create naive time with {} {}",
262                            std::any::type_name::<T>(),
263                            v
264                        ))
265                    })
266                })?
267            }
268        }
269        None => array.unary::<_, O>(|v| {
270            // The remainder within a day is the time of day; `rem_euclid` keeps it
271            // nonnegative for timestamps before the epoch. The units are constants,
272            // so the branch and the divisions fold at compile time.
273            let from = time_unit_multiple(&T::UNIT);
274            let time = v.rem_euclid(SECONDS_IN_DAY * from);
275            let time = if from >= O::UNIT_MULTIPLE {
276                time / (from / O::UNIT_MULTIPLE)
277            } else {
278                time * (O::UNIT_MULTIPLE / from)
279            };
280            time.as_()
281        }),
282    };
283    Ok(Arc::new(array))
284}
285
286pub(crate) fn timestamp_to_date32<T: ArrowTimestampType>(
287    array: &PrimitiveArray<T>,
288    cast_options: &CastOptions,
289) -> Result<ArrayRef, ArrowError> {
290    let err = |x: i64| {
291        ArrowError::CastError(format!(
292            "Cannot convert {} {x} to Date32",
293            std::any::type_name::<T>()
294        ))
295    };
296
297    let array: Date32Array = match array.timezone() {
298        Some(tz) => {
299            let tz: Tz = tz.parse()?;
300            let date = |x: i64| {
301                as_datetime_with_timezone::<T>(x, tz)
302                    .map(|d| Date32Type::from_naive_date(d.date_naive()))
303            };
304            if cast_options.safe {
305                array.unary_opt(date)
306            } else {
307                array.try_unary(|x| date(x).ok_or_else(|| err(x)))?
308            }
309        }
310        None => {
311            // Date32 stores days since the epoch. Round down so that a timestamp
312            // just before the epoch belongs to the preceding day.
313            let days = |x: i64| x.div_euclid(units_per_day::<T>());
314            let all_in_range = match T::UNIT {
315                // Every microsecond or nanosecond timestamp lies within the Date32 range.
316                TimeUnit::Microsecond | TimeUnit::Nanosecond => true,
317                // A branch-free scan lets the common case skip the per-value check.
318                _ => {
319                    let day = units_per_day::<T>();
320                    let (lo, hi) = (i32::MIN as i64 * day, (i32::MAX as i64 + 1) * day);
321                    array
322                        .values()
323                        .iter()
324                        .fold(true, |ok, &x| ok & (lo <= x) & (x < hi))
325                }
326            };
327            if all_in_range {
328                array.unary(|x| days(x) as i32)
329            } else if cast_options.safe {
330                array.unary_opt(|x| i32::try_from(days(x)).ok())
331            } else {
332                array.try_unary(|x| i32::try_from(days(x)).map_err(|_| err(x)))?
333            }
334        }
335    };
336    Ok(Arc::new(array))
337}
338
339/// Casts a timestamp array to `Date64`.
340///
341/// `Date64` values must be whole days, so this truncates to the start of the
342/// day the same way [`timestamp_to_date32`] does: in the array's timezone when it
343/// has one, otherwise rounding down so that a timestamp just before the epoch
344/// belongs to the preceding day.
345pub(crate) fn timestamp_to_date64<T: ArrowTimestampType>(
346    array: &PrimitiveArray<T>,
347    cast_options: &CastOptions,
348) -> Result<ArrayRef, ArrowError> {
349    let err = |x: i64| {
350        ArrowError::CastError(format!(
351            "Cannot convert {} {x} to Date64",
352            std::any::type_name::<T>()
353        ))
354    };
355
356    let array: Date64Array = match array.timezone() {
357        Some(tz) => {
358            let tz: Tz = tz.parse()?;
359            // Chrono's date range is small enough that the product cannot overflow.
360            let date = |x: i64| {
361                as_datetime_with_timezone::<T>(x, tz).map(|d| {
362                    Date32Type::from_naive_date(d.date_naive()) as i64 * MILLISECONDS_IN_DAY
363                })
364            };
365            if cast_options.safe {
366                array.unary_opt(date)
367            } else {
368                array.try_unary(|x| date(x).ok_or_else(|| err(x)))?
369            }
370        }
371        None => {
372            // Rescaling whole days to milliseconds can overflow for second and
373            // millisecond timestamps near the ends of the i64 range.
374            let date = |x: i64| {
375                x.div_euclid(units_per_day::<T>())
376                    .checked_mul(MILLISECONDS_IN_DAY)
377            };
378            if cast_options.safe {
379                array.unary_opt(date)
380            } else {
381                array.try_unary(|x| date(x).ok_or_else(|| err(x)))?
382            }
383        }
384    };
385    Ok(Arc::new(array))
386}
387
388pub(crate) fn adjust_timestamp_to_timezone<T: ArrowTimestampType>(
389    array: PrimitiveArray<Int64Type>,
390    to_tz: &Tz,
391    cast_options: &CastOptions,
392) -> Result<PrimitiveArray<Int64Type>, ArrowError> {
393    let adjust = |o| {
394        let local = as_datetime::<T>(o)?;
395        let offset = resolve_local_offset(to_tz, &local)?.fix();
396        T::from_naive_datetime(local - offset, None)
397    };
398    let adjusted = if cast_options.safe {
399        array.unary_opt::<_, Int64Type>(adjust)
400    } else {
401        array.try_unary::<_, Int64Type, _>(|o| {
402            adjust(o).ok_or_else(|| {
403                ArrowError::CastError("Cannot cast timezone to different timezone".to_string())
404            })
405        })?
406    };
407    Ok(adjusted)
408}