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