Skip to main content

minarrow/enums/collections/
temporal_array.rs

1// Copyright 2025 Peter Garfield Bower
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! # **TemporalArray Module** - *High-Level DateTimes Array Type for Unified Signature Dispatch*
16//!
17//! TemporalArray unifies all datetime-based arrays into a single enum for
18//! standardised temporal operations.
19//!   
20//! ## Features:
21//! - direct variant access
22//! - zero-cost casts when the type is known
23//! - lossless conversions between 32-bit and 64-bit datetime types.  
24//! - simplifies function signatures by accepting `impl Into<TemporalArray>`
25//! - centralises dispatch
26//! - preserves SIMD-aligned buffers across all temporal variants.
27
28use std::{
29    fmt::{Display, Formatter},
30    sync::Arc,
31};
32
33use crate::{Bitmask, DatetimeArray, MaskedArray, TimeUnit};
34use crate::{
35    enums::{error::MinarrowError, shape_dim::ShapeDim},
36    traits::{concatenate::Concatenate, shape::Shape},
37};
38
39/// Temporal Array
40///
41/// Unified datetime array container
42///
43/// ## Purpose
44/// Exists to unify datetime operations,
45/// simplify API's and streamline user ergonomics.
46///
47/// ## Usage:
48/// - It is accessible from `Array` using `.dt()`,
49/// and provides typed variant access via for e.g.,
50/// `.dt32()`, so one can drill down to the required
51/// granularity via `myarr.dt().dt32()`
52/// - This streamlines function implementations *(at least for the `NumericArray`
53/// case where this pattern is the most useful)*,
54/// and, despite the additional `enum` layer,
55/// matching lanes in many real-world scenarios.
56/// This is because one can for e.g., unify a
57/// function signature with `impl Into<TemporalArray>`,
58/// and all of the subtypes, plus `Array` and `TemporalArray`,
59/// all qualify.
60/// - Additionally, you can then use one `Temporal` implementation
61/// on the enum dispatch arm for all `Temporal` variants, or,
62/// in many cases, for the entire datetime arm when they are the same.
63///
64/// ### Handling Times, Durations, etc.
65/// We use one Physical type to hold all datetime variants,
66/// i.e., the *Apache Arrow* types `DATE32`, `TIME32`, `DURATION` etc.,
67/// and the Logical type is stored on the `Field` as metadata, given they
68/// otherwise have the same underlying data representation. To treat
69/// them differently in API usage, you can use the `TimeUnit` and `IntervalUnit`,
70/// along with the `ArrowType` that is stored on the `Field` in `Minarrow`,
71/// and match on these for any desired behaviour. The `Field` is packaged together
72/// with `Array` *(which then drill-down accesses `TemporalArray` on the fly, or
73/// in dispatch routing scenarios)*.
74///
75/// ### Typecasting behaviour
76/// - If the enum already holds the given type *(which should be known at compile-time)*,
77/// then using accessors like `.dt32()` is zero-cost, as it transfers ownership.
78/// - If you want to keep the original, of course use `.clone()` beforehand.
79/// - If you use an accessor to a different base type, e.g., `.dt64()` when it's a
80/// `.dt32()` already in the enum, it will convert it. Therefore, be mindful
81/// of performance when this occurs.
82#[repr(C, align(64))]
83#[derive(PartialEq, Clone, Debug, Default)]
84pub enum TemporalArray {
85    // The datetimes are chunked by their common memory layout rather than logical type
86    // These can be casted to the relevant Arrow type at the FFI layer as needed
87    Datetime32(Arc<DatetimeArray<i32>>), // DATE32, TIME32, DURATION(s), DURATION(ms) (32-bit)
88    // DATE64, TIMESTAMP (ms/us/ns), DURATION (ms/us/ns), TIME64, DURATION(us), DURATION(ns)
89    Datetime64(Arc<DatetimeArray<i64>>),
90    #[default]
91    Null, // Default Marker for mem::take
92}
93
94impl TemporalArray {
95    /// Returns the logical length of the temporal array.
96    #[inline]
97    pub fn len(&self) -> usize {
98        match self {
99            TemporalArray::Datetime32(arr) => arr.len(),
100            TemporalArray::Datetime64(arr) => arr.len(),
101            TemporalArray::Null => 0,
102        }
103    }
104
105    /// The time unit the samples are measured in i.e. seconds, milliseconds,
106    /// microseconds, nanoseconds, or days.
107    ///
108    /// Returns `None` for the `Null` placeholder variant, which carries no
109    /// datetime payload and therefore no unit.
110    #[inline]
111    pub fn time_unit(&self) -> Option<TimeUnit> {
112        match self {
113            TemporalArray::Datetime32(arr) => Some(arr.time_unit),
114            TemporalArray::Datetime64(arr) => Some(arr.time_unit),
115            TemporalArray::Null => None,
116        }
117    }
118
119    /// Removes the rows in `[start, end)`, shifting later rows left.
120    /// A shared inner array is cloned first i.e. copy-on-write.
121    ///
122    /// # Panics
123    /// Panics if `start > end` or `end > len`.
124    pub fn delete_range(&mut self, start: usize, end: usize) {
125        match self {
126            TemporalArray::Datetime32(arr) => arr.delete_range(start, end),
127            TemporalArray::Datetime64(arr) => arr.delete_range(start, end),
128            TemporalArray::Null => {
129                assert!(
130                    start == 0 && end == 0,
131                    "TemporalArray::Null: delete_range out of bounds"
132                );
133            }
134        }
135    }
136
137    /// Returns the underlying null mask, if any.
138    #[inline]
139    pub fn null_mask(&self) -> Option<&Bitmask> {
140        match self {
141            TemporalArray::Datetime32(arr) => arr.null_mask.as_ref(),
142            TemporalArray::Datetime64(arr) => arr.null_mask.as_ref(),
143            TemporalArray::Null => None,
144        }
145    }
146
147    /// Returns true when the variant holds at least one null.
148    ///
149    /// Delegates to each inner array's `MaskedArray::has_nulls`; `Null` is
150    /// treated as empty (no elements means no nulls).
151    #[inline]
152    pub fn has_nulls(&self) -> bool {
153        match self {
154            TemporalArray::Datetime32(arr) => arr.has_nulls(),
155            TemporalArray::Datetime64(arr) => arr.has_nulls(),
156            TemporalArray::Null => false,
157        }
158    }
159
160    /// Appends all values (and null mask if present) from `other` into `self`.
161    ///
162    /// Panics if the two arrays are of different variants or incompatible types.
163    ///
164    /// This function uses copy-on-write semantics for arrays wrapped in `Arc`.
165    /// If `self` is the only owner of its data, appends are performed in place without copying.
166    /// If the array data is shared (`Arc` reference count > 1), the data is first cloned
167    /// (so the mutation does not affect other owners), and the append is then performed on the unique copy.
168    ///
169    /// This ensures that calling `append_array` never mutates data referenced elsewhere,
170    /// but also avoids unnecessary cloning when the data is uniquely owned.
171    pub fn append_array(&mut self, other: &Self) {
172        match (self, other) {
173            (TemporalArray::Datetime32(a), TemporalArray::Datetime32(b)) => {
174                Arc::make_mut(a).append_array(b)
175            }
176            (TemporalArray::Datetime64(a), TemporalArray::Datetime64(b)) => {
177                Arc::make_mut(a).append_array(b)
178            }
179            (TemporalArray::Null, TemporalArray::Null) => (),
180            (lhs, rhs) => panic!("Cannot append {:?} into {:?}", rhs, lhs),
181        }
182    }
183
184    pub fn append_range(
185        &mut self,
186        other: &Self,
187        offset: usize,
188        len: usize,
189    ) -> Result<(), MinarrowError> {
190        match (self, other) {
191            (TemporalArray::Datetime32(a), TemporalArray::Datetime32(b)) => {
192                Arc::make_mut(a).append_range(b, offset, len)
193            }
194            (TemporalArray::Datetime64(a), TemporalArray::Datetime64(b)) => {
195                Arc::make_mut(a).append_range(b, offset, len)
196            }
197            (TemporalArray::Null, TemporalArray::Null) => Ok(()),
198            (lhs, rhs) => Err(MinarrowError::TypeError {
199                from: "TemporalArray",
200                to: "TemporalArray",
201                message: Some(format!("Cannot append_range {:?} into {:?}", rhs, lhs)),
202            }),
203        }
204    }
205
206    /// Inserts all values (and null mask if present) from `other` into `self` at the specified index.
207    ///
208    /// This is an **O(n)** operation.
209    ///
210    /// Returns an error if the two arrays are of different variants or incompatible types,
211    /// or if the index is out of bounds.
212    ///
213    /// This function uses copy-on-write semantics for arrays wrapped in `Arc`.
214    pub fn insert_rows(&mut self, index: usize, other: &Self) -> Result<(), MinarrowError> {
215        match (self, other) {
216            (TemporalArray::Datetime32(a), TemporalArray::Datetime32(b)) => {
217                Arc::make_mut(a).insert_rows(index, b)
218            }
219            (TemporalArray::Datetime64(a), TemporalArray::Datetime64(b)) => {
220                Arc::make_mut(a).insert_rows(index, b)
221            }
222            (TemporalArray::Null, TemporalArray::Null) => Ok(()),
223            (lhs, rhs) => Err(MinarrowError::TypeError {
224                from: "TemporalArray",
225                to: "TemporalArray",
226                message: Some(format!(
227                    "Cannot insert {} into {}: incompatible types",
228                    temporal_variant_name(rhs),
229                    temporal_variant_name(lhs)
230                )),
231            }),
232        }
233    }
234
235    /// Splits the TemporalArray at the specified index, consuming self and returning two arrays.
236    pub fn split(self, index: usize) -> Result<(Self, Self), MinarrowError> {
237        use std::sync::Arc;
238
239        match self {
240            TemporalArray::Datetime32(a) => {
241                let (left, right) = Arc::try_unwrap(a)
242                    .unwrap_or_else(|arc| (*arc).clone())
243                    .split(index)?;
244                Ok((
245                    TemporalArray::Datetime32(Arc::new(left)),
246                    TemporalArray::Datetime32(Arc::new(right)),
247                ))
248            }
249            TemporalArray::Datetime64(a) => {
250                let (left, right) = Arc::try_unwrap(a)
251                    .unwrap_or_else(|arc| (*arc).clone())
252                    .split(index)?;
253                Ok((
254                    TemporalArray::Datetime64(Arc::new(left)),
255                    TemporalArray::Datetime64(Arc::new(right)),
256                ))
257            }
258            TemporalArray::Null => Err(MinarrowError::IndexError(
259                "Cannot split Null array".to_string(),
260            )),
261        }
262    }
263
264    /// Returns the inner array as `Arc<DatetimeArray<i32>>`, casting when the variant differs.
265    ///
266    /// - The matching variant returns as a shared handle without copying data.
267    /// - Panics on failure. Consider the try variant for a safe alternative.
268    #[inline]
269    pub fn dt32(&self) -> Arc<DatetimeArray<i32>> {
270        self.try_dt32().unwrap()
271    }
272
273    /// Returns an Arc<DatetimeArray<i32>> (casting if needed).
274    ///
275    /// The matching variant returns as a shared handle without copying data.
276    pub fn try_dt32(&self) -> Result<Arc<DatetimeArray<i32>>, MinarrowError> {
277        match self {
278            TemporalArray::Datetime32(arr) => Ok(arr.clone()),
279            TemporalArray::Datetime64(arr) => Ok(Arc::new(DatetimeArray::<i32>::try_from(&**arr)?)),
280            TemporalArray::Null => Err(MinarrowError::NullError { message: None }),
281        }
282    }
283
284    /// Returns the inner array as `Arc<DatetimeArray<i64>>`, casting when the variant differs.
285    ///
286    /// - The matching variant returns as a shared handle without copying data.
287    /// - Panics on failure. Consider the try variant for a safe alternative.
288    #[inline]
289    pub fn dt64(&self) -> Arc<DatetimeArray<i64>> {
290        self.try_dt64().unwrap()
291    }
292
293    /// Returns an Arc<DatetimeArray<i64>> (casting if needed).
294    ///
295    /// The matching variant returns as a shared handle without copying data.
296    pub fn try_dt64(&self) -> Result<Arc<DatetimeArray<i64>>, MinarrowError> {
297        match self {
298            TemporalArray::Datetime64(arr) => Ok(arr.clone()),
299            TemporalArray::Datetime32(arr) => Ok(Arc::new(DatetimeArray::<i64>::from(&**arr))),
300            TemporalArray::Null => Err(MinarrowError::NullError { message: None }),
301        }
302    }
303}
304
305impl Shape for TemporalArray {
306    fn shape(&self) -> ShapeDim {
307        ShapeDim::Rank1(self.len())
308    }
309}
310
311impl Concatenate for TemporalArray {
312    fn concat(self, other: Self) -> Result<Self, MinarrowError> {
313        match (self, other) {
314            (TemporalArray::Datetime32(a), TemporalArray::Datetime32(b)) => {
315                let a = Arc::try_unwrap(a).unwrap_or_else(|arc| (*arc).clone());
316                let b = Arc::try_unwrap(b).unwrap_or_else(|arc| (*arc).clone());
317                Ok(TemporalArray::Datetime32(Arc::new(a.concat(b)?)))
318            }
319            (TemporalArray::Datetime64(a), TemporalArray::Datetime64(b)) => {
320                let a = Arc::try_unwrap(a).unwrap_or_else(|arc| (*arc).clone());
321                let b = Arc::try_unwrap(b).unwrap_or_else(|arc| (*arc).clone());
322                Ok(TemporalArray::Datetime64(Arc::new(a.concat(b)?)))
323            }
324            (TemporalArray::Null, TemporalArray::Null) => Ok(TemporalArray::Null),
325            (lhs, rhs) => Err(MinarrowError::IncompatibleTypeError {
326                from: "TemporalArray",
327                to: "TemporalArray",
328                message: Some(format!(
329                    "Cannot concatenate mismatched TemporalArray variants: {:?} and {:?}",
330                    temporal_variant_name(&lhs),
331                    temporal_variant_name(&rhs)
332                )),
333            }),
334        }
335    }
336}
337
338#[cfg(feature = "datetime_ops")]
339use crate::DatetimeOps;
340
341#[cfg(feature = "datetime_ops")]
342use crate::enums::time_units::TimePeriod;
343
344#[cfg(feature = "datetime_ops")]
345use time::Duration;
346
347#[cfg(feature = "datetime_ops")]
348use crate::structs::variants::{boolean::BooleanArray, integer::IntegerArray};
349
350#[cfg(feature = "datetime_ops")]
351impl DatetimeOps for TemporalArray {
352    // Component Extraction - delegate to inner variant, return directly
353
354    fn year(&self) -> IntegerArray<i32> {
355        match self {
356            TemporalArray::Datetime32(arr) => arr.year(),
357            TemporalArray::Datetime64(arr) => arr.year(),
358            TemporalArray::Null => IntegerArray::default(),
359        }
360    }
361
362    fn month(&self) -> IntegerArray<i32> {
363        match self {
364            TemporalArray::Datetime32(arr) => arr.month(),
365            TemporalArray::Datetime64(arr) => arr.month(),
366            TemporalArray::Null => IntegerArray::default(),
367        }
368    }
369
370    fn day(&self) -> IntegerArray<i32> {
371        match self {
372            TemporalArray::Datetime32(arr) => arr.day(),
373            TemporalArray::Datetime64(arr) => arr.day(),
374            TemporalArray::Null => IntegerArray::default(),
375        }
376    }
377
378    fn hour(&self) -> IntegerArray<i32> {
379        match self {
380            TemporalArray::Datetime32(arr) => arr.hour(),
381            TemporalArray::Datetime64(arr) => arr.hour(),
382            TemporalArray::Null => IntegerArray::default(),
383        }
384    }
385
386    fn minute(&self) -> IntegerArray<i32> {
387        match self {
388            TemporalArray::Datetime32(arr) => arr.minute(),
389            TemporalArray::Datetime64(arr) => arr.minute(),
390            TemporalArray::Null => IntegerArray::default(),
391        }
392    }
393
394    fn second(&self) -> IntegerArray<i32> {
395        match self {
396            TemporalArray::Datetime32(arr) => arr.second(),
397            TemporalArray::Datetime64(arr) => arr.second(),
398            TemporalArray::Null => IntegerArray::default(),
399        }
400    }
401
402    fn weekday(&self) -> IntegerArray<i32> {
403        match self {
404            TemporalArray::Datetime32(arr) => arr.weekday(),
405            TemporalArray::Datetime64(arr) => arr.weekday(),
406            TemporalArray::Null => IntegerArray::default(),
407        }
408    }
409
410    fn day_of_year(&self) -> IntegerArray<i32> {
411        match self {
412            TemporalArray::Datetime32(arr) => arr.day_of_year(),
413            TemporalArray::Datetime64(arr) => arr.day_of_year(),
414            TemporalArray::Null => IntegerArray::default(),
415        }
416    }
417
418    fn iso_week(&self) -> IntegerArray<i32> {
419        match self {
420            TemporalArray::Datetime32(arr) => arr.iso_week(),
421            TemporalArray::Datetime64(arr) => arr.iso_week(),
422            TemporalArray::Null => IntegerArray::default(),
423        }
424    }
425
426    fn quarter(&self) -> IntegerArray<i32> {
427        match self {
428            TemporalArray::Datetime32(arr) => arr.quarter(),
429            TemporalArray::Datetime64(arr) => arr.quarter(),
430            TemporalArray::Null => IntegerArray::default(),
431        }
432    }
433
434    fn week_of_year(&self) -> IntegerArray<i32> {
435        match self {
436            TemporalArray::Datetime32(arr) => arr.week_of_year(),
437            TemporalArray::Datetime64(arr) => arr.week_of_year(),
438            TemporalArray::Null => IntegerArray::default(),
439        }
440    }
441
442    fn is_leap_year(&self) -> BooleanArray<()> {
443        match self {
444            TemporalArray::Datetime32(arr) => arr.is_leap_year(),
445            TemporalArray::Datetime64(arr) => arr.is_leap_year(),
446            TemporalArray::Null => BooleanArray::default(),
447        }
448    }
449
450    // Arithmetic - delegate, wrap result back into enum variant
451
452    fn add_duration(&self, duration: Duration) -> Result<Self, MinarrowError> {
453        match self {
454            TemporalArray::Datetime32(arr) => Ok(TemporalArray::Datetime32(Arc::new(
455                arr.add_duration(duration)?,
456            ))),
457            TemporalArray::Datetime64(arr) => Ok(TemporalArray::Datetime64(Arc::new(
458                arr.add_duration(duration)?,
459            ))),
460            TemporalArray::Null => Err(MinarrowError::NullError { message: None }),
461        }
462    }
463
464    fn sub_duration(&self, duration: Duration) -> Result<Self, MinarrowError> {
465        match self {
466            TemporalArray::Datetime32(arr) => Ok(TemporalArray::Datetime32(Arc::new(
467                arr.sub_duration(duration)?,
468            ))),
469            TemporalArray::Datetime64(arr) => Ok(TemporalArray::Datetime64(Arc::new(
470                arr.sub_duration(duration)?,
471            ))),
472            TemporalArray::Null => Err(MinarrowError::NullError { message: None }),
473        }
474    }
475
476    fn add_days(&self, days: i64) -> Result<Self, MinarrowError> {
477        match self {
478            TemporalArray::Datetime32(arr) => {
479                Ok(TemporalArray::Datetime32(Arc::new(arr.add_days(days)?)))
480            }
481            TemporalArray::Datetime64(arr) => {
482                Ok(TemporalArray::Datetime64(Arc::new(arr.add_days(days)?)))
483            }
484            TemporalArray::Null => Err(MinarrowError::NullError { message: None }),
485        }
486    }
487
488    fn add_months(&self, months: i32) -> Result<Self, MinarrowError> {
489        match self {
490            TemporalArray::Datetime32(arr) => {
491                Ok(TemporalArray::Datetime32(Arc::new(arr.add_months(months)?)))
492            }
493            TemporalArray::Datetime64(arr) => {
494                Ok(TemporalArray::Datetime64(Arc::new(arr.add_months(months)?)))
495            }
496            TemporalArray::Null => Err(MinarrowError::NullError { message: None }),
497        }
498    }
499
500    fn add_years(&self, years: i32) -> Result<Self, MinarrowError> {
501        match self {
502            TemporalArray::Datetime32(arr) => {
503                Ok(TemporalArray::Datetime32(Arc::new(arr.add_years(years)?)))
504            }
505            TemporalArray::Datetime64(arr) => {
506                Ok(TemporalArray::Datetime64(Arc::new(arr.add_years(years)?)))
507            }
508            TemporalArray::Null => Err(MinarrowError::NullError { message: None }),
509        }
510    }
511
512    // Comparison - match on (self, other) tuple, verify same variant
513
514    fn diff(&self, other: &Self, unit: TimeUnit) -> Result<IntegerArray<i64>, MinarrowError> {
515        match (self, other) {
516            (TemporalArray::Datetime32(a), TemporalArray::Datetime32(b)) => a.diff(b, unit),
517            (TemporalArray::Datetime64(a), TemporalArray::Datetime64(b)) => a.diff(b, unit),
518            (TemporalArray::Null, _) | (_, TemporalArray::Null) => {
519                Err(MinarrowError::NullError { message: None })
520            }
521            _ => Err(MinarrowError::TypeError {
522                from: "TemporalArray",
523                to: "TemporalArray",
524                message: Some("Mismatched temporal variants".to_string()),
525            }),
526        }
527    }
528
529    fn abs_diff(&self, other: &Self, unit: TimeUnit) -> Result<IntegerArray<i64>, MinarrowError> {
530        match (self, other) {
531            (TemporalArray::Datetime32(a), TemporalArray::Datetime32(b)) => a.abs_diff(b, unit),
532            (TemporalArray::Datetime64(a), TemporalArray::Datetime64(b)) => a.abs_diff(b, unit),
533            (TemporalArray::Null, _) | (_, TemporalArray::Null) => {
534                Err(MinarrowError::NullError { message: None })
535            }
536            _ => Err(MinarrowError::TypeError {
537                from: "TemporalArray",
538                to: "TemporalArray",
539                message: Some("Mismatched temporal variants".to_string()),
540            }),
541        }
542    }
543
544    fn is_before(&self, other: &Self) -> Result<BooleanArray<()>, MinarrowError> {
545        match (self, other) {
546            (TemporalArray::Datetime32(a), TemporalArray::Datetime32(b)) => a.is_before(b),
547            (TemporalArray::Datetime64(a), TemporalArray::Datetime64(b)) => a.is_before(b),
548            (TemporalArray::Null, _) | (_, TemporalArray::Null) => {
549                Err(MinarrowError::NullError { message: None })
550            }
551            _ => Err(MinarrowError::TypeError {
552                from: "TemporalArray",
553                to: "TemporalArray",
554                message: Some("Mismatched temporal variants".to_string()),
555            }),
556        }
557    }
558
559    fn is_after(&self, other: &Self) -> Result<BooleanArray<()>, MinarrowError> {
560        match (self, other) {
561            (TemporalArray::Datetime32(a), TemporalArray::Datetime32(b)) => a.is_after(b),
562            (TemporalArray::Datetime64(a), TemporalArray::Datetime64(b)) => a.is_after(b),
563            (TemporalArray::Null, _) | (_, TemporalArray::Null) => {
564                Err(MinarrowError::NullError { message: None })
565            }
566            _ => Err(MinarrowError::TypeError {
567                from: "TemporalArray",
568                to: "TemporalArray",
569                message: Some("Mismatched temporal variants".to_string()),
570            }),
571        }
572    }
573
574    fn between(&self, start: &Self, end: &Self) -> Result<BooleanArray<()>, MinarrowError> {
575        match (self, start, end) {
576            (
577                TemporalArray::Datetime32(a),
578                TemporalArray::Datetime32(s),
579                TemporalArray::Datetime32(e),
580            ) => a.between(s, e),
581            (
582                TemporalArray::Datetime64(a),
583                TemporalArray::Datetime64(s),
584                TemporalArray::Datetime64(e),
585            ) => a.between(s, e),
586            (TemporalArray::Null, _, _)
587            | (_, TemporalArray::Null, _)
588            | (_, _, TemporalArray::Null) => Err(MinarrowError::NullError { message: None }),
589            _ => Err(MinarrowError::TypeError {
590                from: "TemporalArray",
591                to: "TemporalArray",
592                message: Some("Mismatched temporal variants".to_string()),
593            }),
594        }
595    }
596
597    // Truncation - delegate, wrap result back into enum variant
598
599    fn truncate<P: Into<TimePeriod>>(&self, period: P) -> Self {
600        let period = period.into();
601        match self {
602            TemporalArray::Datetime32(arr) => {
603                TemporalArray::Datetime32(Arc::new(arr.truncate(period)))
604            }
605            TemporalArray::Datetime64(arr) => {
606                TemporalArray::Datetime64(Arc::new(arr.truncate(period)))
607            }
608            TemporalArray::Null => TemporalArray::Null,
609        }
610    }
611
612    fn us(&self) -> Self {
613        match self {
614            TemporalArray::Datetime32(arr) => TemporalArray::Datetime32(Arc::new(arr.us())),
615            TemporalArray::Datetime64(arr) => TemporalArray::Datetime64(Arc::new(arr.us())),
616            TemporalArray::Null => TemporalArray::Null,
617        }
618    }
619
620    fn ms(&self) -> Self {
621        match self {
622            TemporalArray::Datetime32(arr) => TemporalArray::Datetime32(Arc::new(arr.ms())),
623            TemporalArray::Datetime64(arr) => TemporalArray::Datetime64(Arc::new(arr.ms())),
624            TemporalArray::Null => TemporalArray::Null,
625        }
626    }
627
628    fn sec(&self) -> Self {
629        match self {
630            TemporalArray::Datetime32(arr) => TemporalArray::Datetime32(Arc::new(arr.sec())),
631            TemporalArray::Datetime64(arr) => TemporalArray::Datetime64(Arc::new(arr.sec())),
632            TemporalArray::Null => TemporalArray::Null,
633        }
634    }
635
636    fn min(&self) -> Self {
637        match self {
638            TemporalArray::Datetime32(arr) => TemporalArray::Datetime32(Arc::new(arr.min())),
639            TemporalArray::Datetime64(arr) => TemporalArray::Datetime64(Arc::new(arr.min())),
640            TemporalArray::Null => TemporalArray::Null,
641        }
642    }
643
644    fn hr(&self) -> Self {
645        match self {
646            TemporalArray::Datetime32(arr) => TemporalArray::Datetime32(Arc::new(arr.hr())),
647            TemporalArray::Datetime64(arr) => TemporalArray::Datetime64(Arc::new(arr.hr())),
648            TemporalArray::Null => TemporalArray::Null,
649        }
650    }
651
652    fn week(&self) -> Self {
653        match self {
654            TemporalArray::Datetime32(arr) => TemporalArray::Datetime32(Arc::new(arr.week())),
655            TemporalArray::Datetime64(arr) => TemporalArray::Datetime64(Arc::new(arr.week())),
656            TemporalArray::Null => TemporalArray::Null,
657        }
658    }
659
660    // Type Casting
661
662    fn cast_time_unit(&self, new_unit: TimeUnit) -> Result<Self, MinarrowError> {
663        match self {
664            TemporalArray::Datetime32(arr) => Ok(TemporalArray::Datetime32(Arc::new(
665                arr.cast_time_unit(new_unit)?,
666            ))),
667            TemporalArray::Datetime64(arr) => Ok(TemporalArray::Datetime64(Arc::new(
668                arr.cast_time_unit(new_unit)?,
669            ))),
670            TemporalArray::Null => Err(MinarrowError::NullError { message: None }),
671        }
672    }
673}
674
675/// Helper function to get the variant name for error messages
676fn temporal_variant_name(arr: &TemporalArray) -> &'static str {
677    match arr {
678        TemporalArray::Datetime32(_) => "Datetime32",
679        TemporalArray::Datetime64(_) => "Datetime64",
680        TemporalArray::Null => "Null",
681    }
682}
683
684impl Display for TemporalArray {
685    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
686        match self {
687            TemporalArray::Datetime32(arr) => {
688                write_temporal_array_with_header(f, "Datetime32", arr.as_ref())
689            }
690            TemporalArray::Datetime64(arr) => {
691                write_temporal_array_with_header(f, "Datetime64", arr.as_ref())
692            }
693            TemporalArray::Null => writeln!(f, "TemporalArray::Null [0 values]"),
694        }
695    }
696}
697
698/// Writes the standard header, then delegates to the contained array's Display.
699fn write_temporal_array_with_header(
700    f: &mut Formatter<'_>,
701    dtype: &str,
702    arr: &(impl MaskedArray + Display + ?Sized),
703) -> std::fmt::Result {
704    writeln!(
705        f,
706        "TemporalArray [{dtype}] [{} values] (null count: {})",
707        arr.len(),
708        arr.null_count()
709    )?;
710    Display::fmt(arr, f)
711}