Skip to main content

polars_core/chunked_array/
mod.rs

1//! The typed heart of every Series column.
2#![allow(unsafe_op_in_unsafe_fn)]
3use std::sync::Arc;
4
5use arrow::array::*;
6use arrow::bitmap::Bitmap;
7use arrow::compute::concatenate::concatenate_unchecked;
8use arrow::compute::utils::combine_validities_and;
9use polars_compute::filter::filter_with_bitmap;
10
11use crate::prelude::{ChunkTakeUnchecked, *};
12
13pub mod ops;
14#[macro_use]
15pub mod arithmetic;
16pub mod builder;
17pub mod cast;
18pub mod collect;
19pub mod comparison;
20pub mod flags;
21pub mod float;
22pub mod iterator;
23#[cfg(feature = "ndarray")]
24pub(crate) mod ndarray;
25
26pub mod arg_min_max;
27#[cfg(feature = "dtype-array")]
28pub(crate) mod array;
29mod binary;
30mod binary_offset;
31mod bitwise;
32#[cfg(feature = "object")]
33mod drop;
34mod from;
35mod from_iterator;
36pub mod from_iterator_par;
37pub(crate) mod list;
38pub(crate) mod logical;
39#[cfg(feature = "object")]
40pub mod object;
41#[cfg(feature = "random")]
42mod random;
43#[cfg(feature = "dtype-struct")]
44mod struct_;
45#[cfg(any(
46    feature = "temporal",
47    feature = "dtype-datetime",
48    feature = "dtype-date"
49))]
50pub mod temporal;
51mod to_vec;
52mod trusted_len;
53pub(crate) use arg_min_max::*;
54#[cfg(feature = "dtype-struct")]
55pub use struct_::StructChunked;
56
57use self::flags::{StatisticsFlags, StatisticsFlagsIM};
58use crate::series::IsSorted;
59use crate::utils::{first_non_null, first_null, last_non_null};
60
61pub type ChunkLenIter<'a> = std::iter::Map<std::slice::Iter<'a, ArrayRef>, fn(&ArrayRef) -> usize>;
62
63/// # ChunkedArray
64///
65/// Every Series contains a [`ChunkedArray<T>`]. Unlike [`Series`], [`ChunkedArray`]s are typed. This allows
66/// us to apply closures to the data and collect the results to a [`ChunkedArray`] of the same type `T`.
67/// Below we use an apply to use the cosine function to the values of a [`ChunkedArray`].
68///
69/// ```rust
70/// # use polars_core::prelude::*;
71/// fn apply_cosine_and_cast(ca: &Float32Chunked) -> Float32Chunked {
72///     ca.apply_values(|v| v.cos())
73/// }
74/// ```
75///
76/// ## Conversion between Series and ChunkedArrays
77/// Conversion from a [`Series`] to a [`ChunkedArray`] is effortless.
78///
79/// ```rust
80/// # use polars_core::prelude::*;
81/// fn to_chunked_array(series: &Series) -> PolarsResult<&Int32Chunked>{
82///     series.i32()
83/// }
84///
85/// fn to_series(ca: Int32Chunked) -> Series {
86///     ca.into_series()
87/// }
88/// ```
89///
90/// # Iterators
91///
92/// [`ChunkedArray`]s fully support Rust native [Iterator](https://doc.rust-lang.org/std/iter/trait.Iterator.html)
93/// and [DoubleEndedIterator](https://doc.rust-lang.org/std/iter/trait.DoubleEndedIterator.html) traits, thereby
94/// giving access to all the excellent methods available for [Iterators](https://doc.rust-lang.org/std/iter/trait.Iterator.html).
95///
96/// ```rust
97/// # use polars_core::prelude::*;
98///
99/// fn iter_forward(ca: &Float32Chunked) {
100///     ca.iter()
101///         .for_each(|opt_v| println!("{:?}", opt_v))
102/// }
103///
104/// fn iter_backward(ca: &Float32Chunked) {
105///     ca.iter()
106///         .rev()
107///         .for_each(|opt_v| println!("{:?}", opt_v))
108/// }
109/// ```
110///
111/// # Memory layout
112///
113/// [`ChunkedArray`]s use [Apache Arrow](https://github.com/apache/arrow) as backend for the memory layout.
114/// Arrows memory is immutable which makes it possible to make multiple zero copy (sub)-views from a single array.
115///
116/// To be able to append data, Polars uses chunks to append new memory locations, hence the [`ChunkedArray<T>`] data structure.
117/// Appends are cheap, because it will not lead to a full reallocation of the whole array (as could be the case with a Rust Vec).
118///
119/// However, multiple chunks in a [`ChunkedArray`] will slow down many operations that need random access because we have an extra indirection
120/// and indexes need to be mapped to the proper chunk. Arithmetic may also be slowed down by this.
121/// When multiplying two [`ChunkedArray`]s with different chunk sizes they cannot utilize [SIMD](https://en.wikipedia.org/wiki/SIMD) for instance.
122///
123/// If you want to have predictable performance
124/// (no unexpected re-allocation of memory), it is advised to call the [`ChunkedArray::rechunk`] after
125/// multiple append operations.
126///
127/// See also [`ChunkedArray::extend`] for appends within a chunk.
128///
129/// # Invariants
130/// - A [`ChunkedArray`] should always have at least a single [`ArrayRef`].
131/// - The [`PolarsDataType`] `T` should always map to the correct [`ArrowDataType`] in the [`ArrayRef`]
132///   chunks.
133/// - Nested datatypes such as [`List`] and [`Array`] store the physical types instead of the
134///   logical type given by the datatype.
135///
136/// [`List`]: crate::datatypes::DataType::List
137pub struct ChunkedArray<T: PolarsDataType> {
138    pub(crate) field: Arc<Field>,
139    pub(crate) chunks: Vec<ArrayRef>,
140
141    pub(crate) flags: StatisticsFlagsIM,
142
143    length: usize,
144    null_count: usize,
145    _pd: std::marker::PhantomData<T>,
146}
147
148impl<T: PolarsDataType> ChunkedArray<T> {
149    fn should_rechunk(&self) -> bool {
150        self.chunks.len() > 1 && self.chunks.len() > self.len() / 3
151    }
152
153    fn optional_rechunk(mut self) -> Self {
154        // Rechunk if we have many small chunks.
155        if self.should_rechunk() {
156            self.rechunk_mut()
157        }
158        self
159    }
160
161    pub(crate) fn as_any(&self) -> &dyn std::any::Any {
162        self
163    }
164
165    /// Series to [`ChunkedArray<T>`]
166    pub fn unpack_series_matching_type<'a>(
167        &self,
168        series: &'a Series,
169    ) -> PolarsResult<&'a ChunkedArray<T>> {
170        polars_ensure!(
171            self.dtype() == series.dtype(),
172            SchemaMismatch: "cannot unpack series of type `{}` into `{}`",
173            series.dtype(),
174            self.dtype(),
175        );
176
177        // SAFETY: dtype will be correct.
178        Ok(unsafe { self.unpack_series_matching_physical_type(series) })
179    }
180
181    /// Create a new [`ChunkedArray`] and compute its `length` and `null_count`.
182    ///
183    /// If you want to explicitly the `length` and `null_count`, look at
184    /// [`ChunkedArray::new_with_dims`]
185    fn new_with_compute_len(field: Arc<Field>, chunks: Vec<ArrayRef>) -> Self {
186        unsafe {
187            let mut chunked_arr = Self::new_with_dims(field, chunks, 0, 0);
188            chunked_arr.compute_len();
189            chunked_arr
190        }
191    }
192
193    /// Create a new [`ChunkedArray`] and explicitly set its `length` and `null_count`.
194    /// # Safety
195    /// The length and null_count must be correct.
196    pub unsafe fn new_with_dims(
197        field: Arc<Field>,
198        chunks: Vec<ArrayRef>,
199        length: usize,
200        null_count: usize,
201    ) -> Self {
202        Self {
203            field,
204            chunks,
205            flags: StatisticsFlagsIM::empty(),
206
207            _pd: Default::default(),
208            length,
209            null_count,
210        }
211    }
212
213    pub(crate) fn is_sorted_ascending_flag(&self) -> bool {
214        self.get_flags().is_sorted_ascending()
215    }
216
217    pub(crate) fn is_sorted_descending_flag(&self) -> bool {
218        self.get_flags().is_sorted_descending()
219    }
220
221    /// Whether `self` is sorted in any direction.
222    pub(crate) fn is_sorted_any(&self) -> bool {
223        self.get_flags().is_sorted_any()
224    }
225
226    pub fn unset_fast_explode_list(&mut self) {
227        self.set_fast_explode_list(false)
228    }
229
230    pub fn set_fast_explode_list(&mut self, value: bool) {
231        let mut flags = self.flags.get_mut();
232        flags.set(StatisticsFlags::CAN_FAST_EXPLODE_LIST, value);
233        self.flags.set_mut(flags);
234    }
235
236    pub fn get_fast_explode_list(&self) -> bool {
237        self.get_flags().can_fast_explode_list()
238    }
239
240    pub fn get_flags(&self) -> StatisticsFlags {
241        self.flags.get()
242    }
243
244    /// Set flags for the [`ChunkedArray`]
245    pub fn set_flags(&mut self, flags: StatisticsFlags) {
246        self.flags = StatisticsFlagsIM::new(flags);
247    }
248
249    pub fn is_sorted_flag(&self) -> IsSorted {
250        self.get_flags().is_sorted()
251    }
252
253    pub fn retain_flags_from<U: PolarsDataType>(
254        &mut self,
255        from: &ChunkedArray<U>,
256        retain_flags: StatisticsFlags,
257    ) {
258        let flags = from.flags.get();
259        // Try to avoid write contention.
260        if !flags.is_empty() {
261            self.set_flags(flags & retain_flags)
262        }
263    }
264
265    /// Set the 'sorted' bit meta info.
266    pub fn set_sorted_flag(&mut self, sorted: IsSorted) {
267        let mut flags = self.flags.get_mut();
268        flags.set_sorted(sorted);
269        self.flags.set_mut(flags);
270    }
271
272    /// Set the 'sorted' bit meta info.
273    pub fn with_sorted_flag(&self, sorted: IsSorted) -> Self {
274        let mut out = self.clone();
275        out.set_sorted_flag(sorted);
276        out
277    }
278
279    pub fn first_null(&self) -> Option<usize> {
280        if self.null_count() == 0 {
281            None
282        }
283        // We now know there is at least 1 non-null item in the array, and self.len() > 0
284        else if self.null_count() == self.len() {
285            Some(0)
286        } else if self.is_sorted_any() {
287            let out = if self
288                .chunks
289                .iter()
290                .find(|arr| !arr.is_empty())
291                .unwrap()
292                .is_null(0)
293            {
294                // nulls are all at the start
295                0
296            } else {
297                // nulls are all at the end
298                self.null_count()
299            };
300
301            debug_assert!(
302                // If we are lucky this catches something.
303                unsafe { self.get_unchecked(out) }.is_some(),
304                "incorrect sorted flag"
305            );
306
307            Some(out)
308        } else {
309            first_null(self.chunks().iter().map(|arr| arr.as_ref()))
310        }
311    }
312
313    /// Get the index of the first non null value in this [`ChunkedArray`].
314    pub fn first_non_null(&self) -> Option<usize> {
315        if self.null_count() == self.len() {
316            None
317        }
318        // We now know there is at least 1 non-null item in the array, and self.len() > 0
319        else if self.null_count() == 0 {
320            Some(0)
321        } else if self.is_sorted_any() {
322            let out = if self
323                .chunks
324                .iter()
325                .find(|arr| !arr.is_empty())
326                .unwrap()
327                .is_null(0)
328            {
329                // nulls are all at the start
330                self.null_count()
331            } else {
332                // nulls are all at the end
333                0
334            };
335
336            debug_assert!(
337                // If we are lucky this catches something.
338                unsafe { self.get_unchecked(out) }.is_some(),
339                "incorrect sorted flag"
340            );
341
342            Some(out)
343        } else {
344            first_non_null(self.chunks().iter().map(|arr| arr.as_ref()))
345        }
346    }
347
348    /// Get the index of the last non null value in this [`ChunkedArray`].
349    pub fn last_non_null(&self) -> Option<usize> {
350        if self.null_count() == self.len() {
351            None
352        }
353        // We now know there is at least 1 non-null item in the array, and self.len() > 0
354        else if self.null_count() == 0 {
355            Some(self.len() - 1)
356        } else if self.is_sorted_any() {
357            let out = if self
358                .chunks
359                .iter()
360                .find(|arr| !arr.is_empty())
361                .unwrap()
362                .is_null(0)
363            {
364                // nulls are all at the start
365                self.len() - 1
366            } else {
367                // nulls are all at the end
368                self.len() - self.null_count() - 1
369            };
370
371            debug_assert!(
372                // If we are lucky this catches something.
373                unsafe { self.get_unchecked(out) }.is_some(),
374                "incorrect sorted flag"
375            );
376
377            Some(out)
378        } else {
379            last_non_null(self.chunks().iter().map(|arr| arr.as_ref()), self.len())
380        }
381    }
382
383    pub fn drop_nulls(&self) -> Self {
384        if self.null_count() == 0 {
385            self.clone()
386        } else {
387            let chunks = self
388                .downcast_iter()
389                .map(|arr| {
390                    if arr.null_count() == 0 {
391                        arr.to_boxed()
392                    } else {
393                        filter_with_bitmap(arr, arr.validity().unwrap())
394                    }
395                })
396                .collect();
397            unsafe {
398                Self::new_with_dims(
399                    self.field.clone(),
400                    chunks,
401                    self.len() - self.null_count(),
402                    0,
403                )
404            }
405        }
406    }
407
408    /// Get the buffer of bits representing null values
409    #[inline]
410    #[allow(clippy::type_complexity)]
411    pub fn iter_validities(
412        &self,
413    ) -> impl ExactSizeIterator<Item = Option<&Bitmap>> + DoubleEndedIterator {
414        fn to_validity(arr: &ArrayRef) -> Option<&Bitmap> {
415            arr.validity()
416        }
417        self.chunks.iter().map(to_validity)
418    }
419
420    #[inline]
421    /// Return if any the chunks in this [`ChunkedArray`] have nulls.
422    pub fn has_nulls(&self) -> bool {
423        self.null_count > 0
424    }
425
426    /// Shrink the capacity of this array to fit its length.
427    pub fn shrink_to_fit(&mut self) {
428        self.chunks = vec![concatenate_unchecked(self.chunks.as_slice()).unwrap()];
429    }
430
431    pub fn clear(&self) -> Self {
432        // SAFETY: we keep the correct dtype
433        let mut ca = unsafe {
434            self.copy_with_chunks(vec![new_empty_array(
435                self.chunks.first().unwrap().dtype().clone(),
436            )])
437        };
438
439        use StatisticsFlags as F;
440        ca.retain_flags_from(self, F::IS_SORTED_ANY | F::CAN_FAST_EXPLODE_LIST);
441        ca
442    }
443
444    /// Unpack a [`Series`] to the same physical type.
445    ///
446    /// # Safety
447    ///
448    /// This is unsafe as the dtype may be incorrect and
449    /// is assumed to be correct in other safe code.
450    pub(crate) unsafe fn unpack_series_matching_physical_type<'a>(
451        &self,
452        series: &'a Series,
453    ) -> &'a ChunkedArray<T> {
454        let series_trait = &**series;
455        if self.dtype() == series.dtype() {
456            &*(series_trait as *const dyn SeriesTrait as *const ChunkedArray<T>)
457        } else {
458            use DataType::*;
459            match (self.dtype(), series.dtype()) {
460                (Int64, Datetime(_, _)) | (Int64, Duration(_)) | (Int32, Date) => {
461                    &*(series_trait as *const dyn SeriesTrait as *const ChunkedArray<T>)
462                },
463                _ => panic!(
464                    "cannot unpack series {:?} into matching type {:?}",
465                    series,
466                    self.dtype()
467                ),
468            }
469        }
470    }
471
472    /// Returns an iterator over the lengths of the chunks of the array.
473    pub fn chunk_lengths(&self) -> ChunkLenIter<'_> {
474        self.chunks.iter().map(|chunk| chunk.len())
475    }
476
477    /// A reference to the chunks
478    #[inline]
479    pub fn chunks(&self) -> &Vec<ArrayRef> {
480        &self.chunks
481    }
482
483    /// A mutable reference to the chunks
484    ///
485    /// # Safety
486    /// The caller must ensure to not change the [`DataType`] or `length` of any of the chunks.
487    /// And the `null_count` remains correct.
488    #[inline]
489    pub unsafe fn chunks_mut(&mut self) -> &mut Vec<ArrayRef> {
490        &mut self.chunks
491    }
492
493    /// Returns true if contains a single chunk and has no null values
494    pub fn is_optimal_aligned(&self) -> bool {
495        self.chunks.len() == 1 && self.null_count() == 0
496    }
497
498    /// Create a new [`ChunkedArray`] from self, where the chunks are replaced.
499    ///
500    /// # Safety
501    /// The caller must ensure the dtypes of the chunks are correct
502    unsafe fn copy_with_chunks(&self, chunks: Vec<ArrayRef>) -> Self {
503        Self::new_with_compute_len(self.field.clone(), chunks)
504    }
505
506    /// Get data type of [`ChunkedArray`].
507    pub fn dtype(&self) -> &DataType {
508        self.field.dtype()
509    }
510
511    pub(crate) unsafe fn set_dtype(&mut self, dtype: DataType) {
512        self.field = Arc::new(Field::new(self.name().clone(), dtype))
513    }
514
515    /// Name of the [`ChunkedArray`].
516    pub fn name(&self) -> &PlSmallStr {
517        self.field.name()
518    }
519
520    /// Get a reference to the field.
521    pub fn ref_field(&self) -> &Field {
522        &self.field
523    }
524
525    /// Rename this [`ChunkedArray`].
526    pub fn rename(&mut self, name: PlSmallStr) {
527        self.field = Arc::new(Field::new(name, self.field.dtype().clone()));
528    }
529
530    /// Return this [`ChunkedArray`] with a new name.
531    pub fn with_name(mut self, name: PlSmallStr) -> Self {
532        self.rename(name);
533        self
534    }
535}
536
537impl<T> ChunkedArray<T>
538where
539    T: PolarsDataType,
540{
541    /// Get a single value from this [`ChunkedArray`]. If the return values is `None` this
542    /// indicates a NULL value.
543    ///
544    /// # Panics
545    /// This function will panic if `idx` is out of bounds.
546    #[inline]
547    pub fn get(&self, idx: usize) -> Option<T::Physical<'_>> {
548        let (chunk_idx, arr_idx) = self.index_to_chunked_index(idx);
549        assert!(
550            chunk_idx < self.chunks().len(),
551            "index: {} out of bounds for len: {}",
552            idx,
553            self.len()
554        );
555        unsafe {
556            let arr = self.downcast_get_unchecked(chunk_idx);
557            assert!(
558                arr_idx < arr.len(),
559                "index: {} out of bounds for len: {}",
560                idx,
561                self.len()
562            );
563            arr.get_unchecked(arr_idx)
564        }
565    }
566
567    /// Get a single value from this [`ChunkedArray`]. If the return values is `None` this
568    /// indicates a NULL value.
569    ///
570    /// # Safety
571    /// It is the callers responsibility that the `idx < self.len()`.
572    #[inline]
573    pub unsafe fn get_unchecked(&self, idx: usize) -> Option<T::Physical<'_>> {
574        let (chunk_idx, arr_idx) = self.index_to_chunked_index(idx);
575
576        unsafe {
577            // SAFETY: up to the caller to make sure the index is valid.
578            self.downcast_get_unchecked(chunk_idx)
579                .get_unchecked(arr_idx)
580        }
581    }
582
583    /// Get a single value from this [`ChunkedArray`]. Null values are ignored and the returned
584    /// value could be garbage if it was masked out by NULL. Note that the value always is initialized.
585    ///
586    /// # Safety
587    /// It is the callers responsibility that the `idx < self.len()`.
588    #[inline]
589    pub unsafe fn value_unchecked(&self, idx: usize) -> T::Physical<'_> {
590        let (chunk_idx, arr_idx) = self.index_to_chunked_index(idx);
591
592        unsafe {
593            // SAFETY: up to the caller to make sure the index is valid.
594            self.downcast_get_unchecked(chunk_idx)
595                .value_unchecked(arr_idx)
596        }
597    }
598
599    /// # Panics
600    /// Panics if the [`ChunkedArray`] is empty.
601    #[inline]
602    pub fn first(&self) -> Option<T::Physical<'_>> {
603        self.iter().next().unwrap()
604    }
605
606    /// # Panics
607    /// Panics if the [`ChunkedArray`] is empty.
608    #[inline]
609    pub fn last(&self) -> Option<T::Physical<'_>> {
610        let arr = self
611            .downcast_iter()
612            .rev()
613            .find(|arr| !arr.is_empty())
614            .unwrap();
615        unsafe { arr.get_unchecked(arr.len() - 1) }
616    }
617
618    pub fn set_validity(&mut self, validity: Option<Bitmap>) {
619        assert!(
620            !self.dtype().is_struct(),
621            "set_outer_validity should be used for struct types"
622        );
623        if let Some(v) = &validity {
624            assert_eq!(self.len(), v.len());
625        }
626        let mut i = 0;
627        for chunk in unsafe { self.chunks_mut() } {
628            *chunk =
629                chunk.with_validity(validity.as_ref().map(|v| v.clone().sliced(i, chunk.len())));
630            i += chunk.len();
631        }
632        self.null_count = validity.map(|v| v.unset_bits()).unwrap_or(0);
633        self.set_fast_explode_list(false);
634    }
635
636    pub fn with_validity(mut self, validity: Option<Bitmap>) -> Self {
637        self.set_validity(validity);
638        self
639    }
640}
641
642impl<T> ChunkedArray<T>
643where
644    T: PolarsDataType,
645    ChunkedArray<T>: ChunkTakeUnchecked<[IdxSize]>,
646{
647    /// Deposit values into nulls with a certain validity mask.
648    pub fn deposit(&self, validity: &Bitmap) -> Self {
649        let set_bits = validity.set_bits();
650
651        assert_eq!(self.len(), set_bits);
652
653        if set_bits == validity.len() {
654            return self.clone();
655        }
656
657        if set_bits == 0 {
658            return Self::full_null_like(self, validity.len());
659        }
660
661        let mut null_mask = validity.clone();
662
663        let mut gather_idxs = Vec::with_capacity(validity.len());
664        let leading_nulls = null_mask.take_leading_zeros();
665        gather_idxs.extend(std::iter::repeat_n(0, leading_nulls + 1));
666
667        let mut i = 0 as IdxSize;
668        gather_idxs.extend(null_mask.iter().skip(1).map(|v| {
669            i += IdxSize::from(v);
670            i
671        }));
672
673        let mut ca = unsafe { ChunkTakeUnchecked::take_unchecked(self, &gather_idxs) };
674        ca.set_validity(combine_validities_and(
675            Some(validity),
676            ca.rechunk_validity().as_ref(),
677        ));
678        ca
679    }
680}
681
682impl ListChunked {
683    #[inline]
684    pub fn get_as_series(&self, idx: usize) -> Option<Series> {
685        unsafe {
686            Some(Series::from_chunks_and_dtype_unchecked(
687                self.name().clone(),
688                vec![self.get(idx)?],
689                &self.inner_dtype().to_physical(),
690            ))
691        }
692    }
693
694    pub fn has_empty_lists(&self) -> bool {
695        for arr in self.downcast_iter() {
696            if arr.is_empty() {
697                continue;
698            }
699
700            if match arr.validity() {
701                None => arr.offsets().lengths().any(|l| l == 0),
702                Some(validity) => arr
703                    .offsets()
704                    .lengths()
705                    .enumerate()
706                    .any(|(i, l)| l == 0 && unsafe { validity.get_bit_unchecked(i) }),
707            } {
708                return true;
709            }
710        }
711
712        false
713    }
714
715    pub fn has_masked_out_values(&self) -> bool {
716        for arr in self.downcast_iter() {
717            if arr.is_empty() {
718                continue;
719            }
720
721            if *arr.offsets().first() != 0 || *arr.offsets().last() != arr.values().len() as i64 {
722                return true;
723            }
724
725            let Some(validity) = arr.validity() else {
726                continue;
727            };
728            if validity.set_bits() == 0 {
729                continue;
730            }
731
732            // @Performance: false_idx_iter
733            for i in (!validity).true_idx_iter() {
734                if arr.offsets().length_at(i) > 0 {
735                    return true;
736                }
737            }
738        }
739
740        false
741    }
742}
743
744#[cfg(feature = "dtype-array")]
745impl ArrayChunked {
746    #[inline]
747    pub fn get_as_series(&self, idx: usize) -> Option<Series> {
748        unsafe {
749            Some(Series::from_chunks_and_dtype_unchecked(
750                self.name().clone(),
751                vec![self.get(idx)?],
752                &self.inner_dtype().to_physical(),
753            ))
754        }
755    }
756
757    pub fn from_aligned_values(
758        name: PlSmallStr,
759        inner_dtype: &DataType,
760        width: usize,
761        chunks: Vec<ArrayRef>,
762        length: usize,
763    ) -> Self {
764        let dtype = DataType::Array(Box::new(inner_dtype.clone()), width);
765        let arrow_dtype = inner_dtype
766            .to_physical()
767            .to_arrow(CompatLevel::newest())
768            .to_fixed_size_list(width, true);
769        let field = Arc::new(Field::new(name, dtype));
770        if width == 0 {
771            use arrow::array::builder::{ArrayBuilder, make_builder};
772            let values = make_builder(&inner_dtype.to_arrow(CompatLevel::newest())).freeze();
773            return ArrayChunked::new_with_compute_len(
774                field,
775                vec![FixedSizeListArray::new(arrow_dtype, length, values, None).into_boxed()],
776            );
777        }
778        let mut total_len = 0;
779        let chunks = chunks
780            .iter()
781            .map(|chunk| {
782                debug_assert_eq!(chunk.len() % width, 0);
783                let chunk_len = chunk.len() / width;
784                total_len += chunk_len;
785                FixedSizeListArray::new(arrow_dtype.clone(), chunk_len, chunk.clone(), None)
786                    .into_boxed()
787            })
788            .collect();
789        debug_assert_eq!(total_len, length);
790
791        unsafe { Self::new_with_dims(field, chunks, length, 0) }
792    }
793
794    /// Turn the ArrayChunked into the ListChunked with the same items.
795    ///
796    /// This will always zero copy the values into the ListChunked.
797    pub fn to_list(&self) -> ListChunked {
798        let inner_dtype = self.inner_dtype();
799        let chunks = self
800            .downcast_iter()
801            .map(|chunk| {
802                use arrow::offset::OffsetsBuffer;
803
804                let inner_dtype = chunk.dtype().inner_dtype().unwrap();
805                let dtype = inner_dtype.clone().to_large_list(true);
806
807                let offsets = (0..=chunk.len())
808                    .map(|i| (i * self.width()) as i64)
809                    .collect::<Vec<i64>>();
810
811                // SAFETY: We created our offsets in ascending manner.
812                let offsets = unsafe { OffsetsBuffer::new_unchecked(offsets.into()) };
813
814                ListArray::<i64>::new(
815                    dtype,
816                    offsets,
817                    chunk.values().clone(),
818                    chunk.validity().cloned(),
819                )
820                .into_boxed()
821            })
822            .collect();
823
824        // SAFETY: All the items were mapped 1-1 with the validity staying the same.
825        let mut ca = unsafe {
826            ListChunked::new_with_dims(
827                Arc::new(Field::new(
828                    self.name().clone(),
829                    DataType::List(Box::new(inner_dtype.clone())),
830                )),
831                chunks,
832                self.len(),
833                self.null_count(),
834            )
835        };
836        ca.set_fast_explode_list(!self.has_nulls());
837        ca
838    }
839}
840
841impl<T> ChunkedArray<T>
842where
843    T: PolarsDataType,
844{
845    /// Should be used to match the chunk_id of another [`ChunkedArray`].
846    /// # Panics
847    /// It is the callers responsibility to ensure that this [`ChunkedArray`] has a single chunk.
848    pub fn match_chunks<I>(&self, chunk_id: I) -> Self
849    where
850        I: Iterator<Item = usize>,
851    {
852        debug_assert!(self.chunks.len() == 1);
853        // Takes a ChunkedArray containing a single chunk.
854        let slice = |ca: &Self| {
855            let array = &ca.chunks[0];
856
857            let mut offset = 0;
858            let chunks = chunk_id
859                .map(|len| {
860                    // SAFETY: within bounds.
861                    debug_assert!((offset + len) <= array.len());
862                    let out = unsafe { array.sliced_unchecked(offset, len) };
863                    offset += len;
864                    out
865                })
866                .collect();
867
868            debug_assert_eq!(offset, array.len());
869
870            // SAFETY: We just slice the original chunks, their type will not change.
871            unsafe {
872                Self::from_chunks_and_dtype(self.name().clone(), chunks, self.dtype().clone())
873            }
874        };
875
876        if self.chunks.len() != 1 {
877            let out = self.rechunk();
878            slice(&out)
879        } else {
880            slice(self)
881        }
882    }
883}
884
885impl<T: PolarsDataType> AsRefDataType for ChunkedArray<T> {
886    fn as_ref_dtype(&self) -> &DataType {
887        self.dtype()
888    }
889}
890
891pub(crate) trait AsSinglePtr: AsRefDataType {
892    /// Rechunk and return a ptr to the start of the array
893    fn as_single_ptr(&mut self) -> PolarsResult<usize> {
894        polars_bail!(opq = as_single_ptr, self.as_ref_dtype());
895    }
896}
897
898impl<T> AsSinglePtr for ChunkedArray<T>
899where
900    T: PolarsNumericType,
901{
902    fn as_single_ptr(&mut self) -> PolarsResult<usize> {
903        self.rechunk_mut();
904        let a = self.data_views().next().unwrap();
905        let ptr = a.as_ptr();
906        Ok(ptr as usize)
907    }
908}
909
910impl AsSinglePtr for BooleanChunked {}
911impl AsSinglePtr for ListChunked {}
912#[cfg(feature = "dtype-array")]
913impl AsSinglePtr for ArrayChunked {}
914impl AsSinglePtr for StringChunked {}
915impl AsSinglePtr for BinaryChunked {}
916#[cfg(feature = "object")]
917impl<T: PolarsObject> AsSinglePtr for ObjectChunked<T> {}
918
919pub enum ChunkedArrayLayout<'a, T: PolarsDataType> {
920    SingleNoNull(&'a T::Array),
921    Single(&'a T::Array),
922    MultiNoNull(&'a ChunkedArray<T>),
923    Multi(&'a ChunkedArray<T>),
924}
925
926impl<T> ChunkedArray<T>
927where
928    T: PolarsDataType,
929{
930    pub fn layout(&self) -> ChunkedArrayLayout<'_, T> {
931        if self.chunks.len() == 1 {
932            let arr = self.downcast_iter().next().unwrap();
933            return if arr.null_count() == 0 {
934                ChunkedArrayLayout::SingleNoNull(arr)
935            } else {
936                ChunkedArrayLayout::Single(arr)
937            };
938        }
939
940        if self.downcast_iter().all(|a| a.null_count() == 0) {
941            ChunkedArrayLayout::MultiNoNull(self)
942        } else {
943            ChunkedArrayLayout::Multi(self)
944        }
945    }
946}
947
948impl<T> ChunkedArray<T>
949where
950    T: PolarsNumericType,
951{
952    /// Returns the values of the array as a contiguous slice.
953    pub fn cont_slice(&self) -> PolarsResult<&[T::Native]> {
954        polars_ensure!(
955            self.chunks.len() == 1 && self.chunks[0].null_count() == 0,
956            ComputeError: "chunked array is not contiguous"
957        );
958        Ok(self.downcast_iter().next().map(|arr| arr.values()).unwrap())
959    }
960
961    /// Returns the values of the array as a contiguous mutable slice.
962    pub(crate) fn cont_slice_mut(&mut self) -> Option<&mut [T::Native]> {
963        if self.chunks.len() == 1 && self.chunks[0].null_count() == 0 {
964            // SAFETY, we will not swap the PrimitiveArray.
965            let arr = unsafe { self.downcast_iter_mut().next().unwrap() };
966            arr.get_mut_values()
967        } else {
968            None
969        }
970    }
971
972    /// Get slices of the underlying arrow data.
973    /// NOTE: null values should be taken into account by the user of these slices as they are handled
974    /// separately
975    pub fn data_views(&self) -> impl DoubleEndedIterator<Item = &[T::Native]> {
976        self.downcast_iter().map(|arr| arr.values().as_slice())
977    }
978
979    #[allow(clippy::wrong_self_convention)]
980    pub fn into_no_null_iter(
981        &self,
982    ) -> impl '_ + Send + Sync + ExactSizeIterator<Item = T::Native> + DoubleEndedIterator + TrustedLen
983    {
984        // .copied was significantly slower in benchmark, next call did not inline?
985        #[allow(clippy::map_clone)]
986        // we know the iterators len
987        unsafe {
988            self.data_views()
989                .flatten()
990                .map(|v| *v)
991                .trust_my_length(self.len())
992        }
993    }
994}
995
996impl<T: PolarsDataType> Clone for ChunkedArray<T> {
997    fn clone(&self) -> Self {
998        ChunkedArray {
999            field: self.field.clone(),
1000            chunks: self.chunks.clone(),
1001            flags: self.flags.clone(),
1002
1003            _pd: Default::default(),
1004            length: self.length,
1005            null_count: self.null_count,
1006        }
1007    }
1008}
1009
1010impl<T: PolarsDataType> AsRef<ChunkedArray<T>> for ChunkedArray<T> {
1011    fn as_ref(&self) -> &ChunkedArray<T> {
1012        self
1013    }
1014}
1015
1016impl ValueSize for ListChunked {
1017    fn get_values_size(&self) -> usize {
1018        self.chunks
1019            .iter()
1020            .fold(0usize, |acc, arr| acc + arr.get_values_size())
1021    }
1022}
1023
1024#[cfg(feature = "dtype-array")]
1025impl ValueSize for ArrayChunked {
1026    fn get_values_size(&self) -> usize {
1027        self.chunks
1028            .iter()
1029            .fold(0usize, |acc, arr| acc + arr.get_values_size())
1030    }
1031}
1032impl ValueSize for StringChunked {
1033    fn get_values_size(&self) -> usize {
1034        self.chunks
1035            .iter()
1036            .fold(0usize, |acc, arr| acc + arr.get_values_size())
1037    }
1038}
1039
1040impl ValueSize for BinaryOffsetChunked {
1041    fn get_values_size(&self) -> usize {
1042        self.chunks
1043            .iter()
1044            .fold(0usize, |acc, arr| acc + arr.get_values_size())
1045    }
1046}
1047
1048pub(crate) fn to_primitive<T: PolarsNumericType>(
1049    values: Vec<T::Native>,
1050    validity: Option<Bitmap>,
1051) -> PrimitiveArray<T::Native> {
1052    PrimitiveArray::new(
1053        T::get_static_dtype().to_arrow(CompatLevel::newest()),
1054        values.into(),
1055        validity,
1056    )
1057}
1058
1059pub(crate) fn to_array<T: PolarsNumericType>(
1060    values: Vec<T::Native>,
1061    validity: Option<Bitmap>,
1062) -> ArrayRef {
1063    Box::new(to_primitive::<T>(values, validity))
1064}
1065
1066impl<T: PolarsDataType> Default for ChunkedArray<T> {
1067    fn default() -> Self {
1068        let dtype = T::get_static_dtype();
1069        let arrow_dtype = dtype.to_physical().to_arrow(CompatLevel::newest());
1070        ChunkedArray {
1071            field: Arc::new(Field::new(PlSmallStr::EMPTY, dtype)),
1072            // Invariant: always has 1 chunk.
1073            chunks: vec![new_empty_array(arrow_dtype)],
1074            flags: StatisticsFlagsIM::empty(),
1075
1076            _pd: Default::default(),
1077            length: 0,
1078            null_count: 0,
1079        }
1080    }
1081}
1082
1083#[cfg(test)]
1084pub(crate) mod test {
1085    use crate::prelude::*;
1086
1087    pub(crate) fn get_chunked_array() -> Int32Chunked {
1088        ChunkedArray::new(PlSmallStr::from_static("a"), &[1, 2, 3])
1089    }
1090
1091    #[test]
1092    fn test_sort() {
1093        let a = Int32Chunked::new(PlSmallStr::from_static("a"), &[1, 9, 3, 2]);
1094        let b = a
1095            .sort(false)
1096            .iter()
1097            .map(|opt| opt.unwrap())
1098            .collect::<Vec<_>>();
1099        assert_eq!(b, [1, 2, 3, 9]);
1100        let a = StringChunked::new(PlSmallStr::from_static("a"), &["b", "a", "c"]);
1101        let a = a.sort(false);
1102        let b = a.iter().collect::<Vec<_>>();
1103        assert_eq!(b, [Some("a"), Some("b"), Some("c")]);
1104        assert!(a.is_sorted_ascending_flag());
1105    }
1106
1107    #[test]
1108    fn arithmetic() {
1109        let a = &Int32Chunked::new(PlSmallStr::from_static("a"), &[1, 100, 6, 40]);
1110        let b = &Int32Chunked::new(PlSmallStr::from_static("b"), &[-1, 2, 3, 4]);
1111
1112        // Not really asserting anything here but still making sure the code is exercised
1113        // This (and more) is properly tested from the integration test suite and Python bindings.
1114        println!("{:?}", a + b);
1115        println!("{:?}", a - b);
1116        println!("{:?}", a * b);
1117        println!("{:?}", a / b);
1118    }
1119
1120    #[test]
1121    fn iter() {
1122        let s1 = get_chunked_array();
1123        // sum
1124        assert_eq!(s1.iter().fold(0, |acc, val| { acc + val.unwrap() }), 6)
1125    }
1126
1127    #[test]
1128    fn limit() {
1129        let a = get_chunked_array();
1130        let b = a.limit(2);
1131        println!("{b:?}");
1132        assert_eq!(b.len(), 2)
1133    }
1134
1135    #[test]
1136    fn filter() {
1137        let a = get_chunked_array();
1138        let b = a
1139            .filter(&BooleanChunked::new(
1140                PlSmallStr::from_static("filter"),
1141                &[true, false, false],
1142            ))
1143            .unwrap();
1144        assert_eq!(b.len(), 1);
1145        assert_eq!(b.iter().next(), Some(Some(1)));
1146    }
1147
1148    #[test]
1149    fn aggregates() {
1150        let a = &Int32Chunked::new(PlSmallStr::from_static("a"), &[1, 100, 10, 9]);
1151        assert_eq!(a.max(), Some(100));
1152        assert_eq!(a.min(), Some(1));
1153        assert_eq!(a.sum(), Some(120))
1154    }
1155
1156    #[test]
1157    fn take() {
1158        let a = get_chunked_array();
1159        let new = a.take(&[0 as IdxSize, 1]).unwrap();
1160        assert_eq!(new.len(), 2)
1161    }
1162
1163    #[test]
1164    fn cast() {
1165        let a = get_chunked_array();
1166        let b = a.cast(&DataType::Int64).unwrap();
1167        assert_eq!(b.dtype(), &DataType::Int64)
1168    }
1169
1170    fn assert_slice_equal<T>(ca: &ChunkedArray<T>, eq: &[T::Native])
1171    where
1172        T: PolarsNumericType,
1173    {
1174        assert_eq!(ca.iter().map(|opt| opt.unwrap()).collect::<Vec<_>>(), eq)
1175    }
1176
1177    #[test]
1178    fn slice() {
1179        let mut first = UInt32Chunked::new(PlSmallStr::from_static("first"), &[0, 1, 2]);
1180        let second = UInt32Chunked::new(PlSmallStr::from_static("second"), &[3, 4, 5]);
1181        first.append(&second).unwrap();
1182        assert_slice_equal(&first.slice(0, 3), &[0, 1, 2]);
1183        assert_slice_equal(&first.slice(0, 4), &[0, 1, 2, 3]);
1184        assert_slice_equal(&first.slice(1, 4), &[1, 2, 3, 4]);
1185        assert_slice_equal(&first.slice(3, 2), &[3, 4]);
1186        assert_slice_equal(&first.slice(3, 3), &[3, 4, 5]);
1187        assert_slice_equal(&first.slice(-3, 3), &[3, 4, 5]);
1188        assert_slice_equal(&first.slice(-6, 6), &[0, 1, 2, 3, 4, 5]);
1189
1190        assert_eq!(first.slice(-7, 2).len(), 1);
1191        assert_eq!(first.slice(-3, 4).len(), 3);
1192        assert_eq!(first.slice(3, 4).len(), 3);
1193        assert_eq!(first.slice(10, 4).len(), 0);
1194    }
1195
1196    #[test]
1197    fn sorting() {
1198        let s = UInt32Chunked::new(PlSmallStr::EMPTY, &[9, 2, 4]);
1199        let sorted = s.sort(false);
1200        assert_slice_equal(&sorted, &[2, 4, 9]);
1201        let sorted = s.sort(true);
1202        assert_slice_equal(&sorted, &[9, 4, 2]);
1203
1204        let s: StringChunked = ["b", "a", "z"].iter().collect();
1205        let sorted = s.sort(false);
1206        assert_eq!(
1207            sorted.iter().collect::<Vec<_>>(),
1208            &[Some("a"), Some("b"), Some("z")]
1209        );
1210        let sorted = s.sort(true);
1211        assert_eq!(
1212            sorted.iter().collect::<Vec<_>>(),
1213            &[Some("z"), Some("b"), Some("a")]
1214        );
1215        let s: StringChunked = [Some("b"), None, Some("z")].iter().copied().collect();
1216        let sorted = s.sort(false);
1217        assert_eq!(
1218            sorted.iter().collect::<Vec<_>>(),
1219            &[None, Some("b"), Some("z")]
1220        );
1221    }
1222
1223    #[test]
1224    fn reverse() {
1225        let s = UInt32Chunked::new(PlSmallStr::EMPTY, &[1, 2, 3]);
1226        // path with continuous slice
1227        assert_slice_equal(&s.reverse(), &[3, 2, 1]);
1228        // path with options
1229        let s = UInt32Chunked::new(PlSmallStr::EMPTY, &[Some(1), None, Some(3)]);
1230        assert_eq!(Vec::from(&s.reverse()), &[Some(3), None, Some(1)]);
1231        let s = BooleanChunked::new(PlSmallStr::EMPTY, &[true, false]);
1232        assert_eq!(Vec::from(&s.reverse()), &[Some(false), Some(true)]);
1233
1234        let s = StringChunked::new(PlSmallStr::EMPTY, &["a", "b", "c"]);
1235        assert_eq!(Vec::from(&s.reverse()), &[Some("c"), Some("b"), Some("a")]);
1236
1237        let s = StringChunked::new(PlSmallStr::EMPTY, &[Some("a"), None, Some("c")]);
1238        assert_eq!(Vec::from(&s.reverse()), &[Some("c"), None, Some("a")]);
1239    }
1240
1241    #[test]
1242    #[cfg(feature = "dtype-categorical")]
1243    fn test_iter_categorical() {
1244        let ca = StringChunked::new(
1245            PlSmallStr::EMPTY,
1246            &[Some("foo"), None, Some("bar"), Some("ham")],
1247        );
1248        let cats = Categories::new(
1249            PlSmallStr::EMPTY,
1250            PlSmallStr::EMPTY,
1251            CategoricalPhysical::U32,
1252        );
1253        let ca = ca.cast(&DataType::from_categories(cats)).unwrap();
1254        let ca = ca.cat32().unwrap();
1255        let v: Vec<_> = ca.physical().iter().collect();
1256        assert_eq!(v, &[Some(0), None, Some(1), Some(2)]);
1257    }
1258
1259    #[test]
1260    #[ignore]
1261    fn test_shrink_to_fit() {
1262        let mut builder = StringChunkedBuilder::new(PlSmallStr::from_static("foo"), 2048);
1263        builder.append_value("foo");
1264        let mut arr = builder.finish();
1265        let before = arr
1266            .chunks()
1267            .iter()
1268            .map(|arr| arrow::compute::aggregate::estimated_bytes_size(arr.as_ref()))
1269            .sum::<usize>();
1270        arr.shrink_to_fit();
1271        let after = arr
1272            .chunks()
1273            .iter()
1274            .map(|arr| arrow::compute::aggregate::estimated_bytes_size(arr.as_ref()))
1275            .sum::<usize>();
1276        assert!(before > after);
1277    }
1278}