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    pub fn deposit(&self, validity: &Bitmap) -> Self {
688        let set_bits = validity.set_bits();
689
690        assert_eq!(self.len(), set_bits);
691
692        if set_bits == validity.len() {
693            return self.clone();
694        }
695
696        if set_bits == 0 {
697            return Self::full_null_like(self, validity.len());
698        }
699
700        let mut null_mask = validity.clone();
701
702        let mut gather_idxs = Vec::with_capacity(validity.len());
703        let leading_nulls = null_mask.take_leading_zeros();
704        gather_idxs.extend(std::iter::repeat_n(0, leading_nulls + 1));
705
706        let mut i = 0 as IdxSize;
707        gather_idxs.extend(null_mask.iter().skip(1).map(|v| {
708            i += IdxSize::from(v);
709            i
710        }));
711
712        let mut ca = unsafe { ChunkTakeUnchecked::take_unchecked(self, &gather_idxs) };
713        ca.set_validity(combine_validities_and(
714            Some(validity),
715            ca.rechunk_validity().as_ref(),
716        ));
717        ca
718    }
719}
720
721impl ListChunked {
722    #[inline]
723    pub fn get_as_series(&self, idx: usize) -> Option<Series> {
724        unsafe {
725            Some(Series::from_chunks_and_dtype_unchecked(
726                self.name().clone(),
727                vec![self.get(idx)?],
728                &self.inner_dtype().to_physical(),
729            ))
730        }
731    }
732
733    pub fn has_empty_lists(&self) -> bool {
734        for arr in self.downcast_iter() {
735            if arr.is_empty() {
736                continue;
737            }
738
739            if match arr.validity() {
740                None => arr.offsets().lengths().any(|l| l == 0),
741                Some(validity) => arr
742                    .offsets()
743                    .lengths()
744                    .enumerate()
745                    .any(|(i, l)| l == 0 && unsafe { validity.get_bit_unchecked(i) }),
746            } {
747                return true;
748            }
749        }
750
751        false
752    }
753
754    pub fn has_masked_out_values(&self) -> bool {
755        for arr in self.downcast_iter() {
756            if arr.is_empty() {
757                continue;
758            }
759
760            if *arr.offsets().first() != 0 || *arr.offsets().last() != arr.values().len() as i64 {
761                return true;
762            }
763
764            let Some(validity) = arr.validity() else {
765                continue;
766            };
767            if validity.set_bits() == 0 {
768                continue;
769            }
770
771            // @Performance: false_idx_iter
772            for i in (!validity).true_idx_iter() {
773                if arr.offsets().length_at(i) > 0 {
774                    return true;
775                }
776            }
777        }
778
779        false
780    }
781}
782
783#[cfg(feature = "dtype-array")]
784impl ArrayChunked {
785    #[inline]
786    pub fn get_as_series(&self, idx: usize) -> Option<Series> {
787        unsafe {
788            Some(Series::from_chunks_and_dtype_unchecked(
789                self.name().clone(),
790                vec![self.get(idx)?],
791                &self.inner_dtype().to_physical(),
792            ))
793        }
794    }
795
796    pub fn from_aligned_values(
797        name: PlSmallStr,
798        inner_dtype: &DataType,
799        width: usize,
800        chunks: Vec<ArrayRef>,
801        length: usize,
802    ) -> Self {
803        let dtype = DataType::Array(Box::new(inner_dtype.clone()), width);
804        let arrow_dtype = inner_dtype
805            .to_physical()
806            .to_arrow(CompatLevel::newest())
807            .to_fixed_size_list(width, true);
808        let field = Arc::new(Field::new(name, dtype));
809        if width == 0 {
810            use polars_arrow::array::builder::{ArrayBuilder, make_builder};
811            let values = make_builder(&inner_dtype.to_arrow(CompatLevel::newest())).freeze();
812            return ArrayChunked::new_with_compute_len(
813                field,
814                vec![FixedSizeListArray::new(arrow_dtype, length, values, None).into_boxed()],
815            );
816        }
817        let mut total_len = 0;
818        let chunks = chunks
819            .iter()
820            .map(|chunk| {
821                debug_assert_eq!(chunk.len() % width, 0);
822                let chunk_len = chunk.len() / width;
823                total_len += chunk_len;
824                FixedSizeListArray::new(arrow_dtype.clone(), chunk_len, chunk.clone(), None)
825                    .into_boxed()
826            })
827            .collect();
828        debug_assert_eq!(total_len, length);
829
830        unsafe { Self::new_with_dims(field, chunks, length, 0) }
831    }
832
833    /// Turn the ArrayChunked into the ListChunked with the same items.
834    ///
835    /// This will always zero copy the values into the ListChunked.
836    pub fn to_list(&self) -> ListChunked {
837        let inner_dtype = self.inner_dtype();
838        let chunks = self
839            .downcast_iter()
840            .map(|chunk| {
841                use polars_arrow::offset::OffsetsBuffer;
842
843                let inner_dtype = chunk.dtype().inner_dtype().unwrap();
844                let dtype = inner_dtype.clone().to_large_list(true);
845
846                let offsets = (0..=chunk.len())
847                    .map(|i| (i * self.width()) as i64)
848                    .collect::<Vec<i64>>();
849
850                // SAFETY: We created our offsets in ascending manner.
851                let offsets = unsafe { OffsetsBuffer::new_unchecked(offsets.into()) };
852
853                ListArray::<i64>::new(
854                    dtype,
855                    offsets,
856                    chunk.values().clone(),
857                    chunk.validity().cloned(),
858                )
859                .into_boxed()
860            })
861            .collect();
862
863        // SAFETY: All the items were mapped 1-1 with the validity staying the same.
864        let mut ca = unsafe {
865            ListChunked::new_with_dims(
866                Arc::new(Field::new(
867                    self.name().clone(),
868                    DataType::List(Box::new(inner_dtype.clone())),
869                )),
870                chunks,
871                self.len(),
872                self.null_count(),
873            )
874        };
875        ca.set_fast_explode_list(!self.has_nulls());
876        ca
877    }
878}
879
880impl<T> ChunkedArray<T>
881where
882    T: PolarsDataType,
883{
884    /// Should be used to match the chunk_id of another [`ChunkedArray`].
885    /// # Panics
886    /// It is the callers responsibility to ensure that this [`ChunkedArray`] has a single chunk.
887    pub fn match_chunks<I>(&self, chunk_id: I) -> Self
888    where
889        I: Iterator<Item = usize>,
890    {
891        debug_assert!(self.chunks.len() == 1);
892        // Takes a ChunkedArray containing a single chunk.
893        let slice = |ca: &Self| {
894            let array = &ca.chunks[0];
895
896            let mut offset = 0;
897            let chunks = chunk_id
898                .map(|len| {
899                    // SAFETY: within bounds.
900                    debug_assert!((offset + len) <= array.len());
901                    let out = unsafe { array.sliced_unchecked(offset, len) };
902                    offset += len;
903                    out
904                })
905                .collect();
906
907            debug_assert_eq!(offset, array.len());
908
909            // SAFETY: We just slice the original chunks, their type will not change.
910            unsafe {
911                Self::from_chunks_and_dtype(self.name().clone(), chunks, self.dtype().clone())
912            }
913        };
914
915        if self.chunks.len() != 1 {
916            let out = self.rechunk();
917            slice(&out)
918        } else {
919            slice(self)
920        }
921    }
922}
923
924impl<T: PolarsDataType> AsRefDataType for ChunkedArray<T> {
925    fn as_ref_dtype(&self) -> &DataType {
926        self.dtype()
927    }
928}
929
930pub(crate) trait AsSinglePtr: AsRefDataType {
931    /// Rechunk and return a ptr to the start of the array
932    fn as_single_ptr(&mut self) -> PolarsResult<usize> {
933        polars_bail!(opq = as_single_ptr, self.as_ref_dtype());
934    }
935}
936
937impl<T> AsSinglePtr for ChunkedArray<T>
938where
939    T: PolarsNumericType,
940{
941    fn as_single_ptr(&mut self) -> PolarsResult<usize> {
942        self.rechunk_mut();
943        let a = self.data_views().next().unwrap();
944        let ptr = a.as_ptr();
945        Ok(ptr as usize)
946    }
947}
948
949impl AsSinglePtr for BooleanChunked {}
950impl AsSinglePtr for ListChunked {}
951#[cfg(feature = "dtype-array")]
952impl AsSinglePtr for ArrayChunked {}
953impl AsSinglePtr for StringChunked {}
954impl AsSinglePtr for BinaryChunked {}
955#[cfg(feature = "object")]
956impl<T: PolarsObject> AsSinglePtr for ObjectChunked<T> {}
957
958pub enum ChunkedArrayLayout<'a, T: PolarsDataType> {
959    SingleNoNull(&'a T::Array),
960    Single(&'a T::Array),
961    MultiNoNull(&'a ChunkedArray<T>),
962    Multi(&'a ChunkedArray<T>),
963}
964
965impl<T> ChunkedArray<T>
966where
967    T: PolarsDataType,
968{
969    pub fn layout(&self) -> ChunkedArrayLayout<'_, T> {
970        if self.chunks.len() == 1 {
971            let arr = self.downcast_iter().next().unwrap();
972            return if arr.null_count() == 0 {
973                ChunkedArrayLayout::SingleNoNull(arr)
974            } else {
975                ChunkedArrayLayout::Single(arr)
976            };
977        }
978
979        if self.downcast_iter().all(|a| a.null_count() == 0) {
980            ChunkedArrayLayout::MultiNoNull(self)
981        } else {
982            ChunkedArrayLayout::Multi(self)
983        }
984    }
985}
986
987impl<T> ChunkedArray<T>
988where
989    T: PolarsNumericType,
990{
991    /// Returns the values of the array as a contiguous slice.
992    pub fn cont_slice(&self) -> PolarsResult<&[T::Native]> {
993        polars_ensure!(
994            self.chunks.len() == 1 && self.chunks[0].null_count() == 0,
995            ComputeError: "chunked array is not contiguous"
996        );
997        Ok(self.downcast_iter().next().map(|arr| arr.values()).unwrap())
998    }
999
1000    /// Returns the values of the array as a contiguous mutable slice.
1001    pub(crate) fn cont_slice_mut(&mut self) -> Option<&mut [T::Native]> {
1002        if self.chunks.len() == 1 && self.chunks[0].null_count() == 0 {
1003            // SAFETY, we will not swap the PrimitiveArray.
1004            let arr = unsafe { self.downcast_iter_mut().next().unwrap() };
1005            arr.get_mut_values()
1006        } else {
1007            None
1008        }
1009    }
1010
1011    /// Get slices of the underlying arrow data.
1012    /// NOTE: null values should be taken into account by the user of these slices as they are handled
1013    /// separately
1014    pub fn data_views(&self) -> impl DoubleEndedIterator<Item = &[T::Native]> {
1015        self.downcast_iter().map(|arr| arr.values().as_slice())
1016    }
1017
1018    #[allow(clippy::wrong_self_convention)]
1019    pub fn into_no_null_iter(
1020        &self,
1021    ) -> impl '_ + Send + Sync + ExactSizeIterator<Item = T::Native> + DoubleEndedIterator + TrustedLen
1022    {
1023        // .copied was significantly slower in benchmark, next call did not inline?
1024        #[allow(clippy::map_clone)]
1025        // we know the iterators len
1026        unsafe {
1027            self.data_views()
1028                .flatten()
1029                .map(|v| *v)
1030                .trust_my_length(self.len())
1031        }
1032    }
1033}
1034
1035impl<T> ChunkedArray<T>
1036where
1037    T: PolarsIntegerType,
1038    T::Native: Signed,
1039{
1040    /// Whether any value is negative. Masked out values are also checked.
1041    pub fn has_negative(&self) -> bool {
1042        // Check in blocks, so the inner loop can be vectorized and we can still stop early.
1043        self.data_views().any(|values| {
1044            values
1045                .chunks(1024)
1046                .any(|block| block.iter().fold(false, |acc, v| acc | v.is_negative()))
1047        })
1048    }
1049}
1050
1051impl<T: PolarsDataType> Clone for ChunkedArray<T> {
1052    fn clone(&self) -> Self {
1053        ChunkedArray {
1054            field: self.field.clone(),
1055            chunks: self.chunks.clone(),
1056            flags: self.flags.clone(),
1057
1058            _pd: Default::default(),
1059            length: self.length,
1060            null_count: self.null_count,
1061        }
1062    }
1063}
1064
1065impl<T: PolarsDataType> AsRef<ChunkedArray<T>> for ChunkedArray<T> {
1066    fn as_ref(&self) -> &ChunkedArray<T> {
1067        self
1068    }
1069}
1070
1071impl ValueSize for ListChunked {
1072    fn get_values_size(&self) -> usize {
1073        self.chunks
1074            .iter()
1075            .fold(0usize, |acc, arr| acc + arr.get_values_size())
1076    }
1077}
1078
1079#[cfg(feature = "dtype-array")]
1080impl ValueSize for ArrayChunked {
1081    fn get_values_size(&self) -> usize {
1082        self.chunks
1083            .iter()
1084            .fold(0usize, |acc, arr| acc + arr.get_values_size())
1085    }
1086}
1087impl ValueSize for StringChunked {
1088    fn get_values_size(&self) -> usize {
1089        self.chunks
1090            .iter()
1091            .fold(0usize, |acc, arr| acc + arr.get_values_size())
1092    }
1093}
1094
1095impl ValueSize for BinaryOffsetChunked {
1096    fn get_values_size(&self) -> usize {
1097        self.chunks
1098            .iter()
1099            .fold(0usize, |acc, arr| acc + arr.get_values_size())
1100    }
1101}
1102
1103/// Re-chunk `values` so that its chunk lengths match `chunk_lens`.
1104///
1105/// The sum of `chunk_lens` must equal `values.len()`. Returns a clone when the chunks
1106/// already line up, so passing already-aligned values costs nothing.
1107pub(crate) fn align_inner_chunks(
1108    chunk_lens: impl Iterator<Item = usize>,
1109    values: &Series,
1110) -> Series {
1111    let chunk_lens = chunk_lens.collect::<Vec<_>>();
1112
1113    if chunk_lens.len() == values.chunks().len()
1114        && chunk_lens
1115            .iter()
1116            .zip(values.chunks())
1117            .all(|(len, arr)| *len == arr.len())
1118    {
1119        return values.clone();
1120    }
1121
1122    let mut values = values.rechunk();
1123    let chunks = unsafe { values.chunks_mut() };
1124    let mut arr = chunks.pop().unwrap();
1125    chunks.extend(chunk_lens.into_iter().map(|len| {
1126        let chunk;
1127        (chunk, arr) = arr.split_at_boxed(len);
1128        chunk
1129    }));
1130    assert!(arr.is_empty());
1131    values
1132}
1133
1134pub(crate) fn to_primitive<T: PolarsNumericType>(
1135    values: Vec<T::Native>,
1136    validity: Option<Bitmap>,
1137) -> PrimitiveArray<T::Native> {
1138    PrimitiveArray::new(
1139        T::get_static_dtype().to_arrow(CompatLevel::newest()),
1140        values.into(),
1141        validity,
1142    )
1143}
1144
1145pub(crate) fn to_array<T: PolarsNumericType>(
1146    values: Vec<T::Native>,
1147    validity: Option<Bitmap>,
1148) -> ArrayRef {
1149    Box::new(to_primitive::<T>(values, validity))
1150}
1151
1152impl<T: PolarsDataType> Default for ChunkedArray<T> {
1153    fn default() -> Self {
1154        let dtype = T::get_static_dtype();
1155        let arrow_dtype = dtype.to_physical().to_arrow(CompatLevel::newest());
1156        ChunkedArray {
1157            field: Arc::new(Field::new(PlSmallStr::EMPTY, dtype)),
1158            // Invariant: always has 1 chunk.
1159            chunks: vec![new_empty_array(arrow_dtype)],
1160            flags: StatisticsFlagsIM::empty(),
1161
1162            _pd: Default::default(),
1163            length: 0,
1164            null_count: 0,
1165        }
1166    }
1167}
1168
1169impl<T: PolarsDataType> BroadcastLength for ChunkedArray<T> {
1170    fn _broadcast_len(&self) -> usize {
1171        self.len()
1172    }
1173
1174    fn _column_name(&self) -> Option<&str> {
1175        Some(self.name())
1176    }
1177}
1178
1179#[cfg(test)]
1180pub(crate) mod test {
1181    use crate::prelude::*;
1182
1183    pub(crate) fn get_chunked_array() -> Int32Chunked {
1184        ChunkedArray::new(PlSmallStr::from_static("a"), &[1, 2, 3])
1185    }
1186
1187    #[test]
1188    fn test_sort() {
1189        let a = Int32Chunked::new(PlSmallStr::from_static("a"), &[1, 9, 3, 2]);
1190        let b = a
1191            .sort(false)
1192            .iter()
1193            .map(|opt| opt.unwrap())
1194            .collect::<Vec<_>>();
1195        assert_eq!(b, [1, 2, 3, 9]);
1196        let a = StringChunked::new(PlSmallStr::from_static("a"), &["b", "a", "c"]);
1197        let a = a.sort(false);
1198        let b = a.iter().collect::<Vec<_>>();
1199        assert_eq!(b, [Some("a"), Some("b"), Some("c")]);
1200        assert!(a.is_sorted_ascending_flag());
1201    }
1202
1203    #[test]
1204    fn arithmetic() {
1205        let a = &Int32Chunked::new(PlSmallStr::from_static("a"), &[1, 100, 6, 40]);
1206        let b = &Int32Chunked::new(PlSmallStr::from_static("b"), &[-1, 2, 3, 4]);
1207
1208        // Not really asserting anything here but still making sure the code is exercised
1209        // This (and more) is properly tested from the integration test suite and Python bindings.
1210        println!("{:?}", a + b);
1211        println!("{:?}", a - b);
1212        println!("{:?}", a * b);
1213        println!("{:?}", a / b);
1214    }
1215
1216    #[test]
1217    fn iter() {
1218        let s1 = get_chunked_array();
1219        // sum
1220        assert_eq!(s1.iter().fold(0, |acc, val| { acc + val.unwrap() }), 6)
1221    }
1222
1223    #[test]
1224    fn limit() {
1225        let a = get_chunked_array();
1226        let b = a.limit(2);
1227        println!("{b:?}");
1228        assert_eq!(b.len(), 2)
1229    }
1230
1231    #[test]
1232    fn filter() {
1233        let a = get_chunked_array();
1234        let b = a
1235            .filter(&BooleanChunked::new(
1236                PlSmallStr::from_static("filter"),
1237                &[true, false, false],
1238            ))
1239            .unwrap();
1240        assert_eq!(b.len(), 1);
1241        assert_eq!(b.iter().next(), Some(Some(1)));
1242    }
1243
1244    #[test]
1245    fn aggregates() {
1246        let a = &Int32Chunked::new(PlSmallStr::from_static("a"), &[1, 100, 10, 9]);
1247        assert_eq!(a.max(), Some(100));
1248        assert_eq!(a.min(), Some(1));
1249        assert_eq!(a.sum(), Some(120))
1250    }
1251
1252    #[test]
1253    fn take() {
1254        let a = get_chunked_array();
1255        let new = a.take(&[0 as IdxSize, 1]).unwrap();
1256        assert_eq!(new.len(), 2)
1257    }
1258
1259    #[test]
1260    fn cast() {
1261        let a = get_chunked_array();
1262        let b = a.cast(&DataType::Int64).unwrap();
1263        assert_eq!(b.dtype(), &DataType::Int64)
1264    }
1265
1266    fn assert_slice_equal<T>(ca: &ChunkedArray<T>, eq: &[T::Native])
1267    where
1268        T: PolarsNumericType,
1269    {
1270        assert_eq!(ca.iter().map(|opt| opt.unwrap()).collect::<Vec<_>>(), eq)
1271    }
1272
1273    #[test]
1274    fn slice() {
1275        let mut first = UInt32Chunked::new(PlSmallStr::from_static("first"), &[0, 1, 2]);
1276        let second = UInt32Chunked::new(PlSmallStr::from_static("second"), &[3, 4, 5]);
1277        first.append(&second).unwrap();
1278        assert_slice_equal(&first.slice(0, 3), &[0, 1, 2]);
1279        assert_slice_equal(&first.slice(0, 4), &[0, 1, 2, 3]);
1280        assert_slice_equal(&first.slice(1, 4), &[1, 2, 3, 4]);
1281        assert_slice_equal(&first.slice(3, 2), &[3, 4]);
1282        assert_slice_equal(&first.slice(3, 3), &[3, 4, 5]);
1283        assert_slice_equal(&first.slice(-3, 3), &[3, 4, 5]);
1284        assert_slice_equal(&first.slice(-6, 6), &[0, 1, 2, 3, 4, 5]);
1285
1286        assert_eq!(first.slice(-7, 2).len(), 1);
1287        assert_eq!(first.slice(-3, 4).len(), 3);
1288        assert_eq!(first.slice(3, 4).len(), 3);
1289        assert_eq!(first.slice(10, 4).len(), 0);
1290    }
1291
1292    #[test]
1293    fn sorting() {
1294        let s = UInt32Chunked::new(PlSmallStr::EMPTY, &[9, 2, 4]);
1295        let sorted = s.sort(false);
1296        assert_slice_equal(&sorted, &[2, 4, 9]);
1297        let sorted = s.sort(true);
1298        assert_slice_equal(&sorted, &[9, 4, 2]);
1299
1300        let s: StringChunked = ["b", "a", "z"].iter().collect();
1301        let sorted = s.sort(false);
1302        assert_eq!(
1303            sorted.iter().collect::<Vec<_>>(),
1304            &[Some("a"), Some("b"), Some("z")]
1305        );
1306        let sorted = s.sort(true);
1307        assert_eq!(
1308            sorted.iter().collect::<Vec<_>>(),
1309            &[Some("z"), Some("b"), Some("a")]
1310        );
1311        let s: StringChunked = [Some("b"), None, Some("z")].iter().copied().collect();
1312        let sorted = s.sort(false);
1313        assert_eq!(
1314            sorted.iter().collect::<Vec<_>>(),
1315            &[None, Some("b"), Some("z")]
1316        );
1317    }
1318
1319    #[test]
1320    fn reverse() {
1321        let s = UInt32Chunked::new(PlSmallStr::EMPTY, &[1, 2, 3]);
1322        // path with continuous slice
1323        assert_slice_equal(&s.reverse(), &[3, 2, 1]);
1324        // path with options
1325        let s = UInt32Chunked::new(PlSmallStr::EMPTY, &[Some(1), None, Some(3)]);
1326        assert_eq!(Vec::from(&s.reverse()), &[Some(3), None, Some(1)]);
1327        let s = BooleanChunked::new(PlSmallStr::EMPTY, &[true, false]);
1328        assert_eq!(Vec::from(&s.reverse()), &[Some(false), Some(true)]);
1329
1330        let s = StringChunked::new(PlSmallStr::EMPTY, &["a", "b", "c"]);
1331        assert_eq!(Vec::from(&s.reverse()), &[Some("c"), Some("b"), Some("a")]);
1332
1333        let s = StringChunked::new(PlSmallStr::EMPTY, &[Some("a"), None, Some("c")]);
1334        assert_eq!(Vec::from(&s.reverse()), &[Some("c"), None, Some("a")]);
1335    }
1336
1337    #[test]
1338    #[cfg(feature = "dtype-categorical")]
1339    fn test_iter_categorical() {
1340        let ca = StringChunked::new(
1341            PlSmallStr::EMPTY,
1342            &[Some("foo"), None, Some("bar"), Some("ham")],
1343        );
1344        let cats = Categories::new(
1345            PlSmallStr::EMPTY,
1346            PlSmallStr::EMPTY,
1347            CategoricalPhysical::U32,
1348        );
1349        let ca = ca.cast(&DataType::from_categories(cats)).unwrap();
1350        let ca = ca.cat32().unwrap();
1351        let v: Vec<_> = ca.physical().iter().collect();
1352        assert_eq!(v, &[Some(0), None, Some(1), Some(2)]);
1353    }
1354
1355    #[test]
1356    #[ignore]
1357    fn test_shrink_to_fit() {
1358        let mut builder = StringChunkedBuilder::new(PlSmallStr::from_static("foo"), 2048);
1359        builder.append_value("foo");
1360        let mut arr = builder.finish();
1361        let before = arr
1362            .chunks()
1363            .iter()
1364            .map(|arr| polars_arrow::compute::aggregate::estimated_bytes_size(arr.as_ref()))
1365            .sum::<usize>();
1366        arr.shrink_to_fit();
1367        let after = arr
1368            .chunks()
1369            .iter()
1370            .map(|arr| polars_arrow::compute::aggregate::estimated_bytes_size(arr.as_ref()))
1371            .sum::<usize>();
1372        assert!(before > after);
1373    }
1374}