Skip to main content

datafusion_functions/datetime/
date_trunc.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
18use std::fmt;
19use std::num::NonZeroI64;
20use std::ops::{Add, Sub};
21use std::str::FromStr;
22use std::sync::Arc;
23
24use arrow::array::temporal_conversions::{
25    MICROSECONDS, MILLISECONDS, NANOSECONDS, as_datetime_with_timezone,
26};
27use arrow::array::timezone::Tz;
28use arrow::array::types::{
29    ArrowTimestampType, Time32MillisecondType, Time32SecondType, Time64MicrosecondType,
30    Time64NanosecondType, TimestampMicrosecondType, TimestampMillisecondType,
31    TimestampNanosecondType, TimestampSecondType,
32};
33use arrow::array::{Array, ArrayRef, PrimitiveArray};
34use arrow::datatypes::DataType::{self, Time32, Time64, Timestamp};
35use arrow::datatypes::TimeUnit::{self, Microsecond, Millisecond, Nanosecond, Second};
36use arrow::datatypes::{Field, FieldRef};
37use datafusion_common::cast::as_primitive_array;
38use datafusion_common::types::{NativeType, logical_date, logical_string};
39use datafusion_common::{
40    DataFusionError, Result, ScalarValue, exec_datafusion_err, exec_err, internal_err,
41};
42use datafusion_expr::sort_properties::{ExprProperties, SortProperties};
43use datafusion_expr::{
44    ColumnarValue, Documentation, ReturnFieldArgs, ScalarFunctionArgs, ScalarUDFImpl,
45    Signature, TypeSignature, Volatility,
46};
47use datafusion_expr_common::signature::{Coercion, TypeSignatureClass};
48use datafusion_macros::user_doc;
49
50use chrono::{
51    DateTime, Datelike, Duration, LocalResult, NaiveDateTime, Offset, TimeDelta, Timelike,
52};
53
54/// Represents the granularity for date truncation operations
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56enum DateTruncGranularity {
57    Microsecond,
58    Millisecond,
59    Second,
60    Minute,
61    Hour,
62    Day,
63    Week,
64    Month,
65    Quarter,
66    Year,
67}
68
69impl DateTruncGranularity {
70    /// List of all supported granularity values
71    /// Cannot use HashMap here as it would require lazy_static or once_cell,
72    /// Rust does not support const HashMap yet.
73    const SUPPORTED_GRANULARITIES: &[&str] = &[
74        "microsecond",
75        "millisecond",
76        "second",
77        "minute",
78        "hour",
79        "day",
80        "week",
81        "month",
82        "quarter",
83        "year",
84    ];
85
86    /// Parse a granularity string into a DateTruncGranularity enum
87    fn from_str(s: &str) -> Result<Self> {
88        // Using match for O(1) lookup - compiler optimizes this into a jump table or perfect hash
89        match s.to_lowercase().as_str() {
90            "microsecond" => Ok(Self::Microsecond),
91            "millisecond" => Ok(Self::Millisecond),
92            "second" => Ok(Self::Second),
93            "minute" => Ok(Self::Minute),
94            "hour" => Ok(Self::Hour),
95            "day" => Ok(Self::Day),
96            "week" => Ok(Self::Week),
97            "month" => Ok(Self::Month),
98            "quarter" => Ok(Self::Quarter),
99            "year" => Ok(Self::Year),
100            _ => {
101                let supported = Self::SUPPORTED_GRANULARITIES.join(", ");
102                exec_err!(
103                    "Unsupported date_trunc granularity: '{s}'. Supported values are: {supported}"
104                )
105            }
106        }
107    }
108
109    /// Returns true if this granularity can be handled with simple arithmetic
110    /// (fine granularity: second, minute, millisecond, microsecond)
111    fn is_fine_granularity(&self) -> bool {
112        matches!(
113            self,
114            Self::Second | Self::Minute | Self::Millisecond | Self::Microsecond
115        )
116    }
117
118    /// Returns true if this granularity can be handled with simple arithmetic in UTC
119    /// (hour and day in addition to fine granularities)
120    fn is_fine_granularity_utc(&self) -> bool {
121        self.is_fine_granularity() || matches!(self, Self::Hour | Self::Day)
122    }
123
124    /// Returns true if this granularity is valid for Time types
125    /// Time types don't have date components, so day/week/month/quarter/year are not valid
126    fn valid_for_time(&self) -> bool {
127        matches!(
128            self,
129            Self::Hour
130                | Self::Minute
131                | Self::Second
132                | Self::Millisecond
133                | Self::Microsecond
134        )
135    }
136}
137
138impl fmt::Display for DateTruncGranularity {
139    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
140        let value = match self {
141            Self::Microsecond => "microsecond",
142            Self::Millisecond => "millisecond",
143            Self::Second => "second",
144            Self::Minute => "minute",
145            Self::Hour => "hour",
146            Self::Day => "day",
147            Self::Week => "week",
148            Self::Month => "month",
149            Self::Quarter => "quarter",
150            Self::Year => "year",
151        };
152        f.write_str(value)
153    }
154}
155
156#[user_doc(
157    doc_section(label = "Time and Date Functions"),
158    description = "Truncates a timestamp or time value to a specified precision.",
159    syntax_example = "date_trunc(precision, expression)",
160    argument(
161        name = "precision",
162        description = r#"Time precision to truncate to. The following precisions are supported:
163
164    For Timestamp types:
165    - year / YEAR
166    - quarter / QUARTER
167    - month / MONTH
168    - week / WEEK
169    - day / DAY
170    - hour / HOUR
171    - minute / MINUTE
172    - second / SECOND
173    - millisecond / MILLISECOND
174    - microsecond / MICROSECOND
175
176    For Time types (hour, minute, second, millisecond, microsecond only):
177    - hour / HOUR
178    - minute / MINUTE
179    - second / SECOND
180    - millisecond / MILLISECOND
181    - microsecond / MICROSECOND
182"#
183    ),
184    argument(
185        name = "expression",
186        description = "Timestamp or time expression to operate on. Can be a constant, column, or function."
187    ),
188    sql_example = r#"```sql
189> SELECT date_trunc('month', '2024-05-15T10:30:00');
190+-----------------------------------------------+
191| date_trunc(Utf8("month"),Utf8("2024-05-15T10:30:00")) |
192+-----------------------------------------------+
193| 2024-05-01T00:00:00                           |
194+-----------------------------------------------+
195> SELECT date_trunc('hour', '2024-05-15T10:30:00');
196+----------------------------------------------+
197| date_trunc(Utf8("hour"),Utf8("2024-05-15T10:30:00")) |
198+----------------------------------------------+
199| 2024-05-15T10:00:00                          |
200+----------------------------------------------+
201```"#
202)]
203#[derive(Debug, PartialEq, Eq, Hash)]
204pub struct DateTruncFunc {
205    signature: Signature,
206    aliases: Vec<String>,
207}
208
209impl Default for DateTruncFunc {
210    fn default() -> Self {
211        Self::new()
212    }
213}
214
215impl DateTruncFunc {
216    pub fn new() -> Self {
217        Self {
218            signature: Signature::one_of(
219                vec![
220                    TypeSignature::Coercible(vec![
221                        Coercion::new_exact(TypeSignatureClass::Native(logical_string())),
222                        Coercion::new_implicit(
223                            TypeSignatureClass::Timestamp,
224                            // Allow implicit cast from string and date to timestamp for backward compatibility
225                            vec![
226                                TypeSignatureClass::Native(logical_string()),
227                                TypeSignatureClass::Native(logical_date()),
228                            ],
229                            NativeType::Timestamp(Nanosecond, None),
230                        ),
231                    ]),
232                    TypeSignature::Coercible(vec![
233                        Coercion::new_exact(TypeSignatureClass::Native(logical_string())),
234                        Coercion::new_exact(TypeSignatureClass::Time),
235                    ]),
236                ],
237                Volatility::Immutable,
238            ),
239            aliases: vec![String::from("datetrunc")],
240        }
241    }
242}
243
244impl ScalarUDFImpl for DateTruncFunc {
245    fn name(&self) -> &str {
246        "date_trunc"
247    }
248
249    fn signature(&self) -> &Signature {
250        &self.signature
251    }
252
253    fn return_type(&self, _arg_types: &[DataType]) -> Result<DataType> {
254        internal_err!("return_field_from_args should be called instead")
255    }
256
257    fn return_field_from_args(&self, args: ReturnFieldArgs) -> Result<FieldRef> {
258        let field = &args.arg_fields[1];
259        let return_type = if field.data_type().is_null() {
260            Timestamp(Nanosecond, None)
261        } else {
262            field.data_type().clone()
263        };
264        Ok(Arc::new(Field::new(
265            self.name(),
266            return_type,
267            field.is_nullable(),
268        )))
269    }
270
271    fn invoke_with_args(&self, args: ScalarFunctionArgs) -> Result<ColumnarValue> {
272        let args = args.args;
273        let (granularity, array) = (&args[0], &args[1]);
274
275        let granularity_str = if let ColumnarValue::Scalar(ScalarValue::Utf8(Some(v))) =
276            granularity
277        {
278            v.to_lowercase()
279        } else if let ColumnarValue::Scalar(ScalarValue::Utf8View(Some(v))) = granularity
280        {
281            v.to_lowercase()
282        } else if let ColumnarValue::Scalar(ScalarValue::LargeUtf8(Some(v))) = granularity
283        {
284            v.to_lowercase()
285        } else {
286            return exec_err!("Granularity of `date_trunc` must be non-null scalar Utf8");
287        };
288
289        let granularity = DateTruncGranularity::from_str(&granularity_str)?;
290
291        // Check upfront if granularity is valid for Time types
292        let is_time_type = matches!(array.data_type(), Time64(_) | Time32(_));
293        if is_time_type && !granularity.valid_for_time() {
294            return exec_err!(
295                "date_trunc does not support '{}' granularity for Time types. Valid values are: hour, minute, second, millisecond, microsecond",
296                granularity_str
297            );
298        }
299
300        fn process_array<T: ArrowTimestampType>(
301            array: &dyn Array,
302            granularity: DateTruncGranularity,
303            tz_opt: &Option<Arc<str>>,
304        ) -> Result<ColumnarValue> {
305            let parsed_tz = parse_tz(tz_opt)?;
306            let array = as_primitive_array::<T>(array)?;
307
308            // fast path for fine granularity
309            // For modern timezones, it's correct to truncate "minute" in this way.
310            // Both datafusion and arrow are ignoring historical timezone's non-minute granularity
311            // bias (e.g., Asia/Kathmandu before 1919 is UTC+05:41:16).
312            // In UTC, "hour" and "day" have uniform durations and can be truncated with simple arithmetic
313            if granularity.is_fine_granularity()
314                || (parsed_tz.is_none() && granularity.is_fine_granularity_utc())
315            {
316                let result = general_date_trunc_array_fine_granularity(
317                    T::UNIT,
318                    array,
319                    granularity,
320                    tz_opt.clone(),
321                )?;
322                return Ok(ColumnarValue::Array(result));
323            }
324
325            let array: PrimitiveArray<T> = array
326                .try_unary(|x| general_date_trunc(T::UNIT, x, parsed_tz, granularity))?
327                .with_timezone_opt(tz_opt.clone());
328            Ok(ColumnarValue::Array(Arc::new(array)))
329        }
330
331        fn process_scalar<T: ArrowTimestampType>(
332            v: &Option<i64>,
333            granularity: DateTruncGranularity,
334            tz_opt: &Option<Arc<str>>,
335        ) -> Result<ColumnarValue> {
336            let parsed_tz = parse_tz(tz_opt)?;
337            let value = if let Some(v) = v {
338                Some(general_date_trunc(T::UNIT, *v, parsed_tz, granularity)?)
339            } else {
340                None
341            };
342            let value = ScalarValue::new_timestamp::<T>(value, tz_opt.clone());
343            Ok(ColumnarValue::Scalar(value))
344        }
345
346        Ok(match array {
347            ColumnarValue::Scalar(ScalarValue::Null) => {
348                // NULL input returns NULL timestamp
349                ColumnarValue::Scalar(ScalarValue::TimestampNanosecond(None, None))
350            }
351            ColumnarValue::Scalar(ScalarValue::TimestampNanosecond(v, tz_opt)) => {
352                process_scalar::<TimestampNanosecondType>(v, granularity, tz_opt)?
353            }
354            ColumnarValue::Scalar(ScalarValue::TimestampMicrosecond(v, tz_opt)) => {
355                process_scalar::<TimestampMicrosecondType>(v, granularity, tz_opt)?
356            }
357            ColumnarValue::Scalar(ScalarValue::TimestampMillisecond(v, tz_opt)) => {
358                process_scalar::<TimestampMillisecondType>(v, granularity, tz_opt)?
359            }
360            ColumnarValue::Scalar(ScalarValue::TimestampSecond(v, tz_opt)) => {
361                process_scalar::<TimestampSecondType>(v, granularity, tz_opt)?
362            }
363            ColumnarValue::Scalar(ScalarValue::Time64Nanosecond(v)) => {
364                let truncated = v.map(|val| truncate_time_nanos(val, granularity));
365                ColumnarValue::Scalar(ScalarValue::Time64Nanosecond(truncated))
366            }
367            ColumnarValue::Scalar(ScalarValue::Time64Microsecond(v)) => {
368                let truncated = v.map(|val| truncate_time_micros(val, granularity));
369                ColumnarValue::Scalar(ScalarValue::Time64Microsecond(truncated))
370            }
371            ColumnarValue::Scalar(ScalarValue::Time32Millisecond(v)) => {
372                let truncated = v.map(|val| truncate_time_millis(val, granularity));
373                ColumnarValue::Scalar(ScalarValue::Time32Millisecond(truncated))
374            }
375            ColumnarValue::Scalar(ScalarValue::Time32Second(v)) => {
376                let truncated = v.map(|val| truncate_time_secs(val, granularity));
377                ColumnarValue::Scalar(ScalarValue::Time32Second(truncated))
378            }
379            ColumnarValue::Array(array) => {
380                let array_type = array.data_type();
381                match array_type {
382                    Timestamp(Second, tz_opt) => {
383                        process_array::<TimestampSecondType>(array, granularity, tz_opt)?
384                    }
385                    Timestamp(Millisecond, tz_opt) => process_array::<
386                        TimestampMillisecondType,
387                    >(
388                        array, granularity, tz_opt
389                    )?,
390                    Timestamp(Microsecond, tz_opt) => process_array::<
391                        TimestampMicrosecondType,
392                    >(
393                        array, granularity, tz_opt
394                    )?,
395                    Timestamp(Nanosecond, tz_opt) => process_array::<
396                        TimestampNanosecondType,
397                    >(
398                        array, granularity, tz_opt
399                    )?,
400                    Time64(Nanosecond) => {
401                        let arr = as_primitive_array::<Time64NanosecondType>(array)?;
402                        let result: PrimitiveArray<Time64NanosecondType> =
403                            arr.unary(|v| truncate_time_nanos(v, granularity));
404                        ColumnarValue::Array(Arc::new(result))
405                    }
406                    Time64(Microsecond) => {
407                        let arr = as_primitive_array::<Time64MicrosecondType>(array)?;
408                        let result: PrimitiveArray<Time64MicrosecondType> =
409                            arr.unary(|v| truncate_time_micros(v, granularity));
410                        ColumnarValue::Array(Arc::new(result))
411                    }
412                    Time32(Millisecond) => {
413                        let arr = as_primitive_array::<Time32MillisecondType>(array)?;
414                        let result: PrimitiveArray<Time32MillisecondType> =
415                            arr.unary(|v| truncate_time_millis(v, granularity));
416                        ColumnarValue::Array(Arc::new(result))
417                    }
418                    Time32(Second) => {
419                        let arr = as_primitive_array::<Time32SecondType>(array)?;
420                        let result: PrimitiveArray<Time32SecondType> =
421                            arr.unary(|v| truncate_time_secs(v, granularity));
422                        ColumnarValue::Array(Arc::new(result))
423                    }
424                    _ => {
425                        return exec_err!(
426                            "second argument of `date_trunc` is an unsupported array type: {array_type}"
427                        );
428                    }
429                }
430            }
431            _ => {
432                return exec_err!(
433                    "second argument of `date_trunc` must be timestamp, time scalar or array"
434                );
435            }
436        })
437    }
438
439    fn aliases(&self) -> &[String] {
440        &self.aliases
441    }
442
443    fn output_ordering(&self, input: &[ExprProperties]) -> Result<SortProperties> {
444        // The DATE_TRUNC function preserves the order of its second argument.
445        let precision = &input[0];
446        let date_value = &input[1];
447
448        if precision.sort_properties.eq(&SortProperties::Singleton) {
449            Ok(date_value.sort_properties)
450        } else {
451            Ok(SortProperties::Unordered)
452        }
453    }
454    fn documentation(&self) -> Option<&Documentation> {
455        self.doc()
456    }
457}
458
459const NANOS_PER_MICROSECOND: i64 = NANOSECONDS / MICROSECONDS;
460const NANOS_PER_MILLISECOND: i64 = NANOSECONDS / MILLISECONDS;
461const NANOS_PER_SECOND: i64 = NANOSECONDS;
462const NANOS_PER_MINUTE: i64 = 60 * NANOS_PER_SECOND;
463const NANOS_PER_HOUR: i64 = 60 * NANOS_PER_MINUTE;
464const NANOS_PER_DAY: i64 = 24 * NANOS_PER_HOUR;
465
466const MICROS_PER_MILLISECOND: i64 = MICROSECONDS / MILLISECONDS;
467const MICROS_PER_SECOND: i64 = MICROSECONDS;
468const MICROS_PER_MINUTE: i64 = 60 * MICROS_PER_SECOND;
469const MICROS_PER_HOUR: i64 = 60 * MICROS_PER_MINUTE;
470
471const MILLIS_PER_SECOND: i32 = MILLISECONDS as i32;
472const MILLIS_PER_MINUTE: i32 = 60 * MILLIS_PER_SECOND;
473const MILLIS_PER_HOUR: i32 = 60 * MILLIS_PER_MINUTE;
474
475const SECS_PER_MINUTE: i32 = 60;
476const SECS_PER_HOUR: i32 = 60 * SECS_PER_MINUTE;
477
478/// Truncate time in nanoseconds to the specified granularity
479fn truncate_time_nanos(value: i64, granularity: DateTruncGranularity) -> i64 {
480    match granularity {
481        DateTruncGranularity::Hour => value - (value % NANOS_PER_HOUR),
482        DateTruncGranularity::Minute => value - (value % NANOS_PER_MINUTE),
483        DateTruncGranularity::Second => value - (value % NANOS_PER_SECOND),
484        DateTruncGranularity::Millisecond => value - (value % NANOS_PER_MILLISECOND),
485        DateTruncGranularity::Microsecond => value - (value % NANOS_PER_MICROSECOND),
486        // Other granularities are not valid for time - should be caught earlier
487        _ => value,
488    }
489}
490
491/// Truncate time in microseconds to the specified granularity
492fn truncate_time_micros(value: i64, granularity: DateTruncGranularity) -> i64 {
493    match granularity {
494        DateTruncGranularity::Hour => value - (value % MICROS_PER_HOUR),
495        DateTruncGranularity::Minute => value - (value % MICROS_PER_MINUTE),
496        DateTruncGranularity::Second => value - (value % MICROS_PER_SECOND),
497        DateTruncGranularity::Millisecond => value - (value % MICROS_PER_MILLISECOND),
498        DateTruncGranularity::Microsecond => value, // Already at microsecond precision
499        // Other granularities are not valid for time
500        _ => value,
501    }
502}
503
504/// Truncate time in milliseconds to the specified granularity
505fn truncate_time_millis(value: i32, granularity: DateTruncGranularity) -> i32 {
506    match granularity {
507        DateTruncGranularity::Hour => value - (value % MILLIS_PER_HOUR),
508        DateTruncGranularity::Minute => value - (value % MILLIS_PER_MINUTE),
509        DateTruncGranularity::Second => value - (value % MILLIS_PER_SECOND),
510        DateTruncGranularity::Millisecond => value, // Already at millisecond precision
511        DateTruncGranularity::Microsecond => value, // Can't truncate to finer precision
512        // Other granularities are not valid for time
513        _ => value,
514    }
515}
516
517/// Truncate time in seconds to the specified granularity
518fn truncate_time_secs(value: i32, granularity: DateTruncGranularity) -> i32 {
519    match granularity {
520        DateTruncGranularity::Hour => value - (value % SECS_PER_HOUR),
521        DateTruncGranularity::Minute => value - (value % SECS_PER_MINUTE),
522        DateTruncGranularity::Second => value, // Already at second precision
523        DateTruncGranularity::Millisecond => value, // Can't truncate to finer precision
524        DateTruncGranularity::Microsecond => value, // Can't truncate to finer precision
525        // Other granularities are not valid for time
526        _ => value,
527    }
528}
529
530fn _date_trunc_coarse<T>(
531    granularity: DateTruncGranularity,
532    value: Option<T>,
533) -> Result<Option<T>>
534where
535    T: Datelike + Timelike + Sub<Duration, Output = T> + Copy,
536{
537    let value = match granularity {
538        DateTruncGranularity::Millisecond => value,
539        DateTruncGranularity::Microsecond => value,
540        DateTruncGranularity::Second => value.and_then(|d| d.with_nanosecond(0)),
541        DateTruncGranularity::Minute => value
542            .and_then(|d| d.with_nanosecond(0))
543            .and_then(|d| d.with_second(0)),
544        DateTruncGranularity::Hour => value
545            .and_then(|d| d.with_nanosecond(0))
546            .and_then(|d| d.with_second(0))
547            .and_then(|d| d.with_minute(0)),
548        DateTruncGranularity::Day => value
549            .and_then(|d| d.with_nanosecond(0))
550            .and_then(|d| d.with_second(0))
551            .and_then(|d| d.with_minute(0))
552            .and_then(|d| d.with_hour(0)),
553        DateTruncGranularity::Week => value
554            .and_then(|d| d.with_nanosecond(0))
555            .and_then(|d| d.with_second(0))
556            .and_then(|d| d.with_minute(0))
557            .and_then(|d| d.with_hour(0))
558            .map(|d| {
559                d - TimeDelta::try_seconds(60 * 60 * 24 * d.weekday() as i64).unwrap()
560            }),
561        DateTruncGranularity::Month => value
562            .and_then(|d| d.with_nanosecond(0))
563            .and_then(|d| d.with_second(0))
564            .and_then(|d| d.with_minute(0))
565            .and_then(|d| d.with_hour(0))
566            .and_then(|d| d.with_day0(0)),
567        DateTruncGranularity::Quarter => value
568            .and_then(|d| d.with_nanosecond(0))
569            .and_then(|d| d.with_second(0))
570            .and_then(|d| d.with_minute(0))
571            .and_then(|d| d.with_hour(0))
572            .and_then(|d| d.with_day0(0))
573            .and_then(|d| d.with_month(quarter_month(&d))),
574        DateTruncGranularity::Year => value
575            .and_then(|d| d.with_nanosecond(0))
576            .and_then(|d| d.with_second(0))
577            .and_then(|d| d.with_minute(0))
578            .and_then(|d| d.with_hour(0))
579            .and_then(|d| d.with_day0(0))
580            .and_then(|d| d.with_month0(0)),
581    };
582    Ok(value)
583}
584
585fn quarter_month<T>(date: &T) -> u32
586where
587    T: Datelike,
588{
589    1 + 3 * ((date.month() - 1) / 3)
590}
591
592fn _date_trunc_coarse_with_tz(
593    granularity: DateTruncGranularity,
594    value: DateTime<Tz>,
595) -> Result<Option<i64>> {
596    let local = value.naive_local();
597    let truncated = _date_trunc_coarse::<NaiveDateTime>(granularity, Some(local))?;
598    let truncated = truncated.and_then(|truncated| {
599        match truncated.and_local_timezone(value.timezone()) {
600            LocalResult::None => {
601                // This can happen if the date_trunc operation moves the time into
602                // an hour that doesn't exist due to daylight savings. On known example where
603                // this can happen is with historic dates in the America/Sao_Paulo time zone.
604                // To account for this adjust the time by a few hours, convert to local time,
605                // and then adjust the time back.
606                truncated
607                    .sub(TimeDelta::try_hours(3).unwrap())
608                    .and_local_timezone(value.timezone())
609                    .single()
610                    .map(|v| v.add(TimeDelta::try_hours(3).unwrap()))
611            }
612            LocalResult::Single(datetime) => Some(datetime),
613            LocalResult::Ambiguous(datetime1, datetime2) => {
614                // Because we are truncating from an equally or more specific time
615                // the original time must have been within the ambiguous local time
616                // period. Therefore the offset of one of these times should match the
617                // offset of the original time.
618                if datetime1.offset().fix() == value.offset().fix() {
619                    Some(datetime1)
620                } else {
621                    Some(datetime2)
622                }
623            }
624        }
625    });
626    Ok(truncated.and_then(|value| value.timestamp_nanos_opt()))
627}
628
629// The two helpers below duplicate `chrono::NaiveDate::{from_epoch_days,
630// to_epoch_days}`. They are kept separate because chrono's versions round trip
631// through a validated `NaiveDate`: `from_epoch_days` computes year flags and
632// returns an `Option`, and reading the year/month/day back out decodes them from
633// its packed representation. These helpers stay in plain integers, which is all
634// the truncation below needs.
635
636/// Days from the Unix epoch to 0000-03-01, the epoch used by the civil calendar
637/// conversions below.
638const DAYS_EPOCH_SHIFT: i64 = 719_468;
639
640/// Days in a 400 year era of the proleptic Gregorian calendar.
641const DAYS_PER_ERA: i64 = 146_097;
642
643/// Splits a day count relative to the Unix epoch into a proleptic Gregorian
644/// year, month (1-12) and day of month (1-31).
645///
646/// This is a port of Howard Hinnant's `civil_from_days`, which documents the
647/// derivation of the constants and the March-based year used below:
648/// <https://howardhinnant.github.io/date_algorithms.html#civil_from_days>
649fn civil_from_days(days: i64) -> (i64, i64, i64) {
650    let z = days + DAYS_EPOCH_SHIFT;
651    let era = z.div_euclid(DAYS_PER_ERA);
652    let day_of_era = z.rem_euclid(DAYS_PER_ERA);
653    let year_of_era = (day_of_era - day_of_era / 1460 + day_of_era / 36524
654        - day_of_era / 146_096)
655        / 365;
656    let day_of_year =
657        day_of_era - (365 * year_of_era + year_of_era / 4 - year_of_era / 100);
658    // Month index with March as 0, so that the leap day falls at the end of the year.
659    let month_index = (5 * day_of_year + 2) / 153;
660    let day = day_of_year - (153 * month_index + 2) / 5 + 1;
661    let month = if month_index < 10 {
662        month_index + 3
663    } else {
664        month_index - 9
665    };
666    let year = year_of_era + era * 400 + i64::from(month <= 2);
667    (year, month, day)
668}
669
670/// Inverse of [`civil_from_days`]: the day count relative to the Unix epoch for
671/// the given proleptic Gregorian date.
672///
673/// This is a port of Howard Hinnant's `days_from_civil`, which documents the
674/// derivation of the constants and the March-based year used below:
675/// <https://howardhinnant.github.io/date_algorithms.html#days_from_civil>
676fn days_from_civil(year: i64, month: i64, day: i64) -> i64 {
677    let year = year - i64::from(month <= 2);
678    let era = year.div_euclid(400);
679    let year_of_era = year.rem_euclid(400);
680    let month_index = if month > 2 { month - 3 } else { month + 9 };
681    let day_of_year = (153 * month_index + 2) / 5 + day - 1;
682    let day_of_era =
683        year_of_era * 365 + year_of_era / 4 - year_of_era / 100 + day_of_year;
684    era * DAYS_PER_ERA + day_of_era - DAYS_EPOCH_SHIFT
685}
686
687/// Truncates a UTC nanosecond timestamp with integer arithmetic. Truncating on
688/// the calendar directly avoids converting every value to a `NaiveDateTime` and
689/// rebuilding it field by field.
690///
691/// Returns `None` when the truncated timestamp is no longer representable as
692/// nanoseconds since the epoch, which the caller reports as an out of range
693/// error.
694fn _date_trunc_coarse_without_tz(
695    granularity: DateTruncGranularity,
696    value: i64,
697) -> Option<i64> {
698    let truncate_to = |unit: i64| value.checked_sub(value.rem_euclid(unit));
699    let days = || value.div_euclid(NANOS_PER_DAY);
700    let nanos_from_days = |days: i64| days.checked_mul(NANOS_PER_DAY);
701
702    match granularity {
703        // Sub-second granularities are applied by the caller, which rescales
704        // the nanoseconds to the time unit of the array.
705        DateTruncGranularity::Millisecond | DateTruncGranularity::Microsecond => {
706            Some(value)
707        }
708        DateTruncGranularity::Second => truncate_to(NANOS_PER_SECOND),
709        DateTruncGranularity::Minute => truncate_to(NANOS_PER_MINUTE),
710        DateTruncGranularity::Hour => truncate_to(NANOS_PER_HOUR),
711        DateTruncGranularity::Day => nanos_from_days(days()),
712        DateTruncGranularity::Week => {
713            let days = days();
714            // `Weekday::num_days_from_monday` for the epoch (a Thursday) is 3.
715            nanos_from_days(days - (days + 3).rem_euclid(7))
716        }
717        DateTruncGranularity::Month => {
718            let days = days();
719            let (_, _, day_of_month) = civil_from_days(days);
720            nanos_from_days(days - (day_of_month - 1))
721        }
722        DateTruncGranularity::Quarter => {
723            let (year, month, _) = civil_from_days(days());
724            nanos_from_days(days_from_civil(year, 1 + 3 * ((month - 1) / 3), 1))
725        }
726        DateTruncGranularity::Year => {
727            let (year, _, _) = civil_from_days(days());
728            nanos_from_days(days_from_civil(year, 1, 1))
729        }
730    }
731}
732
733/// Truncates the single `value`, expressed in nanoseconds since the
734/// epoch, for granularities greater than 1 second, in taking into
735/// account that some granularities are not uniform durations of time
736/// (e.g. months are not always the same lengths, leap seconds, etc)
737fn date_trunc_coarse(
738    granularity: DateTruncGranularity,
739    value: i64,
740    tz: Option<Tz>,
741) -> Result<i64> {
742    let input = value;
743    let value = match tz {
744        Some(tz) => {
745            // Use chrono DateTime<Tz> to clear the various fields because need to clear per timezone,
746            // and NaiveDateTime (ISO 8601) has no concept of timezones
747            let value = as_datetime_with_timezone::<TimestampNanosecondType>(value, tz)
748                .ok_or(exec_datafusion_err!("Timestamp {value} out of range"))?;
749            _date_trunc_coarse_with_tz(granularity, value)?
750        }
751        None => _date_trunc_coarse_without_tz(granularity, value),
752    };
753
754    value.ok_or_else(|| {
755        exec_datafusion_err!(
756            "Timestamp {input} out of range after truncating to {granularity}"
757        )
758    })
759}
760
761/// Fast path for fine granularities (hour and smaller) that can be handled
762/// with simple arithmetic operations without calendar complexity.
763///
764/// This function is timezone-agnostic and should only be used when:
765/// - No timezone is specified in the input, OR
766/// - The granularity is less than hour as hour can be affected by DST transitions in some cases
767fn general_date_trunc_array_fine_granularity<T: ArrowTimestampType>(
768    tu: TimeUnit,
769    array: &PrimitiveArray<T>,
770    granularity: DateTruncGranularity,
771    tz_opt: Option<Arc<str>>,
772) -> Result<ArrayRef> {
773    let unit = match (tu, granularity) {
774        (Second, DateTruncGranularity::Minute) => NonZeroI64::new(60),
775        (Second, DateTruncGranularity::Hour) => NonZeroI64::new(3600),
776        (Second, DateTruncGranularity::Day) => NonZeroI64::new(86400),
777
778        (Millisecond, DateTruncGranularity::Second) => NonZeroI64::new(1_000),
779        (Millisecond, DateTruncGranularity::Minute) => NonZeroI64::new(60_000),
780        (Millisecond, DateTruncGranularity::Hour) => NonZeroI64::new(3_600_000),
781        (Millisecond, DateTruncGranularity::Day) => NonZeroI64::new(86_400_000),
782
783        (Microsecond, DateTruncGranularity::Millisecond) => NonZeroI64::new(1_000),
784        (Microsecond, DateTruncGranularity::Second) => NonZeroI64::new(1_000_000),
785        (Microsecond, DateTruncGranularity::Minute) => NonZeroI64::new(60_000_000),
786        (Microsecond, DateTruncGranularity::Hour) => NonZeroI64::new(3_600_000_000),
787        (Microsecond, DateTruncGranularity::Day) => NonZeroI64::new(86_400_000_000),
788
789        (Nanosecond, DateTruncGranularity::Microsecond) => NonZeroI64::new(1_000),
790        (Nanosecond, DateTruncGranularity::Millisecond) => NonZeroI64::new(1_000_000),
791        (Nanosecond, DateTruncGranularity::Second) => NonZeroI64::new(1_000_000_000),
792        (Nanosecond, DateTruncGranularity::Minute) => NonZeroI64::new(60_000_000_000),
793        (Nanosecond, DateTruncGranularity::Hour) => NonZeroI64::new(3_600_000_000_000),
794        (Nanosecond, DateTruncGranularity::Day) => NonZeroI64::new(86_400_000_000_000),
795        _ => None,
796    };
797
798    if let Some(unit) = unit {
799        let unit = unit.get();
800        let array = PrimitiveArray::<T>::from_iter_values_with_nulls(
801            array
802                .values()
803                .iter()
804                .map(|v| *v - i64::rem_euclid(*v, unit)),
805            array.nulls().cloned(),
806        )
807        .with_timezone_opt(tz_opt);
808        Ok(Arc::new(array))
809    } else {
810        // truncate to the same or smaller unit
811        Ok(Arc::new(array.clone()))
812    }
813}
814
815// truncates a single value with the given timeunit to the specified granularity
816fn general_date_trunc(
817    tu: TimeUnit,
818    value: i64,
819    tz: Option<Tz>,
820    granularity: DateTruncGranularity,
821) -> Result<i64, DataFusionError> {
822    let scale = match tu {
823        Second => 1_000_000_000,
824        Millisecond => 1_000_000,
825        Microsecond => 1_000,
826        Nanosecond => 1,
827    };
828
829    // convert to nanoseconds
830    let nano = date_trunc_coarse(
831        granularity,
832        value
833            .checked_mul(scale)
834            .ok_or_else(|| exec_datafusion_err!("Timestamp {value} out of range"))?,
835        tz,
836    )?;
837
838    let result = match tu {
839        Second => match granularity {
840            DateTruncGranularity::Minute => nano / 1_000_000_000 / 60 * 60,
841            _ => nano / 1_000_000_000,
842        },
843        Millisecond => match granularity {
844            DateTruncGranularity::Minute => nano / 1_000_000 / 1_000 / 60 * 1_000 * 60,
845            DateTruncGranularity::Second => nano / 1_000_000 / 1_000 * 1_000,
846            _ => nano / 1_000_000,
847        },
848        Microsecond => match granularity {
849            DateTruncGranularity::Minute => {
850                nano / 1_000 / 1_000_000 / 60 * 60 * 1_000_000
851            }
852            DateTruncGranularity::Second => nano / 1_000 / 1_000_000 * 1_000_000,
853            DateTruncGranularity::Millisecond => nano / 1_000 / 1_000 * 1_000,
854            _ => nano / 1_000,
855        },
856        _ => match granularity {
857            DateTruncGranularity::Minute => {
858                nano / 1_000_000_000 / 60 * 1_000_000_000 * 60
859            }
860            DateTruncGranularity::Second => nano / 1_000_000_000 * 1_000_000_000,
861            DateTruncGranularity::Millisecond => nano / 1_000_000 * 1_000_000,
862            DateTruncGranularity::Microsecond => nano / 1_000 * 1_000,
863            _ => nano,
864        },
865    };
866    Ok(result)
867}
868
869fn parse_tz(tz: &Option<Arc<str>>) -> Result<Option<Tz>> {
870    tz.as_ref()
871        .map(|tz| {
872            Tz::from_str(tz)
873                .map_err(|op| exec_datafusion_err!("failed on timezone {tz}: {op:?}"))
874        })
875        .transpose()
876}
877
878#[cfg(test)]
879mod tests {
880    use std::sync::Arc;
881
882    use crate::datetime::date_trunc::{
883        DateTruncFunc, DateTruncGranularity, date_trunc_coarse,
884    };
885
886    use arrow::array::cast::as_primitive_array;
887    use arrow::array::types::TimestampNanosecondType;
888    use arrow::array::{Array, TimestampNanosecondArray};
889    use arrow::compute::kernels::cast_utils::string_to_timestamp_nanos;
890    use arrow::datatypes::{DataType, Field, TimeUnit};
891    use datafusion_common::ScalarValue;
892    use datafusion_common::config::ConfigOptions;
893    use datafusion_expr::{ColumnarValue, ScalarFunctionArgs, ScalarUDFImpl};
894
895    #[test]
896    fn date_trunc_test() {
897        let cases = vec![
898            (
899                "2020-09-08T13:42:29.190855Z",
900                "second",
901                "2020-09-08T13:42:29.000000Z",
902            ),
903            (
904                "2020-09-08T13:42:29.190855Z",
905                "minute",
906                "2020-09-08T13:42:00.000000Z",
907            ),
908            (
909                "2020-09-08T13:42:29.190855Z",
910                "hour",
911                "2020-09-08T13:00:00.000000Z",
912            ),
913            (
914                "2020-09-08T13:42:29.190855Z",
915                "day",
916                "2020-09-08T00:00:00.000000Z",
917            ),
918            (
919                "2020-09-08T13:42:29.190855Z",
920                "week",
921                "2020-09-07T00:00:00.000000Z",
922            ),
923            (
924                "2020-09-08T13:42:29.190855Z",
925                "month",
926                "2020-09-01T00:00:00.000000Z",
927            ),
928            (
929                "2020-09-08T13:42:29.190855Z",
930                "year",
931                "2020-01-01T00:00:00.000000Z",
932            ),
933            // week
934            (
935                "2021-01-01T13:42:29.190855Z",
936                "week",
937                "2020-12-28T00:00:00.000000Z",
938            ),
939            (
940                "2020-01-01T13:42:29.190855Z",
941                "week",
942                "2019-12-30T00:00:00.000000Z",
943            ),
944            // quarter
945            (
946                "2020-01-01T13:42:29.190855Z",
947                "quarter",
948                "2020-01-01T00:00:00.000000Z",
949            ),
950            (
951                "2020-02-01T13:42:29.190855Z",
952                "quarter",
953                "2020-01-01T00:00:00.000000Z",
954            ),
955            (
956                "2020-03-01T13:42:29.190855Z",
957                "quarter",
958                "2020-01-01T00:00:00.000000Z",
959            ),
960            (
961                "2020-04-01T13:42:29.190855Z",
962                "quarter",
963                "2020-04-01T00:00:00.000000Z",
964            ),
965            (
966                "2020-08-01T13:42:29.190855Z",
967                "quarter",
968                "2020-07-01T00:00:00.000000Z",
969            ),
970            (
971                "2020-11-01T13:42:29.190855Z",
972                "quarter",
973                "2020-10-01T00:00:00.000000Z",
974            ),
975            (
976                "2020-12-01T13:42:29.190855Z",
977                "quarter",
978                "2020-10-01T00:00:00.000000Z",
979            ),
980        ];
981
982        cases.iter().for_each(|(original, granularity, expected)| {
983            let left = string_to_timestamp_nanos(original).unwrap();
984            let right = string_to_timestamp_nanos(expected).unwrap();
985            let granularity_enum = DateTruncGranularity::from_str(granularity).unwrap();
986            let result = date_trunc_coarse(granularity_enum, left, None).unwrap();
987            assert_eq!(result, right, "{original} = {expected}");
988        });
989    }
990
991    #[test]
992    fn date_trunc_out_of_range_lower_bound_returns_error() {
993        let timestamp = string_to_timestamp_nanos("1677-09-22T00:00:00Z").unwrap();
994        let err = date_trunc_coarse(DateTruncGranularity::Year, timestamp, None)
995            .unwrap_err()
996            .to_string();
997
998        assert!(
999            err.contains("out of range after truncating to year"),
1000            "{err}"
1001        );
1002    }
1003
1004    #[test]
1005    fn test_date_trunc_timezones() {
1006        let cases = [
1007            (
1008                vec![
1009                    "2020-09-08T00:00:00Z",
1010                    "2020-09-08T01:00:00Z",
1011                    "2020-09-08T02:00:00Z",
1012                    "2020-09-08T03:00:00Z",
1013                    "2020-09-08T04:00:00Z",
1014                ],
1015                Some("+00".into()),
1016                vec![
1017                    "2020-09-08T00:00:00Z",
1018                    "2020-09-08T00:00:00Z",
1019                    "2020-09-08T00:00:00Z",
1020                    "2020-09-08T00:00:00Z",
1021                    "2020-09-08T00:00:00Z",
1022                ],
1023            ),
1024            (
1025                vec![
1026                    "2020-09-08T00:00:00Z",
1027                    "2020-09-08T01:00:00Z",
1028                    "2020-09-08T02:00:00Z",
1029                    "2020-09-08T03:00:00Z",
1030                    "2020-09-08T04:00:00Z",
1031                ],
1032                None,
1033                vec![
1034                    "2020-09-08T00:00:00Z",
1035                    "2020-09-08T00:00:00Z",
1036                    "2020-09-08T00:00:00Z",
1037                    "2020-09-08T00:00:00Z",
1038                    "2020-09-08T00:00:00Z",
1039                ],
1040            ),
1041            (
1042                vec![
1043                    "2020-09-08T00:00:00Z",
1044                    "2020-09-08T01:00:00Z",
1045                    "2020-09-08T02:00:00Z",
1046                    "2020-09-08T03:00:00Z",
1047                    "2020-09-08T04:00:00Z",
1048                ],
1049                Some("-02".into()),
1050                vec![
1051                    "2020-09-07T02:00:00Z",
1052                    "2020-09-07T02:00:00Z",
1053                    "2020-09-08T02:00:00Z",
1054                    "2020-09-08T02:00:00Z",
1055                    "2020-09-08T02:00:00Z",
1056                ],
1057            ),
1058            (
1059                vec![
1060                    "2020-09-08T00:00:00+05",
1061                    "2020-09-08T01:00:00+05",
1062                    "2020-09-08T02:00:00+05",
1063                    "2020-09-08T03:00:00+05",
1064                    "2020-09-08T04:00:00+05",
1065                ],
1066                Some("+05".into()),
1067                vec![
1068                    "2020-09-08T00:00:00+05",
1069                    "2020-09-08T00:00:00+05",
1070                    "2020-09-08T00:00:00+05",
1071                    "2020-09-08T00:00:00+05",
1072                    "2020-09-08T00:00:00+05",
1073                ],
1074            ),
1075            (
1076                vec![
1077                    "2020-09-08T00:00:00+08",
1078                    "2020-09-08T01:00:00+08",
1079                    "2020-09-08T02:00:00+08",
1080                    "2020-09-08T03:00:00+08",
1081                    "2020-09-08T04:00:00+08",
1082                ],
1083                Some("+08".into()),
1084                vec![
1085                    "2020-09-08T00:00:00+08",
1086                    "2020-09-08T00:00:00+08",
1087                    "2020-09-08T00:00:00+08",
1088                    "2020-09-08T00:00:00+08",
1089                    "2020-09-08T00:00:00+08",
1090                ],
1091            ),
1092            (
1093                vec![
1094                    "2024-10-26T23:00:00Z",
1095                    "2024-10-27T00:00:00Z",
1096                    "2024-10-27T01:00:00Z",
1097                    "2024-10-27T02:00:00Z",
1098                ],
1099                Some("Europe/Berlin".into()),
1100                vec![
1101                    "2024-10-27T00:00:00+02",
1102                    "2024-10-27T00:00:00+02",
1103                    "2024-10-27T00:00:00+02",
1104                    "2024-10-27T00:00:00+02",
1105                ],
1106            ),
1107            (
1108                vec![
1109                    "2018-02-18T00:00:00Z",
1110                    "2018-02-18T01:00:00Z",
1111                    "2018-02-18T02:00:00Z",
1112                    "2018-02-18T03:00:00Z",
1113                    "2018-11-04T01:00:00Z",
1114                    "2018-11-04T02:00:00Z",
1115                    "2018-11-04T03:00:00Z",
1116                    "2018-11-04T04:00:00Z",
1117                ],
1118                Some("America/Sao_Paulo".into()),
1119                vec![
1120                    "2018-02-17T00:00:00-02",
1121                    "2018-02-17T00:00:00-02",
1122                    "2018-02-17T00:00:00-02",
1123                    "2018-02-18T00:00:00-03",
1124                    "2018-11-03T00:00:00-03",
1125                    "2018-11-03T00:00:00-03",
1126                    "2018-11-04T01:00:00-02",
1127                    "2018-11-04T01:00:00-02",
1128                ],
1129            ),
1130        ];
1131
1132        cases.iter().for_each(|(original, tz_opt, expected)| {
1133            let input = original
1134                .iter()
1135                .map(|s| Some(string_to_timestamp_nanos(s).unwrap()))
1136                .collect::<TimestampNanosecondArray>()
1137                .with_timezone_opt(tz_opt.clone());
1138            let right = expected
1139                .iter()
1140                .map(|s| Some(string_to_timestamp_nanos(s).unwrap()))
1141                .collect::<TimestampNanosecondArray>()
1142                .with_timezone_opt(tz_opt.clone());
1143            let batch_len = input.len();
1144            let arg_fields = vec![
1145                Field::new("a", DataType::Utf8, false).into(),
1146                Field::new("b", input.data_type().clone(), false).into(),
1147            ];
1148            let args = ScalarFunctionArgs {
1149                args: vec![
1150                    ColumnarValue::Scalar(ScalarValue::from("day")),
1151                    ColumnarValue::Array(Arc::new(input)),
1152                ],
1153                arg_fields,
1154                number_rows: batch_len,
1155                return_field: Field::new(
1156                    "f",
1157                    DataType::Timestamp(TimeUnit::Nanosecond, tz_opt.clone()),
1158                    true,
1159                )
1160                .into(),
1161                config_options: Arc::new(ConfigOptions::default()),
1162            };
1163            let result = DateTruncFunc::new().invoke_with_args(args).unwrap();
1164            if let ColumnarValue::Array(result) = result {
1165                assert_eq!(
1166                    result.data_type(),
1167                    &DataType::Timestamp(TimeUnit::Nanosecond, tz_opt.clone())
1168                );
1169                let left = as_primitive_array::<TimestampNanosecondType>(&result);
1170                assert_eq!(left, &right);
1171            } else {
1172                panic!("unexpected column type");
1173            }
1174        });
1175    }
1176
1177    #[test]
1178    fn test_date_trunc_hour_timezones() {
1179        let cases = [
1180            (
1181                vec![
1182                    "2020-09-08T00:30:00Z",
1183                    "2020-09-08T01:30:00Z",
1184                    "2020-09-08T02:30:00Z",
1185                    "2020-09-08T03:30:00Z",
1186                    "2020-09-08T04:30:00Z",
1187                ],
1188                Some("+00".into()),
1189                vec![
1190                    "2020-09-08T00:00:00Z",
1191                    "2020-09-08T01:00:00Z",
1192                    "2020-09-08T02:00:00Z",
1193                    "2020-09-08T03:00:00Z",
1194                    "2020-09-08T04:00:00Z",
1195                ],
1196            ),
1197            (
1198                vec![
1199                    "2020-09-08T00:30:00Z",
1200                    "2020-09-08T01:30:00Z",
1201                    "2020-09-08T02:30:00Z",
1202                    "2020-09-08T03:30:00Z",
1203                    "2020-09-08T04:30:00Z",
1204                ],
1205                None,
1206                vec![
1207                    "2020-09-08T00:00:00Z",
1208                    "2020-09-08T01:00:00Z",
1209                    "2020-09-08T02:00:00Z",
1210                    "2020-09-08T03:00:00Z",
1211                    "2020-09-08T04:00:00Z",
1212                ],
1213            ),
1214            (
1215                vec![
1216                    "2020-09-08T00:30:00Z",
1217                    "2020-09-08T01:30:00Z",
1218                    "2020-09-08T02:30:00Z",
1219                    "2020-09-08T03:30:00Z",
1220                    "2020-09-08T04:30:00Z",
1221                ],
1222                Some("-02".into()),
1223                vec![
1224                    "2020-09-08T00:00:00Z",
1225                    "2020-09-08T01:00:00Z",
1226                    "2020-09-08T02:00:00Z",
1227                    "2020-09-08T03:00:00Z",
1228                    "2020-09-08T04:00:00Z",
1229                ],
1230            ),
1231            (
1232                vec![
1233                    "2020-09-08T00:30:00+05",
1234                    "2020-09-08T01:30:00+05",
1235                    "2020-09-08T02:30:00+05",
1236                    "2020-09-08T03:30:00+05",
1237                    "2020-09-08T04:30:00+05",
1238                ],
1239                Some("+05".into()),
1240                vec![
1241                    "2020-09-08T00:00:00+05",
1242                    "2020-09-08T01:00:00+05",
1243                    "2020-09-08T02:00:00+05",
1244                    "2020-09-08T03:00:00+05",
1245                    "2020-09-08T04:00:00+05",
1246                ],
1247            ),
1248            (
1249                vec![
1250                    "2020-09-08T00:30:00+08",
1251                    "2020-09-08T01:30:00+08",
1252                    "2020-09-08T02:30:00+08",
1253                    "2020-09-08T03:30:00+08",
1254                    "2020-09-08T04:30:00+08",
1255                ],
1256                Some("+08".into()),
1257                vec![
1258                    "2020-09-08T00:00:00+08",
1259                    "2020-09-08T01:00:00+08",
1260                    "2020-09-08T02:00:00+08",
1261                    "2020-09-08T03:00:00+08",
1262                    "2020-09-08T04:00:00+08",
1263                ],
1264            ),
1265            (
1266                vec![
1267                    "2024-10-26T23:30:00Z",
1268                    "2024-10-27T00:30:00Z",
1269                    "2024-10-27T01:30:00Z",
1270                    "2024-10-27T02:30:00Z",
1271                ],
1272                Some("Europe/Berlin".into()),
1273                vec![
1274                    "2024-10-27T01:00:00+02",
1275                    "2024-10-27T02:00:00+02",
1276                    "2024-10-27T02:00:00+01",
1277                    "2024-10-27T03:00:00+01",
1278                ],
1279            ),
1280            (
1281                vec![
1282                    "2018-02-18T00:30:00Z",
1283                    "2018-02-18T01:30:00Z",
1284                    "2018-02-18T02:30:00Z",
1285                    "2018-02-18T03:30:00Z",
1286                    "2018-11-04T01:00:00Z",
1287                    "2018-11-04T02:00:00Z",
1288                    "2018-11-04T03:00:00Z",
1289                    "2018-11-04T04:00:00Z",
1290                ],
1291                Some("America/Sao_Paulo".into()),
1292                vec![
1293                    "2018-02-17T22:00:00-02",
1294                    "2018-02-17T23:00:00-02",
1295                    "2018-02-17T23:00:00-03",
1296                    "2018-02-18T00:00:00-03",
1297                    "2018-11-03T22:00:00-03",
1298                    "2018-11-03T23:00:00-03",
1299                    "2018-11-04T01:00:00-02",
1300                    "2018-11-04T02:00:00-02",
1301                ],
1302            ),
1303            (
1304                vec![
1305                    "2024-10-26T23:30:00Z",
1306                    "2024-10-27T00:30:00Z",
1307                    "2024-10-27T01:30:00Z",
1308                    "2024-10-27T02:30:00Z",
1309                ],
1310                Some("Asia/Kathmandu".into()), // UTC+5:45
1311                vec![
1312                    "2024-10-27T05:00:00+05:45",
1313                    "2024-10-27T06:00:00+05:45",
1314                    "2024-10-27T07:00:00+05:45",
1315                    "2024-10-27T08:00:00+05:45",
1316                ],
1317            ),
1318        ];
1319
1320        cases.iter().for_each(|(original, tz_opt, expected)| {
1321            let input = original
1322                .iter()
1323                .map(|s| Some(string_to_timestamp_nanos(s).unwrap()))
1324                .collect::<TimestampNanosecondArray>()
1325                .with_timezone_opt(tz_opt.clone());
1326            let right = expected
1327                .iter()
1328                .map(|s| Some(string_to_timestamp_nanos(s).unwrap()))
1329                .collect::<TimestampNanosecondArray>()
1330                .with_timezone_opt(tz_opt.clone());
1331            let batch_len = input.len();
1332            let arg_fields = vec![
1333                Field::new("a", DataType::Utf8, false).into(),
1334                Field::new("b", input.data_type().clone(), false).into(),
1335            ];
1336            let args = ScalarFunctionArgs {
1337                args: vec![
1338                    ColumnarValue::Scalar(ScalarValue::from("hour")),
1339                    ColumnarValue::Array(Arc::new(input)),
1340                ],
1341                arg_fields,
1342                number_rows: batch_len,
1343                return_field: Field::new(
1344                    "f",
1345                    DataType::Timestamp(TimeUnit::Nanosecond, tz_opt.clone()),
1346                    true,
1347                )
1348                .into(),
1349                config_options: Arc::new(ConfigOptions::default()),
1350            };
1351            let result = DateTruncFunc::new().invoke_with_args(args).unwrap();
1352            if let ColumnarValue::Array(result) = result {
1353                assert_eq!(
1354                    result.data_type(),
1355                    &DataType::Timestamp(TimeUnit::Nanosecond, tz_opt.clone())
1356                );
1357                let left = as_primitive_array::<TimestampNanosecondType>(&result);
1358                assert_eq!(left, &right);
1359            } else {
1360                panic!("unexpected column type");
1361            }
1362        });
1363    }
1364
1365    #[test]
1366    fn test_date_trunc_fine_granularity_timezones() {
1367        let cases = [
1368            // Test "second" granularity
1369            (
1370                vec![
1371                    "2020-09-08T13:42:29.190855Z",
1372                    "2020-09-08T13:42:30.500000Z",
1373                    "2020-09-08T13:42:31.999999Z",
1374                ],
1375                Some("+00".into()),
1376                "second",
1377                vec![
1378                    "2020-09-08T13:42:29.000000Z",
1379                    "2020-09-08T13:42:30.000000Z",
1380                    "2020-09-08T13:42:31.000000Z",
1381                ],
1382            ),
1383            (
1384                vec![
1385                    "2020-09-08T13:42:29.190855+05",
1386                    "2020-09-08T13:42:30.500000+05",
1387                    "2020-09-08T13:42:31.999999+05",
1388                ],
1389                Some("+05".into()),
1390                "second",
1391                vec![
1392                    "2020-09-08T13:42:29.000000+05",
1393                    "2020-09-08T13:42:30.000000+05",
1394                    "2020-09-08T13:42:31.000000+05",
1395                ],
1396            ),
1397            (
1398                vec![
1399                    "2020-09-08T13:42:29.190855Z",
1400                    "2020-09-08T13:42:30.500000Z",
1401                    "2020-09-08T13:42:31.999999Z",
1402                ],
1403                Some("Europe/Berlin".into()),
1404                "second",
1405                vec![
1406                    "2020-09-08T13:42:29.000000Z",
1407                    "2020-09-08T13:42:30.000000Z",
1408                    "2020-09-08T13:42:31.000000Z",
1409                ],
1410            ),
1411            // Test "minute" granularity
1412            (
1413                vec![
1414                    "2020-09-08T13:42:29.190855Z",
1415                    "2020-09-08T13:43:30.500000Z",
1416                    "2020-09-08T13:44:31.999999Z",
1417                ],
1418                Some("+00".into()),
1419                "minute",
1420                vec![
1421                    "2020-09-08T13:42:00.000000Z",
1422                    "2020-09-08T13:43:00.000000Z",
1423                    "2020-09-08T13:44:00.000000Z",
1424                ],
1425            ),
1426            (
1427                vec![
1428                    "2020-09-08T13:42:29.190855+08",
1429                    "2020-09-08T13:43:30.500000+08",
1430                    "2020-09-08T13:44:31.999999+08",
1431                ],
1432                Some("+08".into()),
1433                "minute",
1434                vec![
1435                    "2020-09-08T13:42:00.000000+08",
1436                    "2020-09-08T13:43:00.000000+08",
1437                    "2020-09-08T13:44:00.000000+08",
1438                ],
1439            ),
1440            (
1441                vec![
1442                    "2020-09-08T13:42:29.190855Z",
1443                    "2020-09-08T13:43:30.500000Z",
1444                    "2020-09-08T13:44:31.999999Z",
1445                ],
1446                Some("America/Sao_Paulo".into()),
1447                "minute",
1448                vec![
1449                    "2020-09-08T13:42:00.000000Z",
1450                    "2020-09-08T13:43:00.000000Z",
1451                    "2020-09-08T13:44:00.000000Z",
1452                ],
1453            ),
1454            // Test with None (no timezone)
1455            (
1456                vec![
1457                    "2020-09-08T13:42:29.190855Z",
1458                    "2020-09-08T13:43:30.500000Z",
1459                    "2020-09-08T13:44:31.999999Z",
1460                ],
1461                None,
1462                "minute",
1463                vec![
1464                    "2020-09-08T13:42:00.000000Z",
1465                    "2020-09-08T13:43:00.000000Z",
1466                    "2020-09-08T13:44:00.000000Z",
1467                ],
1468            ),
1469            // Test millisecond granularity
1470            (
1471                vec![
1472                    "2020-09-08T13:42:29.190855Z",
1473                    "2020-09-08T13:42:29.191999Z",
1474                    "2020-09-08T13:42:29.192500Z",
1475                ],
1476                Some("Asia/Kolkata".into()),
1477                "millisecond",
1478                vec![
1479                    "2020-09-08T19:12:29.190000+05:30",
1480                    "2020-09-08T19:12:29.191000+05:30",
1481                    "2020-09-08T19:12:29.192000+05:30",
1482                ],
1483            ),
1484        ];
1485
1486        cases
1487            .iter()
1488            .for_each(|(original, tz_opt, granularity, expected)| {
1489                let input = original
1490                    .iter()
1491                    .map(|s| Some(string_to_timestamp_nanos(s).unwrap()))
1492                    .collect::<TimestampNanosecondArray>()
1493                    .with_timezone_opt(tz_opt.clone());
1494                let right = expected
1495                    .iter()
1496                    .map(|s| Some(string_to_timestamp_nanos(s).unwrap()))
1497                    .collect::<TimestampNanosecondArray>()
1498                    .with_timezone_opt(tz_opt.clone());
1499                let batch_len = input.len();
1500                let arg_fields = vec![
1501                    Field::new("a", DataType::Utf8, false).into(),
1502                    Field::new("b", input.data_type().clone(), false).into(),
1503                ];
1504                let args = ScalarFunctionArgs {
1505                    args: vec![
1506                        ColumnarValue::Scalar(ScalarValue::from(*granularity)),
1507                        ColumnarValue::Array(Arc::new(input)),
1508                    ],
1509                    arg_fields,
1510                    number_rows: batch_len,
1511                    return_field: Field::new(
1512                        "f",
1513                        DataType::Timestamp(TimeUnit::Nanosecond, tz_opt.clone()),
1514                        true,
1515                    )
1516                    .into(),
1517                    config_options: Arc::new(ConfigOptions::default()),
1518                };
1519                let result = DateTruncFunc::new().invoke_with_args(args).unwrap();
1520                if let ColumnarValue::Array(result) = result {
1521                    assert_eq!(
1522                        result.data_type(),
1523                        &DataType::Timestamp(TimeUnit::Nanosecond, tz_opt.clone()),
1524                        "Failed for granularity: {granularity}, timezone: {tz_opt:?}"
1525                    );
1526                    let left = as_primitive_array::<TimestampNanosecondType>(&result);
1527                    assert_eq!(
1528                        left, &right,
1529                        "Failed for granularity: {granularity}, timezone: {tz_opt:?}"
1530                    );
1531                } else {
1532                    panic!("unexpected column type");
1533                }
1534            });
1535    }
1536}