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 polars_arrow::array::*;
7use polars_arrow::bitmap::Bitmap;
8use polars_arrow::compute::concatenate::concatenate_unchecked;
9use polars_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    #[inline]
243    pub fn get_flags(&self) -> StatisticsFlags {
244        self.flags.get()
245    }
246
247    /// Set flags for the [`ChunkedArray`]
248    pub fn set_flags(&mut self, flags: StatisticsFlags) {
249        self.flags = StatisticsFlagsIM::new(flags);
250    }
251
252    pub fn is_sorted_flag(&self) -> IsSorted {
253        self.get_flags().is_sorted()
254    }
255
256    pub fn retain_flags_from<U: PolarsDataType>(
257        &mut self,
258        from: &ChunkedArray<U>,
259        retain_flags: StatisticsFlags,
260    ) {
261        let flags = from.flags.get();
262        // Try to avoid write contention.
263        if !flags.is_empty() {
264            self.set_flags(flags & retain_flags)
265        }
266    }
267
268    /// Set the 'sorted' bit meta info.
269    pub fn set_sorted_flag(&mut self, sorted: IsSorted) {
270        let mut flags = self.flags.get_mut();
271        flags.set_sorted(sorted);
272        self.flags.set_mut(flags);
273    }
274
275    /// Set the 'sorted' bit meta info.
276    pub fn with_sorted_flag(&self, sorted: IsSorted) -> Self {
277        let mut out = self.clone();
278        out.set_sorted_flag(sorted);
279        out
280    }
281
282    pub fn first_null(&self) -> Option<usize> {
283        if self.null_count() == 0 {
284            None
285        }
286        // We now know there is at least 1 non-null item in the array, and self.len() > 0
287        else if self.null_count() == self.len() {
288            Some(0)
289        } else if self.is_sorted_any() {
290            let out = if self
291                .chunks
292                .iter()
293                .find(|arr| !arr.is_empty())
294                .unwrap()
295                .is_null(0)
296            {
297                // nulls are all at the start
298                0
299            } else {
300                // nulls are all at the end
301                self.len() - self.null_count()
302            };
303
304            debug_assert!(
305                // If we are lucky this catches something.
306                unsafe { self.get_unchecked(out) }.is_none(),
307                "incorrect sorted flag"
308            );
309
310            Some(out)
311        } else {
312            first_null(self.chunks().iter().map(|arr| arr.as_ref()))
313        }
314    }
315
316    /// Get the index of the first non null value in this [`ChunkedArray`].
317    pub fn first_non_null(&self) -> Option<usize> {
318        if self.null_count() == self.len() {
319            None
320        }
321        // We now know there is at least 1 non-null item in the array, and self.len() > 0
322        else if self.null_count() == 0 {
323            Some(0)
324        } else if self.is_sorted_any() {
325            let out = if self
326                .chunks
327                .iter()
328                .find(|arr| !arr.is_empty())
329                .unwrap()
330                .is_null(0)
331            {
332                // nulls are all at the start
333                self.null_count()
334            } else {
335                // nulls are all at the end
336                0
337            };
338
339            debug_assert!(
340                // If we are lucky this catches something.
341                unsafe { self.get_unchecked(out) }.is_some(),
342                "incorrect sorted flag"
343            );
344
345            Some(out)
346        } else {
347            first_non_null(self.chunks().iter().map(|arr| arr.as_ref()))
348        }
349    }
350
351    /// Get the index of the last non null value in this [`ChunkedArray`].
352    pub fn last_non_null(&self) -> Option<usize> {
353        if self.null_count() == self.len() {
354            None
355        }
356        // We now know there is at least 1 non-null item in the array, and self.len() > 0
357        else if self.null_count() == 0 {
358            Some(self.len() - 1)
359        } else if self.is_sorted_any() {
360            let out = if self
361                .chunks
362                .iter()
363                .find(|arr| !arr.is_empty())
364                .unwrap()
365                .is_null(0)
366            {
367                // nulls are all at the start
368                self.len() - 1
369            } else {
370                // nulls are all at the end
371                self.len() - self.null_count() - 1
372            };
373
374            debug_assert!(
375                // If we are lucky this catches something.
376                unsafe { self.get_unchecked(out) }.is_some(),
377                "incorrect sorted flag"
378            );
379
380            Some(out)
381        } else {
382            last_non_null(self.chunks().iter().map(|arr| arr.as_ref()), self.len())
383        }
384    }
385
386    pub fn drop_nulls(&self) -> Self {
387        if self.null_count() == 0 {
388            self.clone()
389        } else {
390            let chunks = self
391                .downcast_iter()
392                .map(|arr| {
393                    if arr.null_count() == 0 {
394                        arr.to_boxed()
395                    } else {
396                        filter_with_bitmap(arr, arr.validity().unwrap())
397                    }
398                })
399                .collect();
400            unsafe {
401                Self::new_with_dims(
402                    self.field.clone(),
403                    chunks,
404                    self.len() - self.null_count(),
405                    0,
406                )
407            }
408        }
409    }
410
411    /// Get the buffer of bits representing null values
412    #[inline]
413    #[allow(clippy::type_complexity)]
414    pub fn iter_validities(
415        &self,
416    ) -> impl ExactSizeIterator<Item = Option<&Bitmap>> + DoubleEndedIterator {
417        fn to_validity(arr: &ArrayRef) -> Option<&Bitmap> {
418            arr.validity()
419        }
420        self.chunks.iter().map(to_validity)
421    }
422
423    #[inline]
424    /// Return if any the chunks in this [`ChunkedArray`] have nulls.
425    pub fn has_nulls(&self) -> bool {
426        self.null_count > 0
427    }
428
429    /// Shrink the capacity of this array to fit its length.
430    pub fn shrink_to_fit(&mut self) {
431        self.chunks = vec![concatenate_unchecked(self.chunks.as_slice()).unwrap()];
432    }
433
434    pub fn clear(&self) -> Self {
435        // SAFETY: we keep the correct dtype
436        let mut ca = unsafe {
437            self.copy_with_chunks(vec![new_empty_array(
438                self.chunks.first().unwrap().dtype().clone(),
439            )])
440        };
441
442        use StatisticsFlags as F;
443        ca.retain_flags_from(self, F::IS_SORTED_ANY | F::CAN_FAST_EXPLODE_LIST);
444        ca
445    }
446
447    /// Unpack a [`Series`] to the same physical type.
448    ///
449    /// # Safety
450    ///
451    /// This is unsafe as the dtype may be incorrect and
452    /// is assumed to be correct in other safe code.
453    pub(crate) unsafe fn unpack_series_matching_physical_type<'a>(
454        &self,
455        series: &'a Series,
456    ) -> &'a ChunkedArray<T> {
457        let series_trait = &**series;
458        if self.dtype() == series.dtype() {
459            &*(series_trait as *const dyn SeriesTrait as *const ChunkedArray<T>)
460        } else {
461            use DataType::*;
462            match (self.dtype(), series.dtype()) {
463                (Int64, Datetime(_, _)) | (Int64, Duration(_)) | (Int32, Date) => {
464                    &*(series_trait as *const dyn SeriesTrait as *const ChunkedArray<T>)
465                },
466                _ => panic!(
467                    "cannot unpack series {:?} into matching type {:?}",
468                    series,
469                    self.dtype()
470                ),
471            }
472        }
473    }
474
475    /// Returns an iterator over the lengths of the chunks of the array.
476    pub fn chunk_lengths(&self) -> ChunkLenIter<'_> {
477        self.chunks.iter().map(|chunk| chunk.len())
478    }
479
480    /// A reference to the chunks
481    #[inline]
482    pub fn chunks(&self) -> &Vec<ArrayRef> {
483        &self.chunks
484    }
485
486    /// A mutable reference to the chunks
487    ///
488    /// # Safety
489    /// The caller must ensure to not change the [`DataType`] or `length` of any of the chunks.
490    /// And the `null_count` remains correct.
491    #[inline]
492    pub unsafe fn chunks_mut(&mut self) -> &mut Vec<ArrayRef> {
493        &mut self.chunks
494    }
495
496    /// Create a new [`ChunkedArray`] from self, where the chunks are replaced.
497    ///
498    /// # Safety
499    /// The caller must ensure the dtypes of the chunks are correct
500    unsafe fn copy_with_chunks(&self, chunks: Vec<ArrayRef>) -> Self {
501        Self::new_with_compute_len(self.field.clone(), chunks)
502    }
503
504    /// Get data type of [`ChunkedArray`].
505    #[inline(always)]
506    pub fn dtype(&self) -> &DataType {
507        self.field.dtype()
508    }
509
510    pub(crate) unsafe fn set_dtype(&mut self, dtype: DataType) {
511        self.field = Arc::new(Field::new(self.name().clone(), dtype))
512    }
513
514    /// Name of the [`ChunkedArray`].
515    #[inline]
516    pub fn name(&self) -> &PlSmallStr {
517        self.field.name()
518    }
519
520    /// Get a reference to the field.
521    #[inline(always)]
522    pub fn ref_field(&self) -> &Field {
523        &self.field
524    }
525
526    /// Rename this [`ChunkedArray`].
527    pub fn rename(&mut self, name: PlSmallStr) {
528        self.field = Arc::new(Field::new(name, self.field.dtype().clone()));
529    }
530
531    /// Return this [`ChunkedArray`] with a new name.
532    pub fn with_name(mut self, name: PlSmallStr) -> Self {
533        self.rename(name);
534        self
535    }
536}
537
538impl<T> ChunkedArray<T>
539where
540    T: PolarsDataType,
541{
542    /// Get a single value from this [`ChunkedArray`]. If the return values is `None` this
543    /// indicates a NULL value.
544    ///
545    /// # Panics
546    /// This function will panic if `idx` is out of bounds.
547    #[inline]
548    pub fn get(&self, idx: usize) -> Option<T::Physical<'_>> {
549        let (chunk_idx, arr_idx) = self.index_to_chunked_index(idx);
550        assert!(
551            chunk_idx < self.chunks().len(),
552            "index: {} out of bounds for len: {}",
553            idx,
554            self.len()
555        );
556        unsafe {
557            let arr = self.downcast_get_unchecked(chunk_idx);
558            assert!(
559                arr_idx < arr.len(),
560                "index: {} out of bounds for len: {}",
561                idx,
562                self.len()
563            );
564            arr.get_unchecked(arr_idx)
565        }
566    }
567
568    /// Get a single value from this [`ChunkedArray`]. If the return values is `None` this
569    /// indicates a NULL value.
570    ///
571    /// # Safety
572    /// It is the callers responsibility that the `idx < self.len()`.
573    #[inline]
574    pub unsafe fn get_unchecked(&self, idx: usize) -> Option<T::Physical<'_>> {
575        let (chunk_idx, arr_idx) = self.index_to_chunked_index(idx);
576
577        unsafe {
578            // SAFETY: up to the caller to make sure the index is valid.
579            self.downcast_get_unchecked(chunk_idx)
580                .get_unchecked(arr_idx)
581        }
582    }
583
584    /// Get a single value from this [`ChunkedArray`]. Null values are ignored and the returned
585    /// value could be garbage if it was masked out by NULL. Note that the value always is initialized.
586    ///
587    /// # Safety
588    /// It is the callers responsibility that the `idx < self.len()`.
589    #[inline]
590    pub unsafe fn value_unchecked(&self, idx: usize) -> T::Physical<'_> {
591        let (chunk_idx, arr_idx) = self.index_to_chunked_index(idx);
592
593        unsafe {
594            // SAFETY: up to the caller to make sure the index is valid.
595            self.downcast_get_unchecked(chunk_idx)
596                .value_unchecked(arr_idx)
597        }
598    }
599
600    /// # Panics
601    /// Panics if the [`ChunkedArray`] is empty.
602    #[inline]
603    pub fn first(&self) -> Option<T::Physical<'_>> {
604        self.iter().next().unwrap()
605    }
606
607    /// # Panics
608    /// Panics if the [`ChunkedArray`] is empty.
609    #[inline]
610    pub fn last(&self) -> Option<T::Physical<'_>> {
611        let arr = self
612            .downcast_iter()
613            .rev()
614            .find(|arr| !arr.is_empty())
615            .unwrap();
616        unsafe { arr.get_unchecked(arr.len() - 1) }
617    }
618
619    pub fn set_validity(&mut self, validity: Option<Bitmap>) {
620        assert!(
621            !self.dtype().is_struct(),
622            "set_outer_validity should be used for struct types"
623        );
624        if let Some(v) = &validity {
625            assert_eq!(self.len(), v.len());
626        }
627        let mut i = 0;
628        for chunk in unsafe { self.chunks_mut() } {
629            *chunk =
630                chunk.with_validity(validity.as_ref().map(|v| v.clone().sliced(i, chunk.len())));
631            i += chunk.len();
632        }
633        self.null_count = validity.map(|v| v.unset_bits()).unwrap_or(0);
634        self.set_fast_explode_list(false);
635    }
636
637    pub fn with_validity(mut self, validity: Option<Bitmap>) -> Self {
638        self.set_validity(validity);
639        self
640    }
641}
642
643impl<T> ChunkedArray<T>
644where
645    T: PolarsDataType,
646    ChunkedArray<T>: ChunkExpandAtIndex<T>,
647{
648    /// Returns a ChunkedArray with the given length.
649    ///
650    /// Errors if this ChunkedArray's length is not 1 and also not equal to the requested length.
651    pub fn broadcast_to(&self, length: usize) -> PolarsResult<Cow<'_, Self>> {
652        let len = self.len();
653        if len == length {
654            Ok(Cow::Borrowed(self))
655        } else if len == 1 {
656            Ok(Cow::Owned(self.new_from_index(0, length)))
657        } else {
658            polars_bail!(
659                ShapeMismatch: "can't broadcast Series '{}' of length {len} to length {length}",
660                self.name()
661            );
662        }
663    }
664
665    /// See broadcast_to.
666    pub fn broadcast_in_place_to(&mut self, length: usize) -> PolarsResult<()> {
667        if let Cow::Owned(new) = self.broadcast_to(length)? {
668            *self = new;
669        }
670        Ok(())
671    }
672
673    /// See broadcast_to.
674    pub fn broadcast_owned_to(mut self, length: usize) -> PolarsResult<Self> {
675        self.broadcast_in_place_to(length)?;
676        Ok(self)
677    }
678}
679
680impl<T> ChunkedArray<T>
681where
682    T: PolarsDataType,
683    ChunkedArray<T>: ChunkTakeUnchecked<[IdxSize]>,
684{
685    /// Deposit values into nulls with a certain validity mask.
686    pub fn deposit(&self, validity: &Bitmap) -> Self {
687        let set_bits = validity.set_bits();
688
689        assert_eq!(self.len(), set_bits);
690
691        if set_bits == validity.len() {
692            return self.clone();
693        }
694
695        if set_bits == 0 {
696            return Self::full_null_like(self, validity.len());
697        }
698
699        let mut null_mask = validity.clone();
700
701        let mut gather_idxs = Vec::with_capacity(validity.len());
702        let leading_nulls = null_mask.take_leading_zeros();
703        gather_idxs.extend(std::iter::repeat_n(0, leading_nulls + 1));
704
705        let mut i = 0 as IdxSize;
706        gather_idxs.extend(null_mask.iter().skip(1).map(|v| {
707            i += IdxSize::from(v);
708            i
709        }));
710
711        let mut ca = unsafe { ChunkTakeUnchecked::take_unchecked(self, &gather_idxs) };
712        ca.set_validity(combine_validities_and(
713            Some(validity),
714            ca.rechunk_validity().as_ref(),
715        ));
716        ca
717    }
718}
719
720impl ListChunked {
721    #[inline]
722    pub fn get_as_series(&self, idx: usize) -> Option<Series> {
723        unsafe {
724            Some(Series::from_chunks_and_dtype_unchecked(
725                self.name().clone(),
726                vec![self.get(idx)?],
727                &self.inner_dtype().to_physical(),
728            ))
729        }
730    }
731
732    pub fn has_empty_lists(&self) -> bool {
733        for arr in self.downcast_iter() {
734            if arr.is_empty() {
735                continue;
736            }
737
738            if match arr.validity() {
739                None => arr.offsets().lengths().any(|l| l == 0),
740                Some(validity) => arr
741                    .offsets()
742                    .lengths()
743                    .enumerate()
744                    .any(|(i, l)| l == 0 && unsafe { validity.get_bit_unchecked(i) }),
745            } {
746                return true;
747            }
748        }
749
750        false
751    }
752
753    pub fn has_masked_out_values(&self) -> bool {
754        for arr in self.downcast_iter() {
755            if arr.is_empty() {
756                continue;
757            }
758
759            if *arr.offsets().first() != 0 || *arr.offsets().last() != arr.values().len() as i64 {
760                return true;
761            }
762
763            let Some(validity) = arr.validity() else {
764                continue;
765            };
766            if validity.set_bits() == 0 {
767                continue;
768            }
769
770            // @Performance: false_idx_iter
771            for i in (!validity).true_idx_iter() {
772                if arr.offsets().length_at(i) > 0 {
773                    return true;
774                }
775            }
776        }
777
778        false
779    }
780}
781
782#[cfg(feature = "dtype-array")]
783impl ArrayChunked {
784    #[inline]
785    pub fn get_as_series(&self, idx: usize) -> Option<Series> {
786        unsafe {
787            Some(Series::from_chunks_and_dtype_unchecked(
788                self.name().clone(),
789                vec![self.get(idx)?],
790                &self.inner_dtype().to_physical(),
791            ))
792        }
793    }
794
795    pub fn from_aligned_values(
796        name: PlSmallStr,
797        inner_dtype: &DataType,
798        width: usize,
799        chunks: Vec<ArrayRef>,
800        length: usize,
801    ) -> Self {
802        let dtype = DataType::Array(Box::new(inner_dtype.clone()), width);
803        let arrow_dtype = inner_dtype
804            .to_physical()
805            .to_arrow(CompatLevel::newest())
806            .to_fixed_size_list(width, true);
807        let field = Arc::new(Field::new(name, dtype));
808        if width == 0 {
809            use polars_arrow::array::builder::{ArrayBuilder, make_builder};
810            let values = make_builder(&inner_dtype.to_arrow(CompatLevel::newest())).freeze();
811            return ArrayChunked::new_with_compute_len(
812                field,
813                vec![FixedSizeListArray::new(arrow_dtype, length, values, None).into_boxed()],
814            );
815        }
816        let mut total_len = 0;
817        let chunks = chunks
818            .iter()
819            .map(|chunk| {
820                debug_assert_eq!(chunk.len() % width, 0);
821                let chunk_len = chunk.len() / width;
822                total_len += chunk_len;
823                FixedSizeListArray::new(arrow_dtype.clone(), chunk_len, chunk.clone(), None)
824                    .into_boxed()
825            })
826            .collect();
827        debug_assert_eq!(total_len, length);
828
829        unsafe { Self::new_with_dims(field, chunks, length, 0) }
830    }
831
832    /// Turn the ArrayChunked into the ListChunked with the same items.
833    ///
834    /// This will always zero copy the values into the ListChunked.
835    pub fn to_list(&self) -> ListChunked {
836        let inner_dtype = self.inner_dtype();
837        let chunks = self
838            .downcast_iter()
839            .map(|chunk| {
840                use polars_arrow::offset::OffsetsBuffer;
841
842                let inner_dtype = chunk.dtype().inner_dtype().unwrap();
843                let dtype = inner_dtype.clone().to_large_list(true);
844
845                let offsets = (0..=chunk.len())
846                    .map(|i| (i * self.width()) as i64)
847                    .collect::<Vec<i64>>();
848
849                // SAFETY: We created our offsets in ascending manner.
850                let offsets = unsafe { OffsetsBuffer::new_unchecked(offsets.into()) };
851
852                ListArray::<i64>::new(
853                    dtype,
854                    offsets,
855                    chunk.values().clone(),
856                    chunk.validity().cloned(),
857                )
858                .into_boxed()
859            })
860            .collect();
861
862        // SAFETY: All the items were mapped 1-1 with the validity staying the same.
863        let mut ca = unsafe {
864            ListChunked::new_with_dims(
865                Arc::new(Field::new(
866                    self.name().clone(),
867                    DataType::List(Box::new(inner_dtype.clone())),
868                )),
869                chunks,
870                self.len(),
871                self.null_count(),
872            )
873        };
874        ca.set_fast_explode_list(!self.has_nulls());
875        ca
876    }
877}
878
879impl<T> ChunkedArray<T>
880where
881    T: PolarsDataType,
882{
883    /// Should be used to match the chunk_id of another [`ChunkedArray`].
884    /// # Panics
885    /// It is the callers responsibility to ensure that this [`ChunkedArray`] has a single chunk.
886    pub fn match_chunks<I>(&self, chunk_id: I) -> Self
887    where
888        I: Iterator<Item = usize>,
889    {
890        debug_assert!(self.chunks.len() == 1);
891        // Takes a ChunkedArray containing a single chunk.
892        let slice = |ca: &Self| {
893            let array = &ca.chunks[0];
894
895            let mut offset = 0;
896            let chunks = chunk_id
897                .map(|len| {
898                    // SAFETY: within bounds.
899                    debug_assert!((offset + len) <= array.len());
900                    let out = unsafe { array.sliced_unchecked(offset, len) };
901                    offset += len;
902                    out
903                })
904                .collect();
905
906            debug_assert_eq!(offset, array.len());
907
908            // SAFETY: We just slice the original chunks, their type will not change.
909            unsafe {
910                Self::from_chunks_and_dtype(self.name().clone(), chunks, self.dtype().clone())
911            }
912        };
913
914        if self.chunks.len() != 1 {
915            let out = self.rechunk();
916            slice(&out)
917        } else {
918            slice(self)
919        }
920    }
921}
922
923impl<T: PolarsDataType> AsRefDataType for ChunkedArray<T> {
924    fn as_ref_dtype(&self) -> &DataType {
925        self.dtype()
926    }
927}
928
929pub(crate) trait AsSinglePtr: AsRefDataType {
930    /// Rechunk and return a ptr to the start of the array
931    fn as_single_ptr(&mut self) -> PolarsResult<usize> {
932        polars_bail!(opq = as_single_ptr, self.as_ref_dtype());
933    }
934}
935
936impl<T> AsSinglePtr for ChunkedArray<T>
937where
938    T: PolarsNumericType,
939{
940    fn as_single_ptr(&mut self) -> PolarsResult<usize> {
941        self.rechunk_mut();
942        let a = self.data_views().next().unwrap();
943        let ptr = a.as_ptr();
944        Ok(ptr as usize)
945    }
946}
947
948impl AsSinglePtr for BooleanChunked {}
949impl AsSinglePtr for ListChunked {}
950#[cfg(feature = "dtype-array")]
951impl AsSinglePtr for ArrayChunked {}
952impl AsSinglePtr for StringChunked {}
953impl AsSinglePtr for BinaryChunked {}
954#[cfg(feature = "object")]
955impl<T: PolarsObject> AsSinglePtr for ObjectChunked<T> {}
956
957pub enum ChunkedArrayLayout<'a, T: PolarsDataType> {
958    SingleNoNull(&'a T::Array),
959    Single(&'a T::Array),
960    MultiNoNull(&'a ChunkedArray<T>),
961    Multi(&'a ChunkedArray<T>),
962}
963
964impl<T> ChunkedArray<T>
965where
966    T: PolarsDataType,
967{
968    pub fn layout(&self) -> ChunkedArrayLayout<'_, T> {
969        if self.chunks.len() == 1 {
970            let arr = self.downcast_iter().next().unwrap();
971            return if arr.null_count() == 0 {
972                ChunkedArrayLayout::SingleNoNull(arr)
973            } else {
974                ChunkedArrayLayout::Single(arr)
975            };
976        }
977
978        if self.downcast_iter().all(|a| a.null_count() == 0) {
979            ChunkedArrayLayout::MultiNoNull(self)
980        } else {
981            ChunkedArrayLayout::Multi(self)
982        }
983    }
984}
985
986impl<T> ChunkedArray<T>
987where
988    T: PolarsNumericType,
989{
990    /// Returns the values of the array as a contiguous slice.
991    pub fn cont_slice(&self) -> PolarsResult<&[T::Native]> {
992        polars_ensure!(
993            self.chunks.len() == 1 && self.chunks[0].null_count() == 0,
994            ComputeError: "chunked array is not contiguous"
995        );
996        Ok(self.downcast_iter().next().map(|arr| arr.values()).unwrap())
997    }
998
999    /// Returns the values of the array as a contiguous mutable slice.
1000    pub(crate) fn cont_slice_mut(&mut self) -> Option<&mut [T::Native]> {
1001        if self.chunks.len() == 1 && self.chunks[0].null_count() == 0 {
1002            // SAFETY, we will not swap the PrimitiveArray.
1003            let arr = unsafe { self.downcast_iter_mut().next().unwrap() };
1004            arr.get_mut_values()
1005        } else {
1006            None
1007        }
1008    }
1009
1010    /// Get slices of the underlying arrow data.
1011    /// NOTE: null values should be taken into account by the user of these slices as they are handled
1012    /// separately
1013    pub fn data_views(&self) -> impl DoubleEndedIterator<Item = &[T::Native]> {
1014        self.downcast_iter().map(|arr| arr.values().as_slice())
1015    }
1016
1017    #[allow(clippy::wrong_self_convention)]
1018    pub fn into_no_null_iter(
1019        &self,
1020    ) -> impl '_ + Send + Sync + ExactSizeIterator<Item = T::Native> + DoubleEndedIterator + TrustedLen
1021    {
1022        // .copied was significantly slower in benchmark, next call did not inline?
1023        #[allow(clippy::map_clone)]
1024        // we know the iterators len
1025        unsafe {
1026            self.data_views()
1027                .flatten()
1028                .map(|v| *v)
1029                .trust_my_length(self.len())
1030        }
1031    }
1032}
1033
1034impl<T: PolarsDataType> Clone for ChunkedArray<T> {
1035    fn clone(&self) -> Self {
1036        ChunkedArray {
1037            field: self.field.clone(),
1038            chunks: self.chunks.clone(),
1039            flags: self.flags.clone(),
1040
1041            _pd: Default::default(),
1042            length: self.length,
1043            null_count: self.null_count,
1044        }
1045    }
1046}
1047
1048impl<T: PolarsDataType> AsRef<ChunkedArray<T>> for ChunkedArray<T> {
1049    fn as_ref(&self) -> &ChunkedArray<T> {
1050        self
1051    }
1052}
1053
1054impl ValueSize for ListChunked {
1055    fn get_values_size(&self) -> usize {
1056        self.chunks
1057            .iter()
1058            .fold(0usize, |acc, arr| acc + arr.get_values_size())
1059    }
1060}
1061
1062#[cfg(feature = "dtype-array")]
1063impl ValueSize for ArrayChunked {
1064    fn get_values_size(&self) -> usize {
1065        self.chunks
1066            .iter()
1067            .fold(0usize, |acc, arr| acc + arr.get_values_size())
1068    }
1069}
1070impl ValueSize for StringChunked {
1071    fn get_values_size(&self) -> usize {
1072        self.chunks
1073            .iter()
1074            .fold(0usize, |acc, arr| acc + arr.get_values_size())
1075    }
1076}
1077
1078impl ValueSize for BinaryOffsetChunked {
1079    fn get_values_size(&self) -> usize {
1080        self.chunks
1081            .iter()
1082            .fold(0usize, |acc, arr| acc + arr.get_values_size())
1083    }
1084}
1085
1086/// Re-chunk `values` so that its chunk lengths match `chunk_lens`.
1087///
1088/// The sum of `chunk_lens` must equal `values.len()`. Returns a clone when the chunks
1089/// already line up, so passing already-aligned values costs nothing.
1090pub(crate) fn align_inner_chunks(
1091    chunk_lens: impl Iterator<Item = usize>,
1092    values: &Series,
1093) -> Series {
1094    let chunk_lens = chunk_lens.collect::<Vec<_>>();
1095
1096    if chunk_lens.len() == values.chunks().len()
1097        && chunk_lens
1098            .iter()
1099            .zip(values.chunks())
1100            .all(|(len, arr)| *len == arr.len())
1101    {
1102        return values.clone();
1103    }
1104
1105    let mut values = values.rechunk();
1106    let chunks = unsafe { values.chunks_mut() };
1107    let mut arr = chunks.pop().unwrap();
1108    chunks.extend(chunk_lens.into_iter().map(|len| {
1109        let chunk;
1110        (chunk, arr) = arr.split_at_boxed(len);
1111        chunk
1112    }));
1113    assert!(arr.is_empty());
1114    values
1115}
1116
1117pub(crate) fn to_primitive<T: PolarsNumericType>(
1118    values: Vec<T::Native>,
1119    validity: Option<Bitmap>,
1120) -> PrimitiveArray<T::Native> {
1121    PrimitiveArray::new(
1122        T::get_static_dtype().to_arrow(CompatLevel::newest()),
1123        values.into(),
1124        validity,
1125    )
1126}
1127
1128pub(crate) fn to_array<T: PolarsNumericType>(
1129    values: Vec<T::Native>,
1130    validity: Option<Bitmap>,
1131) -> ArrayRef {
1132    Box::new(to_primitive::<T>(values, validity))
1133}
1134
1135impl<T: PolarsDataType> Default for ChunkedArray<T> {
1136    fn default() -> Self {
1137        let dtype = T::get_static_dtype();
1138        let arrow_dtype = dtype.to_physical().to_arrow(CompatLevel::newest());
1139        ChunkedArray {
1140            field: Arc::new(Field::new(PlSmallStr::EMPTY, dtype)),
1141            // Invariant: always has 1 chunk.
1142            chunks: vec![new_empty_array(arrow_dtype)],
1143            flags: StatisticsFlagsIM::empty(),
1144
1145            _pd: Default::default(),
1146            length: 0,
1147            null_count: 0,
1148        }
1149    }
1150}
1151
1152impl<T: PolarsDataType> BroadcastLength for ChunkedArray<T> {
1153    fn _broadcast_len(&self) -> usize {
1154        self.len()
1155    }
1156
1157    fn _column_name(&self) -> Option<&str> {
1158        Some(self.name())
1159    }
1160}
1161
1162#[cfg(test)]
1163pub(crate) mod test {
1164    use crate::prelude::*;
1165
1166    pub(crate) fn get_chunked_array() -> Int32Chunked {
1167        ChunkedArray::new(PlSmallStr::from_static("a"), &[1, 2, 3])
1168    }
1169
1170    #[test]
1171    fn test_sort() {
1172        let a = Int32Chunked::new(PlSmallStr::from_static("a"), &[1, 9, 3, 2]);
1173        let b = a
1174            .sort(false)
1175            .iter()
1176            .map(|opt| opt.unwrap())
1177            .collect::<Vec<_>>();
1178        assert_eq!(b, [1, 2, 3, 9]);
1179        let a = StringChunked::new(PlSmallStr::from_static("a"), &["b", "a", "c"]);
1180        let a = a.sort(false);
1181        let b = a.iter().collect::<Vec<_>>();
1182        assert_eq!(b, [Some("a"), Some("b"), Some("c")]);
1183        assert!(a.is_sorted_ascending_flag());
1184    }
1185
1186    #[test]
1187    fn arithmetic() {
1188        let a = &Int32Chunked::new(PlSmallStr::from_static("a"), &[1, 100, 6, 40]);
1189        let b = &Int32Chunked::new(PlSmallStr::from_static("b"), &[-1, 2, 3, 4]);
1190
1191        // Not really asserting anything here but still making sure the code is exercised
1192        // This (and more) is properly tested from the integration test suite and Python bindings.
1193        println!("{:?}", a + b);
1194        println!("{:?}", a - b);
1195        println!("{:?}", a * b);
1196        println!("{:?}", a / b);
1197    }
1198
1199    #[test]
1200    fn iter() {
1201        let s1 = get_chunked_array();
1202        // sum
1203        assert_eq!(s1.iter().fold(0, |acc, val| { acc + val.unwrap() }), 6)
1204    }
1205
1206    #[test]
1207    fn limit() {
1208        let a = get_chunked_array();
1209        let b = a.limit(2);
1210        println!("{b:?}");
1211        assert_eq!(b.len(), 2)
1212    }
1213
1214    #[test]
1215    fn filter() {
1216        let a = get_chunked_array();
1217        let b = a
1218            .filter(&BooleanChunked::new(
1219                PlSmallStr::from_static("filter"),
1220                &[true, false, false],
1221            ))
1222            .unwrap();
1223        assert_eq!(b.len(), 1);
1224        assert_eq!(b.iter().next(), Some(Some(1)));
1225    }
1226
1227    #[test]
1228    fn aggregates() {
1229        let a = &Int32Chunked::new(PlSmallStr::from_static("a"), &[1, 100, 10, 9]);
1230        assert_eq!(a.max(), Some(100));
1231        assert_eq!(a.min(), Some(1));
1232        assert_eq!(a.sum(), Some(120))
1233    }
1234
1235    #[test]
1236    fn take() {
1237        let a = get_chunked_array();
1238        let new = a.take(&[0 as IdxSize, 1]).unwrap();
1239        assert_eq!(new.len(), 2)
1240    }
1241
1242    #[test]
1243    fn cast() {
1244        let a = get_chunked_array();
1245        let b = a.cast(&DataType::Int64).unwrap();
1246        assert_eq!(b.dtype(), &DataType::Int64)
1247    }
1248
1249    fn assert_slice_equal<T>(ca: &ChunkedArray<T>, eq: &[T::Native])
1250    where
1251        T: PolarsNumericType,
1252    {
1253        assert_eq!(ca.iter().map(|opt| opt.unwrap()).collect::<Vec<_>>(), eq)
1254    }
1255
1256    #[test]
1257    fn slice() {
1258        let mut first = UInt32Chunked::new(PlSmallStr::from_static("first"), &[0, 1, 2]);
1259        let second = UInt32Chunked::new(PlSmallStr::from_static("second"), &[3, 4, 5]);
1260        first.append(&second).unwrap();
1261        assert_slice_equal(&first.slice(0, 3), &[0, 1, 2]);
1262        assert_slice_equal(&first.slice(0, 4), &[0, 1, 2, 3]);
1263        assert_slice_equal(&first.slice(1, 4), &[1, 2, 3, 4]);
1264        assert_slice_equal(&first.slice(3, 2), &[3, 4]);
1265        assert_slice_equal(&first.slice(3, 3), &[3, 4, 5]);
1266        assert_slice_equal(&first.slice(-3, 3), &[3, 4, 5]);
1267        assert_slice_equal(&first.slice(-6, 6), &[0, 1, 2, 3, 4, 5]);
1268
1269        assert_eq!(first.slice(-7, 2).len(), 1);
1270        assert_eq!(first.slice(-3, 4).len(), 3);
1271        assert_eq!(first.slice(3, 4).len(), 3);
1272        assert_eq!(first.slice(10, 4).len(), 0);
1273    }
1274
1275    #[test]
1276    fn sorting() {
1277        let s = UInt32Chunked::new(PlSmallStr::EMPTY, &[9, 2, 4]);
1278        let sorted = s.sort(false);
1279        assert_slice_equal(&sorted, &[2, 4, 9]);
1280        let sorted = s.sort(true);
1281        assert_slice_equal(&sorted, &[9, 4, 2]);
1282
1283        let s: StringChunked = ["b", "a", "z"].iter().collect();
1284        let sorted = s.sort(false);
1285        assert_eq!(
1286            sorted.iter().collect::<Vec<_>>(),
1287            &[Some("a"), Some("b"), Some("z")]
1288        );
1289        let sorted = s.sort(true);
1290        assert_eq!(
1291            sorted.iter().collect::<Vec<_>>(),
1292            &[Some("z"), Some("b"), Some("a")]
1293        );
1294        let s: StringChunked = [Some("b"), None, Some("z")].iter().copied().collect();
1295        let sorted = s.sort(false);
1296        assert_eq!(
1297            sorted.iter().collect::<Vec<_>>(),
1298            &[None, Some("b"), Some("z")]
1299        );
1300    }
1301
1302    #[test]
1303    fn reverse() {
1304        let s = UInt32Chunked::new(PlSmallStr::EMPTY, &[1, 2, 3]);
1305        // path with continuous slice
1306        assert_slice_equal(&s.reverse(), &[3, 2, 1]);
1307        // path with options
1308        let s = UInt32Chunked::new(PlSmallStr::EMPTY, &[Some(1), None, Some(3)]);
1309        assert_eq!(Vec::from(&s.reverse()), &[Some(3), None, Some(1)]);
1310        let s = BooleanChunked::new(PlSmallStr::EMPTY, &[true, false]);
1311        assert_eq!(Vec::from(&s.reverse()), &[Some(false), Some(true)]);
1312
1313        let s = StringChunked::new(PlSmallStr::EMPTY, &["a", "b", "c"]);
1314        assert_eq!(Vec::from(&s.reverse()), &[Some("c"), Some("b"), Some("a")]);
1315
1316        let s = StringChunked::new(PlSmallStr::EMPTY, &[Some("a"), None, Some("c")]);
1317        assert_eq!(Vec::from(&s.reverse()), &[Some("c"), None, Some("a")]);
1318    }
1319
1320    #[test]
1321    #[cfg(feature = "dtype-categorical")]
1322    fn test_iter_categorical() {
1323        let ca = StringChunked::new(
1324            PlSmallStr::EMPTY,
1325            &[Some("foo"), None, Some("bar"), Some("ham")],
1326        );
1327        let cats = Categories::new(
1328            PlSmallStr::EMPTY,
1329            PlSmallStr::EMPTY,
1330            CategoricalPhysical::U32,
1331        );
1332        let ca = ca.cast(&DataType::from_categories(cats)).unwrap();
1333        let ca = ca.cat32().unwrap();
1334        let v: Vec<_> = ca.physical().iter().collect();
1335        assert_eq!(v, &[Some(0), None, Some(1), Some(2)]);
1336    }
1337
1338    #[test]
1339    #[ignore]
1340    fn test_shrink_to_fit() {
1341        let mut builder = StringChunkedBuilder::new(PlSmallStr::from_static("foo"), 2048);
1342        builder.append_value("foo");
1343        let mut arr = builder.finish();
1344        let before = arr
1345            .chunks()
1346            .iter()
1347            .map(|arr| polars_arrow::compute::aggregate::estimated_bytes_size(arr.as_ref()))
1348            .sum::<usize>();
1349        arr.shrink_to_fit();
1350        let after = arr
1351            .chunks()
1352            .iter()
1353            .map(|arr| polars_arrow::compute::aggregate::estimated_bytes_size(arr.as_ref()))
1354            .sum::<usize>();
1355        assert!(before > after);
1356    }
1357}