Skip to main content

minarrow/structs/views/collections/
temporal_array_view.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//! # **TemporalArrayView Module** - *Windowed View over a TemporalArray*
16//!
17//! `TemporalArrayV` is a **read-only, windowed view** over a [`TemporalArray`].
18//! It exposes a zero-copy slice `[offset .. offset + len)` for fast, indexable
19//! access to datetime/timestamp data.
20//!
21//! ## Role
22//! - Let APIs accept either a full `TemporalArray` or a pre-sliced view.
23//! - Avoid deep copies while enabling per-window operations and previews.
24//! - Optionally cache per-window null counts to speed up repeated scans.
25//!
26//! ## Behaviour
27//! - Works with 32-bit and 64-bit datetime variants behind `TemporalArray`.
28//! - Accessors - [`get_i64`](TemporalArrayV::get_i64) and
29//!   [`get_i32`](TemporalArrayV::get_i32) (the latter returns `None` for 64-bit).
30//! - Slicing yields another borrowed view; buffers are not cloned.
31//!
32//! ## Threading
33//! - Not thread-safe: uses `Cell` to cache the window’s null count.
34//! - For parallel use, create per-thread views via [`slice`](TemporalArrayV::slice).
35//!
36//! ## Interop
37//! - Convert to an owned `TemporalArray` of the window with
38//!   [`to_temporal_array`](TemporalArrayV::to_temporal_array).
39//! - Lift to `Array` with [`inner_array`](TemporalArrayV::inner_array) for enum-level APIs.
40//!
41//! ## Invariants
42//! - `offset + len <= array.len()`
43//! - `len` is the logical element count of this view.
44
45use std::fmt::{self, Debug, Display, Formatter};
46use std::sync::OnceLock;
47
48use crate::enums::error::MinarrowError;
49use crate::enums::shape_dim::ShapeDim;
50use crate::traits::concatenate::Concatenate;
51use crate::traits::print::MAX_PREVIEW;
52use crate::traits::shape::Shape;
53use crate::{Array, ArrayV, BitmaskV, MaskedArray, TemporalArray};
54
55/// # TemporalArrayView
56///
57/// Read-only, zero-copy view over a `[offset .. offset + len)` window of a
58/// [`TemporalArray`].
59///
60/// ## Purpose
61/// - Return an indexable subrange without cloning buffers.
62/// - Optionally cache per-window null counts for faster repeated passes.
63///
64/// ## Behaviour
65/// - Supports 32-bit and 64-bit datetime storage behind `TemporalArray`.
66/// - Provides upcast helpers - [`get_i64`](Self::get_i64) and
67///   [`get_i32`](Self::get_i32).
68/// - Further slicing yields another borrowed view.
69///
70/// ## Fields
71/// - `array`: backing [`TemporalArray`] (enum over temporal types).
72/// - `offset`: starting index into the backing array.
73/// - `len`: logical number of elements in the view.
74/// - `null_count`: cached `Option<usize>` for this window (internal).
75///
76/// ## Notes
77/// - Not thread-safe due to `Cell`. Create per-thread views with [`slice`](Self::slice).
78/// - Use [`to_temporal_array`](Self::to_temporal_array) to materialise the window.
79#[derive(Clone, PartialEq)]
80pub struct TemporalArrayV {
81    /// The **outer array** that this view is derived from - we retain a reference to it. 
82    /// Importantly, this is the ***full array*** - not the *view*, and thus should not be 
83    /// accessed as though it were the view subset.
84    pub array: TemporalArray,
85    /// The index offset from 0 that for where this view starts from the outer array
86    pub offset: usize,
87    /// The length of the array view
88    len: usize,
89    /// How many nulls are in the TemporalArrayView
90    /// At construction, this is None, unless constructed via new_nc. When one uses '.null_count()',
91    /// the first time it will calculate it (quickly) using Bitmask popcount, and then from that 
92    /// point onwards the null count is a cached value. 
93    null_count: OnceLock<usize>,
94}
95
96impl TemporalArrayV {
97    /// Creates a new `TemporalArrayView` with the given offset and length.
98    pub fn new(array: TemporalArray, offset: usize, len: usize) -> Self {
99        assert!(
100            offset + len <= array.len(),
101            "TemporalArrayView: window out of bounds (offset + len = {}, array.len = {})",
102            offset + len,
103            array.len()
104        );
105        Self {
106            array,
107            offset,
108            len,
109            null_count: OnceLock::new(),
110        }
111    }
112
113    /// Creates a new `TemporalArrayView` with a precomputed null count.
114    pub fn new_nc(
115        array: TemporalArray,
116        offset: usize,
117        len: usize,
118        null_count: usize,
119    ) -> Self {
120        assert!(
121            offset + len <= array.len(),
122            "TemporalArrayView: window out of bounds (offset + len = {}, array.len = {})",
123            offset + len,
124            array.len()
125        );
126        let lock = OnceLock::new();
127        let _ = lock.set(null_count); // Pre-initialize with the provided count
128        Self {
129            array,
130            offset,
131            len,
132            null_count: lock,
133        }
134    }
135
136    /// Returns `true` if the view is empty.
137    #[inline]
138    pub fn is_empty(&self) -> bool {
139        self.len == 0
140    }
141
142    /// Returns the value at logical index `i` within the window as i64 (upcasts i32 variant).
143    #[inline]
144    pub fn get_i64(&self, i: usize) -> Option<i64> {
145        if i >= self.len {
146            return None;
147        }
148        let phys_idx = self.offset + i;
149        match &self.array {
150            TemporalArray::Datetime32(arr) => arr.get(phys_idx).map(|v| v as i64),
151            TemporalArray::Datetime64(arr) => arr.get(phys_idx),
152            TemporalArray::Null => None,
153        }
154    }
155
156    /// Returns the value at logical index `i` as i32 (None for Datetime64).
157    #[inline]
158    pub fn get_i32(&self, i: usize) -> Option<i32> {
159        if i >= self.len {
160            return None;
161        }
162        let phys_idx = self.offset + i;
163        match &self.array {
164            TemporalArray::Datetime32(arr) => arr.get(phys_idx),
165            TemporalArray::Datetime64(_) => None,
166            TemporalArray::Null => None,
167        }
168    }
169
170    /// Returns a sliced `TemporalArrayView` from the current view.
171    #[inline]
172    pub fn slice(&self, offset: usize, len: usize) -> Self {
173        assert!(
174            offset + len <= self.len,
175            "TemporalArrayView::slice: out of bounds"
176        );
177        Self {
178            array: self.array.clone(),
179            offset: self.offset + offset,
180            len,
181            null_count: OnceLock::new(),
182        }
183    }
184
185    /// Returns the length of the window
186    #[inline]
187    pub fn len(&self) -> usize {
188        self.len
189    }
190
191    /// Returns the full backing array wrapped as an `Array` enum, ignoring the view's offset and length.
192    ///
193    /// Use this to access inner array methods. The returned array is the unwindowed original.
194    #[inline]
195    pub fn inner_array(&self) -> Array {
196        Array::TemporalArray(self.array.clone()) // Arc clone for data buffer
197    }
198
199    /// Converts the view into an owned `TemporalArray` for the window.
200    ///
201    /// If the view covers the entire backing array, returns a cheap Arc clone
202    /// of the original variant. Otherwise deep-copies the window via
203    /// `slice_clone` through `inner_array`.
204    pub fn to_temporal_array(&self) -> TemporalArray {
205        if self.offset == 0 && self.len == self.array.len() {
206            return self.array.clone();
207        }
208        self.inner_array().slice_clone(self.offset, self.len).dt()
209    }
210
211    /// Returns the end index of the view.
212    #[inline]
213    pub fn end(&self) -> usize {
214        self.offset + self.len
215    }
216
217    /// Returns the view as a tuple `(array, offset, len)`
218    #[inline]
219    pub fn as_tuple(&self) -> (TemporalArray, usize, usize) {
220        (self.array.clone(), self.offset, self.len)
221    }
222
223    /// Returns the number of nulls in the view.
224    #[inline]
225    pub fn null_count(&self) -> usize {
226        *self
227            .null_count
228            .get_or_init(|| match self.array.null_mask() {
229                Some(mask) => mask.view(self.offset, self.len).count_zeros(),
230                None => 0,
231            })
232    }
233
234    /// Returns true when the windowed view holds at least one null.
235    ///
236    /// Reads through `null_count`, so the cached value is trusted when set
237    /// and the full popcount is only paid on the first call that observes
238    /// this view.
239    #[inline]
240    pub fn has_nulls(&self) -> bool {
241        self.null_count() > 0
242    }
243
244    /// Returns the null mask as a windowed `BitmaskView`.
245    #[inline]
246    pub fn null_mask_view(&self) -> Option<BitmaskV> {
247        self.array
248            .null_mask()
249            .map(|mask| mask.view(self.offset, self.len))
250    }
251
252    /// Sets the cached null count for the view.
253    #[inline]
254    pub fn set_null_count(&self, count: usize) -> Result<(), usize> {
255        self.null_count.set(count).map_err(|_| count)
256    }
257}
258
259impl From<TemporalArray> for TemporalArrayV {
260    fn from(array: TemporalArray) -> Self {
261        let len = array.len();
262        TemporalArrayV {
263            array,
264            offset: 0,
265            len,
266            null_count: OnceLock::new(),
267        }
268    }
269}
270
271impl From<Array> for TemporalArrayV {
272    fn from(array: Array) -> Self {
273        match array {
274            Array::TemporalArray(arr) => {
275                let len = arr.len();
276                TemporalArrayV {
277                    array: arr,
278                    offset: 0,
279                    len,
280                    null_count: OnceLock::new(),
281                }
282            }
283            _ => panic!("Array is not a TemporalArray"),
284        }
285    }
286}
287
288impl From<ArrayV> for TemporalArrayV {
289    /// Converts an `ArrayView` to a `TemporalArrayView`, panicking if the array is not temporal.
290    fn from(view: ArrayV) -> Self {
291        let (array, offset, len) = view.as_tuple();
292        match array {
293            Array::TemporalArray(inner) => Self {
294                array: inner,
295                offset,
296                len,
297                null_count: OnceLock::new(),
298            },
299            _ => panic!("From<ArrayView>: expected TemporalArray variant"),
300        }
301    }
302}
303
304impl Debug for TemporalArrayV {
305    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
306        f.debug_struct("TemporalArrayView")
307            .field("offset", &self.offset)
308            .field("len", &self.len)
309            .field("array", &self.array)
310            .field("cached_null_count", &self.null_count.get())
311            .finish()
312    }
313}
314
315impl Display for TemporalArrayV {
316    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
317        let dtype = match &self.array {
318            TemporalArray::Datetime32(_) => "Datetime32<i32>",
319            TemporalArray::Datetime64(_) => "Datetime64<i64>",
320            TemporalArray::Null => "Null",
321        };
322
323        writeln!(
324            f,
325            "TemporalArrayView<{dtype}> [{} values] (offset: {}, nulls: {})",
326            self.len(),
327            self.offset,
328            self.null_count()
329        )?;
330
331        let max = self.len().min(MAX_PREVIEW);
332
333        #[cfg(feature = "datetime_ops")]
334        {
335            use time::OffsetDateTime;
336
337            use crate::TimeUnit;
338            let unit = match &self.array {
339                TemporalArray::Datetime32(arr) => &arr.time_unit,
340                TemporalArray::Datetime64(arr) => &arr.time_unit,
341                TemporalArray::Null => &TimeUnit::Milliseconds,
342            };
343            for i in 0..max {
344                match self.get_i64(i) {
345                    Some(val) => match unit {
346                        TimeUnit::Seconds => match OffsetDateTime::from_unix_timestamp(val) {
347                            Ok(dt) => writeln!(f, "  {dt}")?,
348                            Err(_) => writeln!(f, "  {val}s")?,
349                        },
350                        TimeUnit::Milliseconds => {
351                            match OffsetDateTime::from_unix_timestamp_nanos(
352                                (val as i128) * 1_000_000,
353                            ) {
354                                Ok(dt) => writeln!(f, "  {dt}")?,
355                                Err(_) => writeln!(f, "  {val}ms")?,
356                            }
357                        }
358                        TimeUnit::Microseconds => {
359                            match OffsetDateTime::from_unix_timestamp_nanos((val as i128) * 1_000) {
360                                Ok(dt) => writeln!(f, "  {dt}")?,
361                                Err(_) => writeln!(f, "  {val}µs")?,
362                            }
363                        }
364                        TimeUnit::Nanoseconds => {
365                            match OffsetDateTime::from_unix_timestamp_nanos(val as i128) {
366                                Ok(dt) => writeln!(f, "  {dt}")?,
367                                Err(_) => writeln!(f, "  {val}ns")?,
368                            }
369                        }
370                        TimeUnit::Days => {
371                            use crate::structs::variants::datetime::UNIX_EPOCH_JULIAN_DAY;
372
373                            let days = val;
374                            match time::Date::from_julian_day((days + UNIX_EPOCH_JULIAN_DAY) as i32)
375                            {
376                                Ok(d) => writeln!(f, "  {d}")?,
377                                Err(_) => writeln!(f, "  {days}d")?,
378                            }
379                        }
380                    },
381                    None => writeln!(f, "  null")?,
382                }
383            }
384        }
385
386        #[cfg(not(feature = "datetime_ops"))]
387        {
388            use crate::TimeUnit;
389
390            let unit = match &self.array {
391                TemporalArray::Datetime32(arr) => &arr.time_unit,
392                TemporalArray::Datetime64(arr) => &arr.time_unit,
393                TemporalArray::Null => &TimeUnit::Milliseconds,
394            };
395            let suffix = match unit {
396                TimeUnit::Seconds => "s",
397                TimeUnit::Milliseconds => "ms",
398                TimeUnit::Microseconds => "µs",
399                TimeUnit::Nanoseconds => "ns",
400                TimeUnit::Days => "d",
401            };
402            for i in 0..max {
403                match self.get_i64(i) {
404                    Some(val) => writeln!(f, "  {}{}", val, suffix)?,
405                    None => writeln!(f, "  null")?,
406                }
407            }
408        }
409
410        if self.len() > MAX_PREVIEW {
411            writeln!(f, "  ... ({} more)", self.len() - MAX_PREVIEW)?;
412        }
413
414        Ok(())
415    }
416}
417
418#[cfg(feature = "datetime_ops")]
419use crate::DatetimeOps;
420
421#[cfg(feature = "datetime_ops")]
422use crate::enums::time_units::TimeUnit;
423
424#[cfg(feature = "datetime_ops")]
425use time::Duration;
426
427#[cfg(feature = "datetime_ops")]
428use crate::structs::variants::{boolean::BooleanArray, integer::IntegerArray};
429
430#[cfg(feature = "datetime_ops")]
431use crate::DatetimeArray;
432
433#[cfg(feature = "datetime_ops")]
434use num_traits::{FromPrimitive, ToPrimitive};
435
436#[cfg(feature = "datetime_ops")]
437use std::sync::Arc;
438
439/// Macro to dispatch a windowed component extraction over both temporal variants.
440///
441/// Iterates `self.offset .. self.offset + self.len` on the inner `DatetimeArray`,
442/// pushing the extracted component into a fresh `IntegerArray<i32>`.
443#[cfg(feature = "datetime_ops")]
444macro_rules! windowed_component {
445    ($self:expr, $extract:expr) => {{
446        let offset = $self.offset;
447        let len = $self.len();
448        match &$self.array {
449            TemporalArray::Datetime32(arr) => {
450                windowed_component_inner!(arr, offset, len, $extract)
451            }
452            TemporalArray::Datetime64(arr) => {
453                windowed_component_inner!(arr, offset, len, $extract)
454            }
455            TemporalArray::Null => IntegerArray::default(),
456        }
457    }};
458}
459
460#[cfg(feature = "datetime_ops")]
461macro_rules! windowed_component_inner {
462    ($arr:expr, $offset:expr, $len:expr, $extract:expr) => {{
463        let mut result = IntegerArray::with_capacity($len, $arr.is_nullable());
464        for i in $offset..$offset + $len {
465            if $arr.is_null(i) {
466                result.push_null();
467            } else if let Some(val_i64) = $arr.data[i].to_i64() {
468                if let Some(dt) = DatetimeArray::<i64>::i64_to_datetime(val_i64, $arr.time_unit) {
469                    #[allow(clippy::redundant_closure_call)]
470                    result.push(($extract)(&dt));
471                } else {
472                    result.push_null();
473                }
474            } else {
475                result.push_null();
476            }
477        }
478        result
479    }};
480}
481
482/// Macro to dispatch a windowed Self-returning operation over both temporal variants.
483#[cfg(feature = "datetime_ops")]
484macro_rules! windowed_self_op {
485    ($self:expr, $method:ident $(, $arg:expr)*) => {{
486        let offset = $self.offset;
487        let len = $self.len();
488        match &$self.array {
489            TemporalArray::Datetime32(arr) => {
490                let sliced = arr.slice_clone(offset, len);
491                let result = sliced.$method($($arg),*)?;
492                Ok(TemporalArrayV::from(TemporalArray::Datetime32(Arc::new(result))))
493            }
494            TemporalArray::Datetime64(arr) => {
495                let sliced = arr.slice_clone(offset, len);
496                let result = sliced.$method($($arg),*)?;
497                Ok(TemporalArrayV::from(TemporalArray::Datetime64(Arc::new(result))))
498            }
499            TemporalArray::Null => Err(MinarrowError::NullError { message: None }),
500        }
501    }};
502}
503
504/// Macro for infallible Self-returning operations.
505#[cfg(feature = "datetime_ops")]
506macro_rules! windowed_self_op_infallible {
507    ($self:expr, $method:ident $(, $arg:expr)*) => {{
508        let offset = $self.offset;
509        let len = $self.len();
510        match &$self.array {
511            TemporalArray::Datetime32(arr) => {
512                let sliced = arr.slice_clone(offset, len);
513                let result = sliced.$method($($arg),*);
514                TemporalArrayV::from(TemporalArray::Datetime32(Arc::new(result)))
515            }
516            TemporalArray::Datetime64(arr) => {
517                let sliced = arr.slice_clone(offset, len);
518                let result = sliced.$method($($arg),*);
519                TemporalArrayV::from(TemporalArray::Datetime64(Arc::new(result)))
520            }
521            TemporalArray::Null => TemporalArrayV::from(TemporalArray::Null),
522        }
523    }};
524}
525
526#[cfg(feature = "datetime_ops")]
527impl DatetimeOps for TemporalArrayV {
528    // Component Extraction - iterate only the windowed range
529
530    fn year(&self) -> IntegerArray<i32> {
531        windowed_component!(self, |dt: &time::OffsetDateTime| dt.year())
532    }
533
534    fn month(&self) -> IntegerArray<i32> {
535        windowed_component!(self, |dt: &time::OffsetDateTime| dt.month() as i32)
536    }
537
538    fn day(&self) -> IntegerArray<i32> {
539        windowed_component!(self, |dt: &time::OffsetDateTime| dt.day() as i32)
540    }
541
542    fn hour(&self) -> IntegerArray<i32> {
543        windowed_component!(self, |dt: &time::OffsetDateTime| dt.hour() as i32)
544    }
545
546    fn minute(&self) -> IntegerArray<i32> {
547        windowed_component!(self, |dt: &time::OffsetDateTime| dt.minute() as i32)
548    }
549
550    fn second(&self) -> IntegerArray<i32> {
551        windowed_component!(self, |dt: &time::OffsetDateTime| dt.second() as i32)
552    }
553
554    fn weekday(&self) -> IntegerArray<i32> {
555        windowed_component!(
556            self,
557            |dt: &time::OffsetDateTime| dt.weekday().number_from_sunday() as i32
558        )
559    }
560
561    fn day_of_year(&self) -> IntegerArray<i32> {
562        windowed_component!(self, |dt: &time::OffsetDateTime| dt.ordinal() as i32)
563    }
564
565    fn iso_week(&self) -> IntegerArray<i32> {
566        windowed_component!(self, |dt: &time::OffsetDateTime| dt.iso_week() as i32)
567    }
568
569    fn quarter(&self) -> IntegerArray<i32> {
570        windowed_component!(self, |dt: &time::OffsetDateTime| {
571            let month = dt.month() as i32;
572            ((month - 1) / 3) + 1
573        })
574    }
575
576    fn week_of_year(&self) -> IntegerArray<i32> {
577        windowed_component!(self, |dt: &time::OffsetDateTime| {
578            let day_of_year = dt.ordinal() as i32;
579            let weekday = dt.weekday().number_from_sunday() as i32;
580            (day_of_year + 7 - weekday) / 7
581        })
582    }
583
584    fn is_leap_year(&self) -> BooleanArray<()> {
585        let offset = self.offset;
586        let len = self.len();
587        match &self.array {
588            TemporalArray::Datetime32(arr) => is_leap_year_windowed(arr, offset, len),
589            TemporalArray::Datetime64(arr) => is_leap_year_windowed(arr, offset, len),
590            TemporalArray::Null => BooleanArray::default(),
591        }
592    }
593
594    // Arithmetic - slice the windowed range, delegate, wrap result as view
595
596    fn add_duration(&self, duration: Duration) -> Result<Self, MinarrowError> {
597        windowed_self_op!(self, add_duration, duration)
598    }
599
600    fn sub_duration(&self, duration: Duration) -> Result<Self, MinarrowError> {
601        windowed_self_op!(self, sub_duration, duration)
602    }
603
604    fn add_days(&self, days: i64) -> Result<Self, MinarrowError> {
605        windowed_self_op!(self, add_days, days)
606    }
607
608    fn add_months(&self, months: i32) -> Result<Self, MinarrowError> {
609        windowed_self_op!(self, add_months, months)
610    }
611
612    fn add_years(&self, years: i32) -> Result<Self, MinarrowError> {
613        windowed_self_op!(self, add_years, years)
614    }
615
616    // Comparison - slice both operands, delegate
617
618    fn diff(&self, other: &Self, unit: TimeUnit) -> Result<IntegerArray<i64>, MinarrowError> {
619        let self_arr = self.to_temporal_array();
620        let other_arr = other.to_temporal_array();
621        self_arr.diff(&other_arr, unit)
622    }
623
624    fn abs_diff(&self, other: &Self, unit: TimeUnit) -> Result<IntegerArray<i64>, MinarrowError> {
625        let self_arr = self.to_temporal_array();
626        let other_arr = other.to_temporal_array();
627        self_arr.abs_diff(&other_arr, unit)
628    }
629
630    fn is_before(&self, other: &Self) -> Result<BooleanArray<()>, MinarrowError> {
631        let self_arr = self.to_temporal_array();
632        let other_arr = other.to_temporal_array();
633        self_arr.is_before(&other_arr)
634    }
635
636    fn is_after(&self, other: &Self) -> Result<BooleanArray<()>, MinarrowError> {
637        let self_arr = self.to_temporal_array();
638        let other_arr = other.to_temporal_array();
639        self_arr.is_after(&other_arr)
640    }
641
642    fn between(&self, start: &Self, end: &Self) -> Result<BooleanArray<()>, MinarrowError> {
643        let self_arr = self.to_temporal_array();
644        let start_arr = start.to_temporal_array();
645        let end_arr = end.to_temporal_array();
646        self_arr.between(&start_arr, &end_arr)
647    }
648
649    // Truncation - slice the windowed range, delegate
650
651    fn truncate(&self, unit: &str) -> Result<Self, MinarrowError> {
652        windowed_self_op!(self, truncate, unit)
653    }
654
655    fn us(&self) -> Self {
656        windowed_self_op_infallible!(self, us)
657    }
658
659    fn ms(&self) -> Self {
660        windowed_self_op_infallible!(self, ms)
661    }
662
663    fn sec(&self) -> Self {
664        windowed_self_op_infallible!(self, sec)
665    }
666
667    fn min(&self) -> Self {
668        windowed_self_op_infallible!(self, min)
669    }
670
671    fn hr(&self) -> Self {
672        windowed_self_op_infallible!(self, hr)
673    }
674
675    fn week(&self) -> Self {
676        windowed_self_op_infallible!(self, week)
677    }
678
679    // Type Casting
680
681    fn cast_time_unit(&self, new_unit: TimeUnit) -> Result<Self, MinarrowError> {
682        windowed_self_op!(self, cast_time_unit, new_unit)
683    }
684}
685
686/// Helper for windowed `is_leap_year` over a generic `DatetimeArray<T>`.
687#[cfg(feature = "datetime_ops")]
688fn is_leap_year_windowed<T: crate::Integer + FromPrimitive>(
689    arr: &DatetimeArray<T>,
690    offset: usize,
691    len: usize,
692) -> BooleanArray<()> {
693    let mut result = BooleanArray::with_capacity(len, arr.is_nullable());
694    for i in offset..offset + len {
695        if arr.is_null(i) {
696            result.push_null();
697        } else if let Some(val_i64) = arr.data[i].to_i64() {
698            if let Some(dt) = DatetimeArray::<i64>::i64_to_datetime(val_i64, arr.time_unit) {
699                result.push(time::util::is_leap_year(dt.year()));
700            } else {
701                result.push_null();
702            }
703        } else {
704            result.push_null();
705        }
706    }
707    result
708}
709
710impl Shape for TemporalArrayV {
711    fn shape(&self) -> ShapeDim {
712        ShapeDim::Rank1(self.len())
713    }
714}
715
716impl Concatenate for TemporalArrayV {
717    /// Concatenates two temporal array views by materialising both to owned temporal arrays,
718    /// concatenating them, and wrapping the result back in a view.
719    ///
720    /// # Notes
721    /// - This operation copies data from both views to create owned temporal arrays.
722    /// - The resulting view has offset=0 and length equal to the combined length.
723    fn concat(self, other: Self) -> Result<Self, MinarrowError> {
724        // Materialise both views to owned temporal arrays
725        let self_array = self.to_temporal_array();
726        let other_array = other.to_temporal_array();
727
728        // Concatenate the owned temporal arrays
729        let concatenated = self_array.concat(other_array)?;
730
731        // Wrap the result in a new view
732        Ok(TemporalArrayV::from(concatenated))
733    }
734}
735
736#[cfg(test)]
737mod tests {
738    use std::sync::Arc;
739
740    use super::*;
741    use crate::{Bitmask, DatetimeArray, TemporalArray};
742
743    #[test]
744    fn test_temporal_array_view_basic_indexing_and_slice() {
745        let arr = DatetimeArray::<i64>::from_slice(&[10_000, 20_000, 30_000, 40_000], None);
746        let temporal = TemporalArray::Datetime64(Arc::new(arr));
747        let view = TemporalArrayV::new(temporal, 1, 2);
748
749        assert_eq!(view.len(), 2);
750        assert_eq!(view.offset, 1);
751        assert_eq!(view.get_i64(0), Some(20_000));
752        assert_eq!(view.get_i64(1), Some(30_000));
753        assert_eq!(view.get_i64(2), None);
754
755        let sub = view.slice(1, 1);
756        assert_eq!(sub.len(), 1);
757        assert_eq!(sub.get_i64(0), Some(30_000));
758        assert_eq!(sub.get_i64(1), None);
759    }
760
761    #[test]
762    fn test_temporal_array_view_null_count_and_cache() {
763        let mut arr = DatetimeArray::<i64>::from_slice(&[1, 2, 3, 4], None);
764        let mut mask = Bitmask::new_set_all(4, true);
765        mask.set(2, false);
766        arr.null_mask = Some(mask);
767
768        let temporal = TemporalArray::Datetime64(Arc::new(arr));
769        let view = TemporalArrayV::new(temporal, 0, 4);
770        assert_eq!(view.null_count(), 1, "Null count should detect one null");
771        assert_eq!(view.null_count(), 1);
772
773        let view2 = view.slice(0, 2);
774        assert_eq!(view2.null_count(), 0);
775        let view3 = view.slice(2, 2);
776        assert_eq!(view3.null_count(), 1);
777    }
778
779    #[test]
780    fn test_temporal_array_view_with_supplied_null_count() {
781        let arr = DatetimeArray::<i64>::from_slice(&[5, 6], None);
782        let temporal = TemporalArray::Datetime64(Arc::new(arr));
783        let view = TemporalArrayV::new_nc(temporal, 0, 2, 99);
784        assert_eq!(view.null_count(), 99);
785        // Trying to set again should fail since it\'s already initialized
786        assert!(view.set_null_count(101).is_err());
787        // Still returns original value
788        assert_eq!(view.null_count(), 99);
789    }
790
791    #[test]
792    fn test_temporal_array_view_to_temporal_array_and_as_tuple() {
793        let arr = DatetimeArray::<i64>::from_slice(&(10..20).collect::<Vec<_>>(), None);
794        let temporal = TemporalArray::Datetime64(Arc::new(arr));
795        let view = TemporalArrayV::new(temporal.clone(), 4, 3);
796        let arr2 = view.to_temporal_array();
797        if let TemporalArray::Datetime64(a2) = arr2 {
798            assert_eq!(&a2.data[..], &[14, 15, 16]);
799        } else {
800            panic!("Unexpected variant");
801        }
802        let tup = view.as_tuple();
803        assert_eq!(&tup.0, &temporal);
804        assert_eq!(tup.1, 4);
805        assert_eq!(tup.2, 3);
806    }
807
808    #[test]
809    fn test_temporal_array_view_null_mask_view() {
810        let mut arr = DatetimeArray::<i64>::from_slice(&[2, 4, 6], None);
811        let mut mask = Bitmask::new_set_all(3, true);
812        mask.set(0, false);
813        arr.null_mask = Some(mask);
814
815        let temporal = TemporalArray::Datetime64(Arc::new(arr));
816        let view = TemporalArrayV::new(temporal, 1, 2);
817        let mask_view = view.null_mask_view().expect("Should have mask");
818        assert_eq!(mask_view.len(), 2);
819        assert!(mask_view.get(0));
820        assert!(mask_view.get(1));
821    }
822
823    #[test]
824    fn test_temporal_array_view_from_temporal_array_and_array() {
825        let arr = DatetimeArray::<i64>::from_slice(&[1, 2], None);
826        let temporal = TemporalArray::Datetime64(Arc::new(arr));
827        let view_from_temporal = TemporalArrayV::from(temporal.clone());
828        assert_eq!(view_from_temporal.len(), 2);
829        assert_eq!(view_from_temporal.get_i64(0), Some(1));
830
831        let array = Array::TemporalArray(temporal);
832        let view_from_array = TemporalArrayV::from(array);
833        assert_eq!(view_from_array.len(), 2);
834        assert_eq!(view_from_array.get_i64(1), Some(2));
835    }
836
837    #[test]
838    #[should_panic(expected = "Array is not a TemporalArray")]
839    fn test_temporal_array_view_from_array_panics_on_wrong_variant() {
840        let array = Array::Null;
841        let _view = TemporalArrayV::from(array);
842    }
843
844    #[test]
845    fn test_to_temporal_array_full_coverage_shares_arc() {
846        let arr = DatetimeArray::<i64>::from_slice(&[10, 20, 30], None);
847        let src = Arc::new(arr);
848        let temporal = TemporalArray::Datetime64(src.clone());
849
850        let view = TemporalArrayV::new(temporal, 0, 3);
851        let out = view.to_temporal_array();
852
853        match out {
854            TemporalArray::Datetime64(out_arc) => assert!(
855                Arc::ptr_eq(&src, &out_arc),
856                "full-coverage to_temporal_array should share the underlying Arc"
857            ),
858            _ => panic!("expected Datetime64 variant"),
859        }
860    }
861
862    #[test]
863    fn test_to_temporal_array_windowed_copies() {
864        let arr = DatetimeArray::<i64>::from_slice(&[10, 20, 30], None);
865        let src = Arc::new(arr);
866        let temporal = TemporalArray::Datetime64(src.clone());
867
868        let view = TemporalArrayV::new(temporal, 1, 2);
869        let out = view.to_temporal_array();
870
871        match out {
872            TemporalArray::Datetime64(out_arc) => assert!(
873                !Arc::ptr_eq(&src, &out_arc),
874                "windowed to_temporal_array must allocate a fresh buffer"
875            ),
876            _ => panic!("expected Datetime64 variant"),
877        }
878    }
879}