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 arrow::array::*;
7use arrow::bitmap::Bitmap;
8use arrow::compute::concatenate::concatenate_unchecked;
9use arrow::compute::utils::combine_validities_and;
10use polars_compute::filter::filter_with_bitmap;
11use polars_utils::broadcast::BroadcastLength;
12
13use crate::prelude::{ChunkTakeUnchecked, *};
14
15pub mod ops;
16#[macro_use]
17pub mod arithmetic;
18pub mod builder;
19pub mod cast;
20pub mod collect;
21pub mod comparison;
22pub mod flags;
23pub mod float;
24pub mod iterator;
25#[cfg(feature = "ndarray")]
26pub(crate) mod ndarray;
27
28pub mod arg_min_max;
29#[cfg(feature = "dtype-array")]
30pub(crate) mod array;
31mod binary;
32mod binary_offset;
33mod bitwise;
34#[cfg(feature = "object")]
35mod drop;
36mod from;
37mod from_iterator;
38pub mod from_iterator_par;
39pub(crate) mod list;
40pub(crate) mod logical;
41#[cfg(feature = "object")]
42pub mod object;
43#[cfg(feature = "random")]
44mod random;
45#[cfg(feature = "dtype-struct")]
46mod struct_;
47#[cfg(any(
48    feature = "temporal",
49    feature = "dtype-datetime",
50    feature = "dtype-date"
51))]
52pub mod temporal;
53mod to_vec;
54mod trusted_len;
55pub(crate) use arg_min_max::*;
56#[cfg(feature = "dtype-struct")]
57pub use struct_::StructChunked;
58
59use self::flags::{StatisticsFlags, StatisticsFlagsIM};
60use crate::series::IsSorted;
61use crate::utils::{first_non_null, first_null, last_non_null};
62
63pub type ChunkLenIter<'a> = std::iter::Map<std::slice::Iter<'a, ArrayRef>, fn(&ArrayRef) -> usize>;
64
65/// # ChunkedArray
66///
67/// Every Series contains a [`ChunkedArray<T>`]. Unlike [`Series`], [`ChunkedArray`]s are typed. This allows
68/// us to apply closures to the data and collect the results to a [`ChunkedArray`] of the same type `T`.
69/// Below we use an apply to use the cosine function to the values of a [`ChunkedArray`].
70///
71/// ```rust
72/// # use polars_core::prelude::*;
73/// fn apply_cosine_and_cast(ca: &Float32Chunked) -> Float32Chunked {
74///     ca.apply_values(|v| v.cos())
75/// }
76/// ```
77///
78/// ## Conversion between Series and ChunkedArrays
79/// Conversion from a [`Series`] to a [`ChunkedArray`] is effortless.
80///
81/// ```rust
82/// # use polars_core::prelude::*;
83/// fn to_chunked_array(series: &Series) -> PolarsResult<&Int32Chunked>{
84///     series.i32()
85/// }
86///
87/// fn to_series(ca: Int32Chunked) -> Series {
88///     ca.into_series()
89/// }
90/// ```
91///
92/// # Iterators
93///
94/// [`ChunkedArray`]s fully support Rust native [Iterator](https://doc.rust-lang.org/std/iter/trait.Iterator.html)
95/// and [DoubleEndedIterator](https://doc.rust-lang.org/std/iter/trait.DoubleEndedIterator.html) traits, thereby
96/// giving access to all the excellent methods available for [Iterators](https://doc.rust-lang.org/std/iter/trait.Iterator.html).
97///
98/// ```rust
99/// # use polars_core::prelude::*;
100///
101/// fn iter_forward(ca: &Float32Chunked) {
102///     ca.iter()
103///         .for_each(|opt_v| println!("{:?}", opt_v))
104/// }
105///
106/// fn iter_backward(ca: &Float32Chunked) {
107///     ca.iter()
108///         .rev()
109///         .for_each(|opt_v| println!("{:?}", opt_v))
110/// }
111/// ```
112///
113/// # Memory layout
114///
115/// [`ChunkedArray`]s use [Apache Arrow](https://github.com/apache/arrow) as backend for the memory layout.
116/// Arrows memory is immutable which makes it possible to make multiple zero copy (sub)-views from a single array.
117///
118/// To be able to append data, Polars uses chunks to append new memory locations, hence the [`ChunkedArray<T>`] data structure.
119/// 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).
120///
121/// However, multiple chunks in a [`ChunkedArray`] will slow down many operations that need random access because we have an extra indirection
122/// and indexes need to be mapped to the proper chunk. Arithmetic may also be slowed down by this.
123/// When multiplying two [`ChunkedArray`]s with different chunk sizes they cannot utilize [SIMD](https://en.wikipedia.org/wiki/SIMD) for instance.
124///
125/// If you want to have predictable performance
126/// (no unexpected re-allocation of memory), it is advised to call the [`ChunkedArray::rechunk`] after
127/// multiple append operations.
128///
129/// See also [`ChunkedArray::extend`] for appends within a chunk.
130///
131/// # Invariants
132/// - A [`ChunkedArray`] should always have at least a single [`ArrayRef`].
133/// - The [`PolarsDataType`] `T` should always map to the correct [`ArrowDataType`] in the [`ArrayRef`]
134///   chunks.
135/// - Nested datatypes such as [`List`] and [`Array`] store the physical types instead of the
136///   logical type given by the datatype.
137///
138/// [`List`]: crate::datatypes::DataType::List
139pub struct ChunkedArray<T: PolarsDataType> {
140    pub(crate) field: Arc<Field>,
141    pub(crate) chunks: Vec<ArrayRef>,
142
143    pub(crate) flags: StatisticsFlagsIM,
144
145    length: usize,
146    null_count: usize,
147    _pd: std::marker::PhantomData<T>,
148}
149
150impl<T: PolarsDataType> ChunkedArray<T> {
151    fn should_rechunk(&self) -> bool {
152        self.chunks.len() > 1 && self.chunks.len() > self.len() / 3
153    }
154
155    fn optional_rechunk(mut self) -> Self {
156        // Rechunk if we have many small chunks.
157        if self.should_rechunk() {
158            self.rechunk_mut()
159        }
160        self
161    }
162
163    pub(crate) fn as_any(&self) -> &dyn std::any::Any {
164        self
165    }
166
167    /// Series to [`ChunkedArray<T>`]
168    pub fn unpack_series_matching_type<'a>(
169        &self,
170        series: &'a Series,
171    ) -> PolarsResult<&'a ChunkedArray<T>> {
172        polars_ensure!(
173            self.dtype() == series.dtype(),
174            SchemaMismatch: "cannot unpack series of type `{}` into `{}`",
175            series.dtype(),
176            self.dtype(),
177        );
178
179        // SAFETY: dtype will be correct.
180        Ok(unsafe { self.unpack_series_matching_physical_type(series) })
181    }
182
183    /// Create a new [`ChunkedArray`] and compute its `length` and `null_count`.
184    ///
185    /// If you want to explicitly the `length` and `null_count`, look at
186    /// [`ChunkedArray::new_with_dims`]
187    fn new_with_compute_len(field: Arc<Field>, chunks: Vec<ArrayRef>) -> Self {
188        unsafe {
189            let mut chunked_arr = Self::new_with_dims(field, chunks, 0, 0);
190            chunked_arr.compute_len();
191            chunked_arr
192        }
193    }
194
195    /// Create a new [`ChunkedArray`] and explicitly set its `length` and `null_count`.
196    /// # Safety
197    /// The length and null_count must be correct.
198    pub unsafe fn new_with_dims(
199        field: Arc<Field>,
200        chunks: Vec<ArrayRef>,
201        length: usize,
202        null_count: usize,
203    ) -> Self {
204        Self {
205            field,
206            chunks,
207            flags: StatisticsFlagsIM::empty(),
208
209            _pd: Default::default(),
210            length,
211            null_count,
212        }
213    }
214
215    pub(crate) fn is_sorted_ascending_flag(&self) -> bool {
216        self.get_flags().is_sorted_ascending()
217    }
218
219    pub(crate) fn is_sorted_descending_flag(&self) -> bool {
220        self.get_flags().is_sorted_descending()
221    }
222
223    /// Whether `self` is sorted in any direction.
224    pub(crate) fn is_sorted_any(&self) -> bool {
225        self.get_flags().is_sorted_any()
226    }
227
228    pub fn unset_fast_explode_list(&mut self) {
229        self.set_fast_explode_list(false)
230    }
231
232    pub fn set_fast_explode_list(&mut self, value: bool) {
233        let mut flags = self.flags.get_mut();
234        flags.set(StatisticsFlags::CAN_FAST_EXPLODE_LIST, value);
235        self.flags.set_mut(flags);
236    }
237
238    pub fn get_fast_explode_list(&self) -> bool {
239        self.get_flags().can_fast_explode_list()
240    }
241
242    pub fn get_flags(&self) -> StatisticsFlags {
243        self.flags.get()
244    }
245
246    /// Set flags for the [`ChunkedArray`]
247    pub fn set_flags(&mut self, flags: StatisticsFlags) {
248        self.flags = StatisticsFlagsIM::new(flags);
249    }
250
251    pub fn is_sorted_flag(&self) -> IsSorted {
252        self.get_flags().is_sorted()
253    }
254
255    pub fn retain_flags_from<U: PolarsDataType>(
256        &mut self,
257        from: &ChunkedArray<U>,
258        retain_flags: StatisticsFlags,
259    ) {
260        let flags = from.flags.get();
261        // Try to avoid write contention.
262        if !flags.is_empty() {
263            self.set_flags(flags & retain_flags)
264        }
265    }
266
267    /// Set the 'sorted' bit meta info.
268    pub fn set_sorted_flag(&mut self, sorted: IsSorted) {
269        let mut flags = self.flags.get_mut();
270        flags.set_sorted(sorted);
271        self.flags.set_mut(flags);
272    }
273
274    /// Set the 'sorted' bit meta info.
275    pub fn with_sorted_flag(&self, sorted: IsSorted) -> Self {
276        let mut out = self.clone();
277        out.set_sorted_flag(sorted);
278        out
279    }
280
281    pub fn first_null(&self) -> Option<usize> {
282        if self.null_count() == 0 {
283            None
284        }
285        // We now know there is at least 1 non-null item in the array, and self.len() > 0
286        else if self.null_count() == self.len() {
287            Some(0)
288        } else if self.is_sorted_any() {
289            let out = if self
290                .chunks
291                .iter()
292                .find(|arr| !arr.is_empty())
293                .unwrap()
294                .is_null(0)
295            {
296                // nulls are all at the start
297                0
298            } else {
299                // nulls are all at the end
300                self.null_count()
301            };
302
303            debug_assert!(
304                // If we are lucky this catches something.
305                unsafe { self.get_unchecked(out) }.is_some(),
306                "incorrect sorted flag"
307            );
308
309            Some(out)
310        } else {
311            first_null(self.chunks().iter().map(|arr| arr.as_ref()))
312        }
313    }
314
315    /// Get the index of the first non null value in this [`ChunkedArray`].
316    pub fn first_non_null(&self) -> Option<usize> {
317        if self.null_count() == self.len() {
318            None
319        }
320        // We now know there is at least 1 non-null item in the array, and self.len() > 0
321        else if self.null_count() == 0 {
322            Some(0)
323        } else if self.is_sorted_any() {
324            let out = if self
325                .chunks
326                .iter()
327                .find(|arr| !arr.is_empty())
328                .unwrap()
329                .is_null(0)
330            {
331                // nulls are all at the start
332                self.null_count()
333            } else {
334                // nulls are all at the end
335                0
336            };
337
338            debug_assert!(
339                // If we are lucky this catches something.
340                unsafe { self.get_unchecked(out) }.is_some(),
341                "incorrect sorted flag"
342            );
343
344            Some(out)
345        } else {
346            first_non_null(self.chunks().iter().map(|arr| arr.as_ref()))
347        }
348    }
349
350    /// Get the index of the last non null value in this [`ChunkedArray`].
351    pub fn last_non_null(&self) -> Option<usize> {
352        if self.null_count() == self.len() {
353            None
354        }
355        // We now know there is at least 1 non-null item in the array, and self.len() > 0
356        else if self.null_count() == 0 {
357            Some(self.len() - 1)
358        } else if self.is_sorted_any() {
359            let out = if self
360                .chunks
361                .iter()
362                .find(|arr| !arr.is_empty())
363                .unwrap()
364                .is_null(0)
365            {
366                // nulls are all at the start
367                self.len() - 1
368            } else {
369                // nulls are all at the end
370                self.len() - self.null_count() - 1
371            };
372
373            debug_assert!(
374                // If we are lucky this catches something.
375                unsafe { self.get_unchecked(out) }.is_some(),
376                "incorrect sorted flag"
377            );
378
379            Some(out)
380        } else {
381            last_non_null(self.chunks().iter().map(|arr| arr.as_ref()), self.len())
382        }
383    }
384
385    pub fn drop_nulls(&self) -> Self {
386        if self.null_count() == 0 {
387            self.clone()
388        } else {
389            let chunks = self
390                .downcast_iter()
391                .map(|arr| {
392                    if arr.null_count() == 0 {
393                        arr.to_boxed()
394                    } else {
395                        filter_with_bitmap(arr, arr.validity().unwrap())
396                    }
397                })
398                .collect();
399            unsafe {
400                Self::new_with_dims(
401                    self.field.clone(),
402                    chunks,
403                    self.len() - self.null_count(),
404                    0,
405                )
406            }
407        }
408    }
409
410    /// Get the buffer of bits representing null values
411    #[inline]
412    #[allow(clippy::type_complexity)]
413    pub fn iter_validities(
414        &self,
415    ) -> impl ExactSizeIterator<Item = Option<&Bitmap>> + DoubleEndedIterator {
416        fn to_validity(arr: &ArrayRef) -> Option<&Bitmap> {
417            arr.validity()
418        }
419        self.chunks.iter().map(to_validity)
420    }
421
422    #[inline]
423    /// Return if any the chunks in this [`ChunkedArray`] have nulls.
424    pub fn has_nulls(&self) -> bool {
425        self.null_count > 0
426    }
427
428    /// Shrink the capacity of this array to fit its length.
429    pub fn shrink_to_fit(&mut self) {
430        self.chunks = vec![concatenate_unchecked(self.chunks.as_slice()).unwrap()];
431    }
432
433    pub fn clear(&self) -> Self {
434        // SAFETY: we keep the correct dtype
435        let mut ca = unsafe {
436            self.copy_with_chunks(vec![new_empty_array(
437                self.chunks.first().unwrap().dtype().clone(),
438            )])
439        };
440
441        use StatisticsFlags as F;
442        ca.retain_flags_from(self, F::IS_SORTED_ANY | F::CAN_FAST_EXPLODE_LIST);
443        ca
444    }
445
446    /// Unpack a [`Series`] to the same physical type.
447    ///
448    /// # Safety
449    ///
450    /// This is unsafe as the dtype may be incorrect and
451    /// is assumed to be correct in other safe code.
452    pub(crate) unsafe fn unpack_series_matching_physical_type<'a>(
453        &self,
454        series: &'a Series,
455    ) -> &'a ChunkedArray<T> {
456        let series_trait = &**series;
457        if self.dtype() == series.dtype() {
458            &*(series_trait as *const dyn SeriesTrait as *const ChunkedArray<T>)
459        } else {
460            use DataType::*;
461            match (self.dtype(), series.dtype()) {
462                (Int64, Datetime(_, _)) | (Int64, Duration(_)) | (Int32, Date) => {
463                    &*(series_trait as *const dyn SeriesTrait as *const ChunkedArray<T>)
464                },
465                _ => panic!(
466                    "cannot unpack series {:?} into matching type {:?}",
467                    series,
468                    self.dtype()
469                ),
470            }
471        }
472    }
473
474    /// Returns an iterator over the lengths of the chunks of the array.
475    pub fn chunk_lengths(&self) -> ChunkLenIter<'_> {
476        self.chunks.iter().map(|chunk| chunk.len())
477    }
478
479    /// A reference to the chunks
480    #[inline]
481    pub fn chunks(&self) -> &Vec<ArrayRef> {
482        &self.chunks
483    }
484
485    /// A mutable reference to the chunks
486    ///
487    /// # Safety
488    /// The caller must ensure to not change the [`DataType`] or `length` of any of the chunks.
489    /// And the `null_count` remains correct.
490    #[inline]
491    pub unsafe fn chunks_mut(&mut self) -> &mut Vec<ArrayRef> {
492        &mut self.chunks
493    }
494
495    /// Returns true if contains a single chunk and has no null values
496    pub fn is_optimal_aligned(&self) -> bool {
497        self.chunks.len() == 1 && self.null_count() == 0
498    }
499
500    /// Create a new [`ChunkedArray`] from self, where the chunks are replaced.
501    ///
502    /// # Safety
503    /// The caller must ensure the dtypes of the chunks are correct
504    unsafe fn copy_with_chunks(&self, chunks: Vec<ArrayRef>) -> Self {
505        Self::new_with_compute_len(self.field.clone(), chunks)
506    }
507
508    /// Get data type of [`ChunkedArray`].
509    pub fn dtype(&self) -> &DataType {
510        self.field.dtype()
511    }
512
513    pub(crate) unsafe fn set_dtype(&mut self, dtype: DataType) {
514        self.field = Arc::new(Field::new(self.name().clone(), dtype))
515    }
516
517    /// Name of the [`ChunkedArray`].
518    pub fn name(&self) -> &PlSmallStr {
519        self.field.name()
520    }
521
522    /// Get a reference to the field.
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 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 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: PolarsDataType> Clone for ChunkedArray<T> {
1036    fn clone(&self) -> Self {
1037        ChunkedArray {
1038            field: self.field.clone(),
1039            chunks: self.chunks.clone(),
1040            flags: self.flags.clone(),
1041
1042            _pd: Default::default(),
1043            length: self.length,
1044            null_count: self.null_count,
1045        }
1046    }
1047}
1048
1049impl<T: PolarsDataType> AsRef<ChunkedArray<T>> for ChunkedArray<T> {
1050    fn as_ref(&self) -> &ChunkedArray<T> {
1051        self
1052    }
1053}
1054
1055impl ValueSize for ListChunked {
1056    fn get_values_size(&self) -> usize {
1057        self.chunks
1058            .iter()
1059            .fold(0usize, |acc, arr| acc + arr.get_values_size())
1060    }
1061}
1062
1063#[cfg(feature = "dtype-array")]
1064impl ValueSize for ArrayChunked {
1065    fn get_values_size(&self) -> usize {
1066        self.chunks
1067            .iter()
1068            .fold(0usize, |acc, arr| acc + arr.get_values_size())
1069    }
1070}
1071impl ValueSize for StringChunked {
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
1079impl ValueSize for BinaryOffsetChunked {
1080    fn get_values_size(&self) -> usize {
1081        self.chunks
1082            .iter()
1083            .fold(0usize, |acc, arr| acc + arr.get_values_size())
1084    }
1085}
1086
1087pub(crate) fn to_primitive<T: PolarsNumericType>(
1088    values: Vec<T::Native>,
1089    validity: Option<Bitmap>,
1090) -> PrimitiveArray<T::Native> {
1091    PrimitiveArray::new(
1092        T::get_static_dtype().to_arrow(CompatLevel::newest()),
1093        values.into(),
1094        validity,
1095    )
1096}
1097
1098pub(crate) fn to_array<T: PolarsNumericType>(
1099    values: Vec<T::Native>,
1100    validity: Option<Bitmap>,
1101) -> ArrayRef {
1102    Box::new(to_primitive::<T>(values, validity))
1103}
1104
1105impl<T: PolarsDataType> Default for ChunkedArray<T> {
1106    fn default() -> Self {
1107        let dtype = T::get_static_dtype();
1108        let arrow_dtype = dtype.to_physical().to_arrow(CompatLevel::newest());
1109        ChunkedArray {
1110            field: Arc::new(Field::new(PlSmallStr::EMPTY, dtype)),
1111            // Invariant: always has 1 chunk.
1112            chunks: vec![new_empty_array(arrow_dtype)],
1113            flags: StatisticsFlagsIM::empty(),
1114
1115            _pd: Default::default(),
1116            length: 0,
1117            null_count: 0,
1118        }
1119    }
1120}
1121
1122impl<T: PolarsDataType> BroadcastLength for ChunkedArray<T> {
1123    fn _broadcast_len(&self) -> usize {
1124        self.len()
1125    }
1126
1127    fn _column_name(&self) -> Option<&str> {
1128        Some(self.name())
1129    }
1130}
1131
1132#[cfg(test)]
1133pub(crate) mod test {
1134    use crate::prelude::*;
1135
1136    pub(crate) fn get_chunked_array() -> Int32Chunked {
1137        ChunkedArray::new(PlSmallStr::from_static("a"), &[1, 2, 3])
1138    }
1139
1140    #[test]
1141    fn test_sort() {
1142        let a = Int32Chunked::new(PlSmallStr::from_static("a"), &[1, 9, 3, 2]);
1143        let b = a
1144            .sort(false)
1145            .iter()
1146            .map(|opt| opt.unwrap())
1147            .collect::<Vec<_>>();
1148        assert_eq!(b, [1, 2, 3, 9]);
1149        let a = StringChunked::new(PlSmallStr::from_static("a"), &["b", "a", "c"]);
1150        let a = a.sort(false);
1151        let b = a.iter().collect::<Vec<_>>();
1152        assert_eq!(b, [Some("a"), Some("b"), Some("c")]);
1153        assert!(a.is_sorted_ascending_flag());
1154    }
1155
1156    #[test]
1157    fn arithmetic() {
1158        let a = &Int32Chunked::new(PlSmallStr::from_static("a"), &[1, 100, 6, 40]);
1159        let b = &Int32Chunked::new(PlSmallStr::from_static("b"), &[-1, 2, 3, 4]);
1160
1161        // Not really asserting anything here but still making sure the code is exercised
1162        // This (and more) is properly tested from the integration test suite and Python bindings.
1163        println!("{:?}", a + b);
1164        println!("{:?}", a - b);
1165        println!("{:?}", a * b);
1166        println!("{:?}", a / b);
1167    }
1168
1169    #[test]
1170    fn iter() {
1171        let s1 = get_chunked_array();
1172        // sum
1173        assert_eq!(s1.iter().fold(0, |acc, val| { acc + val.unwrap() }), 6)
1174    }
1175
1176    #[test]
1177    fn limit() {
1178        let a = get_chunked_array();
1179        let b = a.limit(2);
1180        println!("{b:?}");
1181        assert_eq!(b.len(), 2)
1182    }
1183
1184    #[test]
1185    fn filter() {
1186        let a = get_chunked_array();
1187        let b = a
1188            .filter(&BooleanChunked::new(
1189                PlSmallStr::from_static("filter"),
1190                &[true, false, false],
1191            ))
1192            .unwrap();
1193        assert_eq!(b.len(), 1);
1194        assert_eq!(b.iter().next(), Some(Some(1)));
1195    }
1196
1197    #[test]
1198    fn aggregates() {
1199        let a = &Int32Chunked::new(PlSmallStr::from_static("a"), &[1, 100, 10, 9]);
1200        assert_eq!(a.max(), Some(100));
1201        assert_eq!(a.min(), Some(1));
1202        assert_eq!(a.sum(), Some(120))
1203    }
1204
1205    #[test]
1206    fn take() {
1207        let a = get_chunked_array();
1208        let new = a.take(&[0 as IdxSize, 1]).unwrap();
1209        assert_eq!(new.len(), 2)
1210    }
1211
1212    #[test]
1213    fn cast() {
1214        let a = get_chunked_array();
1215        let b = a.cast(&DataType::Int64).unwrap();
1216        assert_eq!(b.dtype(), &DataType::Int64)
1217    }
1218
1219    fn assert_slice_equal<T>(ca: &ChunkedArray<T>, eq: &[T::Native])
1220    where
1221        T: PolarsNumericType,
1222    {
1223        assert_eq!(ca.iter().map(|opt| opt.unwrap()).collect::<Vec<_>>(), eq)
1224    }
1225
1226    #[test]
1227    fn slice() {
1228        let mut first = UInt32Chunked::new(PlSmallStr::from_static("first"), &[0, 1, 2]);
1229        let second = UInt32Chunked::new(PlSmallStr::from_static("second"), &[3, 4, 5]);
1230        first.append(&second).unwrap();
1231        assert_slice_equal(&first.slice(0, 3), &[0, 1, 2]);
1232        assert_slice_equal(&first.slice(0, 4), &[0, 1, 2, 3]);
1233        assert_slice_equal(&first.slice(1, 4), &[1, 2, 3, 4]);
1234        assert_slice_equal(&first.slice(3, 2), &[3, 4]);
1235        assert_slice_equal(&first.slice(3, 3), &[3, 4, 5]);
1236        assert_slice_equal(&first.slice(-3, 3), &[3, 4, 5]);
1237        assert_slice_equal(&first.slice(-6, 6), &[0, 1, 2, 3, 4, 5]);
1238
1239        assert_eq!(first.slice(-7, 2).len(), 1);
1240        assert_eq!(first.slice(-3, 4).len(), 3);
1241        assert_eq!(first.slice(3, 4).len(), 3);
1242        assert_eq!(first.slice(10, 4).len(), 0);
1243    }
1244
1245    #[test]
1246    fn sorting() {
1247        let s = UInt32Chunked::new(PlSmallStr::EMPTY, &[9, 2, 4]);
1248        let sorted = s.sort(false);
1249        assert_slice_equal(&sorted, &[2, 4, 9]);
1250        let sorted = s.sort(true);
1251        assert_slice_equal(&sorted, &[9, 4, 2]);
1252
1253        let s: StringChunked = ["b", "a", "z"].iter().collect();
1254        let sorted = s.sort(false);
1255        assert_eq!(
1256            sorted.iter().collect::<Vec<_>>(),
1257            &[Some("a"), Some("b"), Some("z")]
1258        );
1259        let sorted = s.sort(true);
1260        assert_eq!(
1261            sorted.iter().collect::<Vec<_>>(),
1262            &[Some("z"), Some("b"), Some("a")]
1263        );
1264        let s: StringChunked = [Some("b"), None, Some("z")].iter().copied().collect();
1265        let sorted = s.sort(false);
1266        assert_eq!(
1267            sorted.iter().collect::<Vec<_>>(),
1268            &[None, Some("b"), Some("z")]
1269        );
1270    }
1271
1272    #[test]
1273    fn reverse() {
1274        let s = UInt32Chunked::new(PlSmallStr::EMPTY, &[1, 2, 3]);
1275        // path with continuous slice
1276        assert_slice_equal(&s.reverse(), &[3, 2, 1]);
1277        // path with options
1278        let s = UInt32Chunked::new(PlSmallStr::EMPTY, &[Some(1), None, Some(3)]);
1279        assert_eq!(Vec::from(&s.reverse()), &[Some(3), None, Some(1)]);
1280        let s = BooleanChunked::new(PlSmallStr::EMPTY, &[true, false]);
1281        assert_eq!(Vec::from(&s.reverse()), &[Some(false), Some(true)]);
1282
1283        let s = StringChunked::new(PlSmallStr::EMPTY, &["a", "b", "c"]);
1284        assert_eq!(Vec::from(&s.reverse()), &[Some("c"), Some("b"), Some("a")]);
1285
1286        let s = StringChunked::new(PlSmallStr::EMPTY, &[Some("a"), None, Some("c")]);
1287        assert_eq!(Vec::from(&s.reverse()), &[Some("c"), None, Some("a")]);
1288    }
1289
1290    #[test]
1291    #[cfg(feature = "dtype-categorical")]
1292    fn test_iter_categorical() {
1293        let ca = StringChunked::new(
1294            PlSmallStr::EMPTY,
1295            &[Some("foo"), None, Some("bar"), Some("ham")],
1296        );
1297        let cats = Categories::new(
1298            PlSmallStr::EMPTY,
1299            PlSmallStr::EMPTY,
1300            CategoricalPhysical::U32,
1301        );
1302        let ca = ca.cast(&DataType::from_categories(cats)).unwrap();
1303        let ca = ca.cat32().unwrap();
1304        let v: Vec<_> = ca.physical().iter().collect();
1305        assert_eq!(v, &[Some(0), None, Some(1), Some(2)]);
1306    }
1307
1308    #[test]
1309    #[ignore]
1310    fn test_shrink_to_fit() {
1311        let mut builder = StringChunkedBuilder::new(PlSmallStr::from_static("foo"), 2048);
1312        builder.append_value("foo");
1313        let mut arr = builder.finish();
1314        let before = arr
1315            .chunks()
1316            .iter()
1317            .map(|arr| arrow::compute::aggregate::estimated_bytes_size(arr.as_ref()))
1318            .sum::<usize>();
1319        arr.shrink_to_fit();
1320        let after = arr
1321            .chunks()
1322            .iter()
1323            .map(|arr| arrow::compute::aggregate::estimated_bytes_size(arr.as_ref()))
1324            .sum::<usize>();
1325        assert!(before > after);
1326    }
1327}