Skip to main content

polars_core/chunked_array/ops/
chunkops.rs

1use std::borrow::Cow;
2use std::cell::Cell;
3
4use polars_arrow::bitmap::{Bitmap, BitmapBuilder};
5use polars_arrow::compute::concatenate::concatenate_unchecked;
6use polars_error::constants::LENGTH_LIMIT_MSG;
7
8use super::*;
9use crate::chunked_array::flags::StatisticsFlags;
10#[cfg(feature = "object")]
11use crate::chunked_array::object::builder::ObjectChunkedBuilder;
12use crate::utils::slice_offsets;
13
14pub(crate) fn split_at(
15    chunks: &[ArrayRef],
16    offset: i64,
17    own_length: usize,
18) -> (Vec<ArrayRef>, Vec<ArrayRef>) {
19    let mut new_chunks_left = Vec::with_capacity(1);
20    let mut new_chunks_right = Vec::with_capacity(1);
21    let (raw_offset, _) = slice_offsets(offset, 0, own_length);
22
23    let mut remaining_offset = raw_offset;
24    let mut iter = chunks.iter().filter(|c| !c.is_empty());
25
26    for chunk in &mut iter {
27        let chunk_len = chunk.len();
28        if remaining_offset > 0 && remaining_offset >= chunk_len {
29            remaining_offset -= chunk_len;
30            new_chunks_left.push(chunk.clone());
31            continue;
32        }
33
34        let (l, r) = chunk.split_at_boxed(remaining_offset);
35        new_chunks_left.push(l);
36        new_chunks_right.push(r);
37        break;
38    }
39
40    for chunk in iter {
41        new_chunks_right.push(chunk.clone())
42    }
43    if new_chunks_left.is_empty() {
44        new_chunks_left.push(chunks[0].sliced(0, 0));
45    }
46    if new_chunks_right.is_empty() {
47        new_chunks_right.push(chunks[0].sliced(0, 0));
48    }
49    (new_chunks_left, new_chunks_right)
50}
51
52pub(crate) fn slice(
53    chunks: &[ArrayRef],
54    offset: i64,
55    slice_length: usize,
56    own_length: usize,
57) -> (Vec<ArrayRef>, usize) {
58    let mut new_chunks = Vec::with_capacity(1);
59    let (raw_offset, slice_len) = slice_offsets(offset, slice_length, own_length);
60
61    let mut remaining_length = slice_len;
62    let mut remaining_offset = raw_offset;
63    let mut new_len = 0;
64
65    for chunk in chunks {
66        let chunk_len = chunk.len();
67        if remaining_offset > 0 && remaining_offset >= chunk_len {
68            remaining_offset -= chunk_len;
69            continue;
70        }
71        let take_len = if remaining_length + remaining_offset > chunk_len {
72            chunk_len - remaining_offset
73        } else {
74            remaining_length
75        };
76        new_len += take_len;
77
78        debug_assert!(remaining_offset + take_len <= chunk.len());
79        unsafe {
80            // SAFETY:
81            // this function ensures the slices are in bounds
82            new_chunks.push(chunk.sliced_unchecked(remaining_offset, take_len));
83        }
84        remaining_length -= take_len;
85        remaining_offset = 0;
86        if remaining_length == 0 {
87            break;
88        }
89    }
90    if new_chunks.is_empty() {
91        new_chunks.push(chunks[0].sliced(0, 0));
92    }
93    (new_chunks, new_len)
94}
95
96// When we deal with arrays and lists we can easily exceed the limit if
97// we take the underlying values array as a Series. This call stack
98// is hard to follow, so for this one case we make an exception
99// and use a thread local.
100thread_local!(pub static CHECK_LENGTH: Cell<bool> = const { Cell::new(true) });
101
102/// Meant for internal use. In very rare conditions this can be turned off.
103/// # Safety
104/// The caller must ensure the Series that exceeds the length get's deconstructed
105/// into array values or list values before and never is used.
106pub unsafe fn _set_check_length(check: bool) {
107    CHECK_LENGTH.set(check)
108}
109
110impl<T: PolarsDataType> ChunkedArray<T> {
111    /// Get the length of the ChunkedArray
112    #[inline]
113    pub fn len(&self) -> usize {
114        self.length
115    }
116
117    /// Return the number of null values in the ChunkedArray.
118    #[inline]
119    pub fn null_count(&self) -> usize {
120        self.null_count
121    }
122
123    /// Set the null count directly.
124    ///
125    /// This can be useful after mutably adjusting the validity of the
126    /// underlying arrays.
127    ///
128    /// # Safety
129    /// The new null count must match the total null count of the underlying
130    /// arrays.
131    pub unsafe fn set_null_count(&mut self, null_count: usize) {
132        self.null_count = null_count;
133    }
134
135    /// Check if ChunkedArray is empty.
136    #[inline]
137    pub fn is_empty(&self) -> bool {
138        self.len() == 0
139    }
140
141    /// Compute the length
142    pub(crate) fn compute_len(&mut self) {
143        fn inner(chunks: &[ArrayRef]) -> usize {
144            match chunks.len() {
145                // fast path
146                1 => chunks[0].len(),
147                _ => chunks.iter().fold(0, |acc, arr| acc + arr.len()),
148            }
149        }
150        let len = inner(&self.chunks);
151        // Length limit is `IdxSize::MAX - 1`. We use `IdxSize::MAX` to indicate `NULL` in indexing.
152        if len >= (IdxSize::MAX as usize) && CHECK_LENGTH.get() {
153            panic!("{}", LENGTH_LIMIT_MSG);
154        }
155        self.length = len;
156        self.null_count = self
157            .chunks
158            .iter()
159            .map(|arr| arr.null_count())
160            .sum::<usize>();
161    }
162
163    /// Rechunks this ChunkedArray, returning a new Cow::Owned ChunkedArray if it was
164    /// rechunked or simply a Cow::Borrowed of itself if it was already a single chunk.
165    pub fn rechunk(&self) -> Cow<'_, Self> {
166        match self.dtype() {
167            #[cfg(feature = "object")]
168            DataType::Object(_) => {
169                panic!("implementation error")
170            },
171            _ => {
172                if self.chunks.len() == 1 {
173                    Cow::Borrowed(self)
174                } else {
175                    let chunks = vec![concatenate_unchecked(&self.chunks).unwrap()];
176
177                    let mut ca = unsafe { self.copy_with_chunks(chunks) };
178                    use StatisticsFlags as F;
179                    ca.retain_flags_from(self, F::IS_SORTED_ANY | F::CAN_FAST_EXPLODE_LIST);
180                    Cow::Owned(ca)
181                }
182            },
183        }
184    }
185
186    /// Rechunks this ChunkedArray in-place.
187    pub fn rechunk_mut(&mut self) {
188        if self.chunks.len() > 1 {
189            let rechunked = concatenate_unchecked(&self.chunks).unwrap();
190            if self.chunks.capacity() <= 8 {
191                // Reuse chunk allocation if not excessive.
192                self.chunks.clear();
193                self.chunks.push(rechunked);
194            } else {
195                self.chunks = vec![rechunked];
196            }
197        }
198    }
199
200    pub fn rechunk_validity(&self) -> Option<Bitmap> {
201        if self.chunks.len() == 1 {
202            return self.chunks[0].validity().cloned();
203        }
204
205        if !self.has_nulls() || self.is_empty() {
206            return None;
207        }
208
209        let mut bm = BitmapBuilder::with_capacity(self.len());
210        for arr in self.downcast_iter() {
211            if let Some(v) = arr.validity() {
212                bm.extend_from_bitmap(v);
213            } else {
214                bm.extend_constant(arr.len(), true);
215            }
216        }
217        bm.into_opt_validity()
218    }
219
220    pub fn with_validities(&mut self, validities: &[Option<Bitmap>]) {
221        assert_eq!(validities.len(), self.chunks.len());
222
223        // SAFETY:
224        // We don't change the data type of the chunks, nor the length.
225        for (arr, validity) in unsafe { self.chunks_mut().iter_mut() }.zip(validities.iter()) {
226            *arr = arr.with_validity(validity.clone())
227        }
228        self.compute_len();
229    }
230
231    /// Split the array. The chunks are reallocated the underlying data slices are zero copy.
232    ///
233    /// When offset is negative it will be counted from the end of the array.
234    /// This method will never error,
235    /// and will slice the best match when offset, or length is out of bounds
236    pub fn split_at(&self, offset: i64) -> (Self, Self) {
237        // A normal slice, slice the buffers and thus keep the whole memory allocated.
238        let (l, r) = split_at(&self.chunks, offset, self.len());
239        let mut out_l = unsafe { self.copy_with_chunks(l) };
240        let mut out_r = unsafe { self.copy_with_chunks(r) };
241
242        use StatisticsFlags as F;
243        out_l.retain_flags_from(self, F::IS_SORTED_ANY | F::CAN_FAST_EXPLODE_LIST);
244        out_r.retain_flags_from(self, F::IS_SORTED_ANY | F::CAN_FAST_EXPLODE_LIST);
245
246        (out_l, out_r)
247    }
248
249    /// Slice the array. The chunks are reallocated the underlying data slices are zero copy.
250    ///
251    /// When offset is negative it will be counted from the end of the array.
252    /// This method will never error,
253    /// and will slice the best match when offset, or length is out of bounds
254    pub fn slice(&self, offset: i64, length: usize) -> Self {
255        // The len: 0 special cases ensure we release memory.
256        // A normal slice, slice the buffers and thus keep the whole memory allocated.
257        let exec = || {
258            let (chunks, len) = slice(&self.chunks, offset, length, self.len());
259            let mut out = unsafe { self.copy_with_chunks(chunks) };
260
261            use StatisticsFlags as F;
262            out.retain_flags_from(self, F::IS_SORTED_ANY | F::CAN_FAST_EXPLODE_LIST);
263            out.length = len;
264
265            out
266        };
267
268        match length {
269            0 => match self.dtype() {
270                #[cfg(feature = "object")]
271                DataType::Object(_) => exec(),
272                _ => self.clear(),
273            },
274            _ => exec(),
275        }
276    }
277
278    /// Take a view of top n elements
279    #[must_use]
280    pub fn limit(&self, num_elements: usize) -> Self
281    where
282        Self: Sized,
283    {
284        self.slice(0, num_elements)
285    }
286
287    /// Get the head of the [`ChunkedArray`]
288    #[must_use]
289    pub fn head(&self, length: Option<usize>) -> Self
290    where
291        Self: Sized,
292    {
293        match length {
294            Some(len) => self.slice(0, std::cmp::min(len, self.len())),
295            None => self.slice(0, std::cmp::min(10, self.len())),
296        }
297    }
298
299    /// Get the tail of the [`ChunkedArray`]
300    #[must_use]
301    pub fn tail(&self, length: Option<usize>) -> Self
302    where
303        Self: Sized,
304    {
305        let len = match length {
306            Some(len) => std::cmp::min(len, self.len()),
307            None => std::cmp::min(10, self.len()),
308        };
309        self.slice(-(len as i64), len)
310    }
311
312    /// Remove empty chunks.
313    pub fn prune_empty_chunks(&mut self) {
314        let mut count = 0u32;
315        unsafe {
316            self.chunks_mut().retain(|arr| {
317                count += 1;
318                // Always keep at least one chunk
319                if count == 1 {
320                    true
321                } else {
322                    // Remove the empty chunks
323                    !arr.is_empty()
324                }
325            })
326        }
327    }
328}
329
330#[cfg(feature = "object")]
331impl<T: PolarsObject> ObjectChunked<T> {
332    pub(crate) fn rechunk_object(&self) -> Self {
333        if self.chunks.len() == 1 {
334            self.clone()
335        } else {
336            use crate::chunked_array::object::registry::run_with_gil;
337
338            run_with_gil(|| {
339                let mut builder = ObjectChunkedBuilder::new(self.name().clone(), self.len());
340                let chunks = self.downcast_iter();
341
342                // todo! use iterators once implemented
343                // no_null path
344                if !self.has_nulls() {
345                    for arr in chunks {
346                        for idx in 0..arr.len() {
347                            builder.append_value(arr.value(idx).clone())
348                        }
349                    }
350                } else {
351                    for arr in chunks {
352                        for idx in 0..arr.len() {
353                            if arr.is_valid(idx) {
354                                builder.append_value(arr.value(idx).clone())
355                            } else {
356                                builder.append_null()
357                            }
358                        }
359                    }
360                }
361                builder.finish()
362            })
363        }
364    }
365}
366
367#[cfg(test)]
368mod test {
369    #[cfg(feature = "dtype-categorical")]
370    use crate::prelude::*;
371
372    #[test]
373    #[cfg(feature = "dtype-categorical")]
374    fn test_categorical_map_after_rechunk() {
375        let s = Series::new(PlSmallStr::EMPTY, &["foo", "bar", "spam"]);
376        let mut a = s
377            .cast(&DataType::from_categories(Categories::global()))
378            .unwrap();
379
380        a.append(&a.slice(0, 2)).unwrap();
381        let a = a.rechunk();
382        assert!(a.cat32().unwrap().get_mapping().num_cats_upper_bound() > 0);
383    }
384}