Skip to main content

polars_core/chunked_array/ops/
explode.rs

1#![allow(unsafe_op_in_unsafe_fn)]
2use polars_arrow::array::*;
3use polars_arrow::bitmap::utils::set_bit_unchecked;
4use polars_arrow::bitmap::{Bitmap, MutableBitmap};
5use polars_arrow::legacy::array::SlicedArray;
6use polars_arrow::offset::OffsetsBuffer;
7
8use crate::prelude::*;
9use crate::series::implementations::null::NullChunked;
10
11pub(crate) trait ExplodeByOffsets {
12    fn explode_by_offsets(&self, offsets: &[i64], options: ExplodeOptions) -> Series;
13}
14
15unsafe fn unset_nulls(
16    start: usize,
17    last: usize,
18    validity_values: &Bitmap,
19    nulls: &mut Vec<usize>,
20    empty_row_idx: &[usize],
21    base_offset: usize,
22) {
23    for i in start..last {
24        if !validity_values.get_bit_unchecked(i) {
25            nulls.push(i + empty_row_idx.len() - base_offset);
26        }
27    }
28}
29
30fn get_capacity(offsets: &[i64]) -> usize {
31    (offsets[offsets.len() - 1] - offsets[0] + 1) as usize
32}
33
34impl<T> ExplodeByOffsets for ChunkedArray<T>
35where
36    T: PolarsIntegerType,
37{
38    fn explode_by_offsets(&self, offsets: &[i64], options: ExplodeOptions) -> Series {
39        debug_assert_eq!(self.chunks.len(), 1);
40        let arr = self.downcast_iter().next().unwrap();
41
42        // make sure that we don't look beyond the sliced array
43        let values = &arr.values().as_slice()[..offsets[offsets.len() - 1] as usize];
44
45        let mut empty_row_idx = vec![];
46        let mut nulls = vec![];
47
48        let mut start = offsets[0] as usize;
49        let base_offset = start;
50        let mut last = start;
51
52        let mut new_values = Vec::with_capacity(offsets[offsets.len() - 1] as usize - start + 1);
53
54        // we check all the offsets and in the case a consecutive offset is the same,
55        // e.g. 0, 1, 4, 4, 6
56        // the 4 4, means that that is an empty row.
57        // the empty row will be replaced with a None value.
58        //
59        // below we memcpy as much as possible and for the empty rows we add a default value
60        // that value will later be masked out by the validity bitmap
61
62        // in the case that the value array has got null values, we need to check every validity
63        // value and collect the indices.
64        // because the length of the array is not known, we first collect the null indexes, offset
65        // with the insertion of empty rows (as None) and later create a validity bitmap
66        if arr.null_count() > 0 {
67            let validity_values = arr.validity().unwrap();
68
69            for &o in &offsets[1..] {
70                let o = o as usize;
71                if options.empty_as_null && o == last {
72                    if start != last {
73                        #[cfg(debug_assertions)]
74                        new_values.extend_from_slice(&values[start..last]);
75
76                        #[cfg(not(debug_assertions))]
77                        unsafe {
78                            new_values.extend_from_slice(values.get_unchecked(start..last))
79                        };
80
81                        // SAFETY:
82                        // we are in bounds
83                        unsafe {
84                            unset_nulls(
85                                start,
86                                last,
87                                validity_values,
88                                &mut nulls,
89                                &empty_row_idx,
90                                base_offset,
91                            )
92                        }
93                    }
94
95                    empty_row_idx.push(o + empty_row_idx.len() - base_offset);
96                    new_values.push(T::Native::default());
97                    start = o;
98                }
99                last = o;
100            }
101
102            // final null check
103            // SAFETY:
104            // we are in bounds
105            unsafe {
106                unset_nulls(
107                    start,
108                    last,
109                    validity_values,
110                    &mut nulls,
111                    &empty_row_idx,
112                    base_offset,
113                )
114            }
115        } else {
116            for &o in &offsets[1..] {
117                let o = o as usize;
118                if options.empty_as_null && o == last {
119                    if start != last {
120                        unsafe { new_values.extend_from_slice(values.get_unchecked(start..last)) };
121                    }
122
123                    empty_row_idx.push(o + empty_row_idx.len() - base_offset);
124                    new_values.push(T::Native::default());
125                    start = o;
126                }
127                last = o;
128            }
129        }
130
131        // add remaining values
132        new_values.extend_from_slice(&values[start..]);
133
134        let mut validity = MutableBitmap::with_capacity(new_values.len());
135        validity.extend_constant(new_values.len(), true);
136        let validity_slice = validity.as_mut_slice();
137
138        for i in empty_row_idx {
139            unsafe { set_bit_unchecked(validity_slice, i, false) }
140        }
141        for i in nulls {
142            unsafe { set_bit_unchecked(validity_slice, i, false) }
143        }
144        let arr = PrimitiveArray::new(
145            T::get_static_dtype().to_arrow(CompatLevel::newest()),
146            new_values.into(),
147            Some(validity.into()),
148        );
149        Series::try_from((self.name().clone(), Box::new(arr) as ArrayRef)).unwrap()
150    }
151}
152
153#[cfg(feature = "dtype-f16")]
154impl ExplodeByOffsets for Float16Chunked {
155    fn explode_by_offsets(&self, offsets: &[i64], options: ExplodeOptions) -> Series {
156        self.apply_as_ints(|s| {
157            let ca = s.u16().unwrap();
158            ca.explode_by_offsets(offsets, options)
159        })
160    }
161}
162
163impl ExplodeByOffsets for Float32Chunked {
164    fn explode_by_offsets(&self, offsets: &[i64], options: ExplodeOptions) -> Series {
165        self.apply_as_ints(|s| {
166            let ca = s.u32().unwrap();
167            ca.explode_by_offsets(offsets, options)
168        })
169    }
170}
171impl ExplodeByOffsets for Float64Chunked {
172    fn explode_by_offsets(&self, offsets: &[i64], options: ExplodeOptions) -> Series {
173        self.apply_as_ints(|s| {
174            let ca = s.u64().unwrap();
175            ca.explode_by_offsets(offsets, options)
176        })
177    }
178}
179
180impl ExplodeByOffsets for NullChunked {
181    fn explode_by_offsets(&self, offsets: &[i64], options: ExplodeOptions) -> Series {
182        let mut last_offset = offsets[0];
183
184        let mut len = 0;
185        for &offset in &offsets[1..] {
186            // If offset == last_offset we have an empty list and a new row is inserted,
187            // therefore we always increase at least 1.
188            len += std::cmp::max(offset - last_offset, i64::from(options.empty_as_null)) as usize;
189            last_offset = offset;
190        }
191        NullChunked::new(self.name.clone(), len).into_series()
192    }
193}
194
195impl ExplodeByOffsets for BooleanChunked {
196    fn explode_by_offsets(&self, offsets: &[i64], options: ExplodeOptions) -> Series {
197        debug_assert_eq!(self.chunks.len(), 1);
198        let arr = self.downcast_iter().next().unwrap();
199
200        let cap = get_capacity(offsets);
201        let mut builder = BooleanChunkedBuilder::new(self.name().clone(), cap);
202
203        let mut start = offsets[0] as usize;
204        let mut last = start;
205        for &o in &offsets[1..] {
206            let o = o as usize;
207            if options.empty_as_null && o == last {
208                if start != last {
209                    let vals = arr.slice_typed(start, last - start);
210
211                    if vals.null_count() == 0 {
212                        builder
213                            .array_builder
214                            .extend_trusted_len_values(vals.values_iter())
215                    } else {
216                        builder.array_builder.extend_trusted_len(vals.into_iter());
217                    }
218                }
219                builder.append_null();
220                start = o;
221            }
222            last = o;
223        }
224        let vals = arr.slice_typed(start, last - start);
225        if vals.null_count() == 0 {
226            builder
227                .array_builder
228                .extend_trusted_len_values(vals.values_iter())
229        } else {
230            builder.array_builder.extend_trusted_len(vals.into_iter());
231        }
232        builder.finish().into()
233    }
234}
235
236/// Number of rows `explode` emits for one row of the offsets returned by `explode_and_offsets`.
237#[inline]
238fn exploded_len(start: i64, end: i64, is_valid: bool, options: ExplodeOptions) -> usize {
239    if start == end {
240        ((is_valid && options.empty_as_null) || (!is_valid && options.keep_nulls)) as usize
241    } else {
242        (end - start) as usize
243    }
244}
245
246/// Explode a List or Array `Series`, also returning where each input row starts in the output.
247pub fn explode_with_row_offsets(
248    s: &Series,
249    options: ExplodeOptions,
250) -> PolarsResult<(Series, OffsetsBuffer<i64>)> {
251    let s = s.rechunk();
252    let (out, offsets) = match s.dtype() {
253        DataType::List(_) => s.list()?.explode_and_offsets(options)?,
254        #[cfg(feature = "dtype-array")]
255        DataType::Array(_, width) => {
256            let (out, offsets) = s.array()?.explode_and_offsets(options)?;
257            // Width 0 doesn't return per-row offsets.
258            let offsets = if *width == 0 {
259                polars_arrow::offset::Offsets::new_zeroed(s.len()).into()
260            } else {
261                offsets
262            };
263            (out, offsets)
264        },
265        dt => polars_bail!(opq = explode_with_row_offsets, dt),
266    };
267
268    let validity = s.rechunk_validity();
269    let mut row_offsets = Vec::with_capacity(offsets.len());
270    let mut length_so_far = 0i64;
271    row_offsets.push(length_so_far);
272    for (i, w) in offsets.as_slice().windows(2).enumerate() {
273        let is_valid = validity.as_ref().is_none_or(|v| v.get_bit(i));
274        length_so_far += exploded_len(w[0], w[1], is_valid, options) as i64;
275        row_offsets.push(length_so_far);
276    }
277    polars_ensure!(
278        length_so_far as usize == out.len(),
279        ComputeError: "explode produced {} rows, expected {}", out.len(), length_so_far
280    );
281
282    // SAFETY: monotonically increasing.
283    Ok((out, unsafe {
284        OffsetsBuffer::new_unchecked(row_offsets.into())
285    }))
286}
287
288/// Convert Arrow array offsets to indexes of the original list
289pub(crate) fn offsets_to_indexes(
290    offsets: &[i64],
291    capacity: usize,
292    options: ExplodeOptions,
293    validity: Option<&Bitmap>,
294) -> Vec<IdxSize> {
295    if offsets.is_empty() {
296        return vec![];
297    }
298
299    let mut idx = Vec::with_capacity(capacity);
300
301    let mut last_idx = 0;
302    match validity {
303        None => {
304            for (offset_start, offset_end) in offsets.iter().zip(offsets[1..].iter()) {
305                if idx.len() >= capacity {
306                    // significant speed-up in edge cases with many offsets,
307                    // no measurable overhead in typical case due to branch prediction
308                    break;
309                }
310
311                let n = exploded_len(*offset_start, *offset_end, true, options);
312                idx.extend(std::iter::repeat_n(last_idx, n));
313                last_idx += 1;
314            }
315        },
316        Some(validity) => {
317            for ((offset_start, offset_end), is_valid) in
318                offsets.iter().zip(offsets[1..].iter()).zip(validity.iter())
319            {
320                if idx.len() >= capacity {
321                    // significant speed-up in edge cases with many offsets,
322                    // no measurable overhead in typical case due to branch prediction
323                    break;
324                }
325
326                let n = exploded_len(*offset_start, *offset_end, is_valid, options);
327                idx.extend(std::iter::repeat_n(last_idx, n));
328                last_idx += 1;
329            }
330        },
331    }
332
333    // take the remaining values
334    for _ in 0..capacity.saturating_sub(idx.len()) {
335        idx.push(last_idx);
336    }
337    idx.truncate(capacity);
338    idx
339}
340
341#[cfg(test)]
342mod test {
343    use super::*;
344    use crate::chunked_array::builder::get_list_builder;
345
346    #[test]
347    fn test_explode_list() -> PolarsResult<()> {
348        let mut builder = get_list_builder(&DataType::Int32, 5, 5, PlSmallStr::from_static("a"));
349
350        builder
351            .append_series(&Series::new(PlSmallStr::EMPTY, &[1, 2, 3, 3]))
352            .unwrap();
353        builder
354            .append_series(&Series::new(PlSmallStr::EMPTY, &[1]))
355            .unwrap();
356        builder
357            .append_series(&Series::new(PlSmallStr::EMPTY, &[2]))
358            .unwrap();
359
360        let ca = builder.finish();
361        assert!(ca._can_fast_explode());
362
363        // normal explode
364        let exploded = ca.explode(ExplodeOptions {
365            empty_as_null: true,
366            keep_nulls: true,
367        })?;
368        let out: Vec<_> = exploded.i32()?.into_no_null_iter().collect();
369        assert_eq!(out, &[1, 2, 3, 3, 1, 2]);
370
371        // sliced explode
372        let exploded = ca.slice(0, 1).explode(ExplodeOptions {
373            empty_as_null: true,
374            keep_nulls: true,
375        })?;
376        let out: Vec<_> = exploded.i32()?.into_no_null_iter().collect();
377        assert_eq!(out, &[1, 2, 3, 3]);
378
379        Ok(())
380    }
381
382    #[test]
383    fn test_explode_empty_list_slot() -> PolarsResult<()> {
384        // primitive
385        let mut builder = get_list_builder(&DataType::Int32, 5, 5, PlSmallStr::from_static("a"));
386        builder
387            .append_series(&Series::new(PlSmallStr::EMPTY, &[1i32, 2]))
388            .unwrap();
389        builder
390            .append_series(&Int32Chunked::from_slice(PlSmallStr::EMPTY, &[]).into_series())
391            .unwrap();
392        builder
393            .append_series(&Series::new(PlSmallStr::EMPTY, &[3i32]))
394            .unwrap();
395
396        let ca = builder.finish();
397        let exploded = ca.explode(ExplodeOptions {
398            empty_as_null: true,
399            keep_nulls: true,
400        })?;
401        assert_eq!(
402            Vec::from(exploded.i32()?),
403            &[Some(1), Some(2), None, Some(3)]
404        );
405
406        // more primitive
407        let mut builder = get_list_builder(&DataType::Int32, 5, 5, PlSmallStr::from_static("a"));
408        builder
409            .append_series(&Series::new(PlSmallStr::EMPTY, &[1i32]))
410            .unwrap();
411        builder
412            .append_series(&Int32Chunked::from_slice(PlSmallStr::EMPTY, &[]).into_series())
413            .unwrap();
414        builder
415            .append_series(&Series::new(PlSmallStr::EMPTY, &[2i32]))
416            .unwrap();
417        builder
418            .append_series(&Int32Chunked::from_slice(PlSmallStr::EMPTY, &[]).into_series())
419            .unwrap();
420        builder
421            .append_series(&Series::new(PlSmallStr::EMPTY, &[3, 4i32]))
422            .unwrap();
423
424        let ca = builder.finish();
425        let exploded = ca.explode(ExplodeOptions {
426            empty_as_null: true,
427            keep_nulls: true,
428        })?;
429        assert_eq!(
430            Vec::from(exploded.i32()?),
431            &[Some(1), None, Some(2), None, Some(3), Some(4)]
432        );
433
434        // string
435        let mut builder = get_list_builder(&DataType::String, 5, 5, PlSmallStr::from_static("a"));
436        builder
437            .append_series(&Series::new(PlSmallStr::EMPTY, &["abc"]))
438            .unwrap();
439        builder
440            .append_series(
441                &<StringChunked as NewChunkedArray<StringType, &str>>::from_slice(
442                    PlSmallStr::EMPTY,
443                    &[],
444                )
445                .into_series(),
446            )
447            .unwrap();
448        builder
449            .append_series(&Series::new(PlSmallStr::EMPTY, &["de"]))
450            .unwrap();
451        builder
452            .append_series(
453                &<StringChunked as NewChunkedArray<StringType, &str>>::from_slice(
454                    PlSmallStr::EMPTY,
455                    &[],
456                )
457                .into_series(),
458            )
459            .unwrap();
460        builder
461            .append_series(&Series::new(PlSmallStr::EMPTY, &["fg"]))
462            .unwrap();
463        builder
464            .append_series(
465                &<StringChunked as NewChunkedArray<StringType, &str>>::from_slice(
466                    PlSmallStr::EMPTY,
467                    &[],
468                )
469                .into_series(),
470            )
471            .unwrap();
472
473        let ca = builder.finish();
474        let exploded = ca.explode(ExplodeOptions {
475            empty_as_null: true,
476            keep_nulls: true,
477        })?;
478        assert_eq!(
479            Vec::from(exploded.str()?),
480            &[Some("abc"), None, Some("de"), None, Some("fg"), None]
481        );
482
483        // boolean
484        let mut builder = get_list_builder(&DataType::Boolean, 5, 5, PlSmallStr::from_static("a"));
485        builder
486            .append_series(&Series::new(PlSmallStr::EMPTY, &[true]))
487            .unwrap();
488        builder
489            .append_series(&BooleanChunked::from_slice(PlSmallStr::EMPTY, &[]).into_series())
490            .unwrap();
491        builder
492            .append_series(&Series::new(PlSmallStr::EMPTY, &[false]))
493            .unwrap();
494        builder
495            .append_series(&BooleanChunked::from_slice(PlSmallStr::EMPTY, &[]).into_series())
496            .unwrap();
497        builder
498            .append_series(&Series::new(PlSmallStr::EMPTY, &[true, true]))
499            .unwrap();
500
501        let ca = builder.finish();
502        let exploded = ca.explode(ExplodeOptions {
503            empty_as_null: true,
504            keep_nulls: true,
505        })?;
506        assert_eq!(
507            Vec::from(exploded.bool()?),
508            &[Some(true), None, Some(false), None, Some(true), Some(true)]
509        );
510
511        Ok(())
512    }
513
514    #[test]
515    fn test_row_offsets() {
516        let offsets = &[0, 1, 2, 2, 3, 4, 4];
517        let out = offsets_to_indexes(
518            offsets,
519            6,
520            ExplodeOptions {
521                empty_as_null: true,
522                keep_nulls: true,
523            },
524            None,
525        );
526        assert_eq!(out, &[0, 1, 2, 3, 4, 5]);
527    }
528
529    #[test]
530    fn test_empty_row_offsets() {
531        let offsets = &[0, 0];
532        let out = offsets_to_indexes(
533            offsets,
534            0,
535            ExplodeOptions {
536                empty_as_null: true,
537                keep_nulls: true,
538            },
539            None,
540        );
541        let expected: Vec<IdxSize> = Vec::new();
542        assert_eq!(out, expected);
543    }
544
545    #[test]
546    fn test_row_offsets_over_capacity() {
547        let offsets = &[0, 1, 1, 2, 2];
548        let out = offsets_to_indexes(
549            offsets,
550            2,
551            ExplodeOptions {
552                empty_as_null: true,
553                keep_nulls: true,
554            },
555            None,
556        );
557        assert_eq!(out, &[0, 1]);
558    }
559
560    #[test]
561    fn test_row_offsets_nonzero_first_offset() {
562        let offsets = &[3, 6, 8];
563        let out = offsets_to_indexes(
564            offsets,
565            10,
566            ExplodeOptions {
567                empty_as_null: true,
568                keep_nulls: true,
569            },
570            None,
571        );
572        assert_eq!(out, &[0, 0, 0, 1, 1, 2, 2, 2, 2, 2]);
573    }
574}