polars_ops/chunked_array/list/
namespace.rs

1use std::fmt::Write;
2
3use arrow::array::ValueSize;
4#[cfg(feature = "list_gather")]
5use num_traits::ToPrimitive;
6#[cfg(feature = "list_gather")]
7use num_traits::{NumCast, Signed, Zero};
8use polars_compute::gather::sublist::list::{index_is_oob, sublist_get};
9use polars_core::chunked_array::builder::get_list_builder;
10#[cfg(feature = "diff")]
11use polars_core::series::ops::NullBehavior;
12use polars_core::utils::try_get_supertype;
13
14use super::*;
15#[cfg(feature = "list_any_all")]
16use crate::chunked_array::list::any_all::*;
17use crate::chunked_array::list::min_max::{list_max_function, list_min_function};
18use crate::chunked_array::list::sum_mean::sum_with_nulls;
19#[cfg(feature = "diff")]
20use crate::prelude::diff;
21use crate::prelude::list::sum_mean::{mean_list_numerical, sum_list_numerical};
22use crate::series::ArgAgg;
23
24pub(super) fn has_inner_nulls(ca: &ListChunked) -> bool {
25    for arr in ca.downcast_iter() {
26        if arr.values().null_count() > 0 {
27            return true;
28        }
29    }
30    false
31}
32
33fn cast_rhs(
34    other: &mut [Column],
35    inner_type: &DataType,
36    dtype: &DataType,
37    length: usize,
38    allow_broadcast: bool,
39) -> PolarsResult<()> {
40    for s in other.iter_mut() {
41        // make sure that inner types match before we coerce into list
42        if !matches!(s.dtype(), DataType::List(_)) {
43            *s = s.cast(inner_type)?
44        }
45        if !matches!(s.dtype(), DataType::List(_)) && s.dtype() == inner_type {
46            // coerce to list JIT
47            *s = s
48                .reshape_list(&[ReshapeDimension::Infer, ReshapeDimension::new_dimension(1)])
49                .unwrap();
50        }
51        if s.dtype() != dtype {
52            *s = s.cast(dtype).map_err(|e| {
53                polars_err!(
54                    SchemaMismatch:
55                    "cannot concat `{}` into a list of `{}`: {}",
56                    s.dtype(),
57                    dtype,
58                    e
59                )
60            })?;
61        }
62
63        if s.len() != length {
64            polars_ensure!(
65                s.len() == 1,
66                ShapeMismatch: "series length {} does not match expected length of {}",
67                s.len(), length
68            );
69            if allow_broadcast {
70                // broadcast JIT
71                *s = s.new_from_index(0, length)
72            }
73            // else do nothing
74        }
75    }
76    Ok(())
77}
78
79pub trait ListNameSpaceImpl: AsList {
80    /// In case the inner dtype [`DataType::String`], the individual items will be joined into a
81    /// single string separated by `separator`.
82    fn lst_join(
83        &self,
84        separator: &StringChunked,
85        ignore_nulls: bool,
86    ) -> PolarsResult<StringChunked> {
87        let ca = self.as_list();
88        match ca.inner_dtype() {
89            DataType::String => match separator.len() {
90                1 => match separator.get(0) {
91                    Some(separator) => self.join_literal(separator, ignore_nulls),
92                    _ => Ok(StringChunked::full_null(ca.name().clone(), ca.len())),
93                },
94                _ => self.join_many(separator, ignore_nulls),
95            },
96            dt => polars_bail!(op = "`lst.join`", got = dt, expected = "String"),
97        }
98    }
99
100    fn join_literal(&self, separator: &str, ignore_nulls: bool) -> PolarsResult<StringChunked> {
101        let ca = self.as_list();
102        // used to amortize heap allocs
103        let mut buf = String::with_capacity(128);
104        let mut builder = StringChunkedBuilder::new(ca.name().clone(), ca.len());
105
106        ca.for_each_amortized(|opt_s| {
107            let opt_val = opt_s.and_then(|s| {
108                // make sure that we don't write values of previous iteration
109                buf.clear();
110                let ca = s.as_ref().str().unwrap();
111
112                if ca.null_count() != 0 && !ignore_nulls {
113                    return None;
114                }
115
116                for arr in ca.downcast_iter() {
117                    for val in arr.non_null_values_iter() {
118                        buf.write_str(val).unwrap();
119                        buf.write_str(separator).unwrap();
120                    }
121                }
122
123                // last value should not have a separator, so slice that off
124                // saturating sub because there might have been nothing written.
125                Some(&buf[..buf.len().saturating_sub(separator.len())])
126            });
127            builder.append_option(opt_val)
128        });
129        Ok(builder.finish())
130    }
131
132    fn join_many(
133        &self,
134        separator: &StringChunked,
135        ignore_nulls: bool,
136    ) -> PolarsResult<StringChunked> {
137        let ca = self.as_list();
138        // used to amortize heap allocs
139        let mut buf = String::with_capacity(128);
140        let mut builder = StringChunkedBuilder::new(ca.name().clone(), ca.len());
141        {
142            ca.amortized_iter()
143                .zip(separator)
144                .for_each(|(opt_s, opt_sep)| match opt_sep {
145                    Some(separator) => {
146                        let opt_val = opt_s.and_then(|s| {
147                            // make sure that we don't write values of previous iteration
148                            buf.clear();
149                            let ca = s.as_ref().str().unwrap();
150
151                            if ca.null_count() != 0 && !ignore_nulls {
152                                return None;
153                            }
154
155                            for arr in ca.downcast_iter() {
156                                for val in arr.non_null_values_iter() {
157                                    buf.write_str(val).unwrap();
158                                    buf.write_str(separator).unwrap();
159                                }
160                            }
161
162                            // last value should not have a separator, so slice that off
163                            // saturating sub because there might have been nothing written.
164                            Some(&buf[..buf.len().saturating_sub(separator.len())])
165                        });
166                        builder.append_option(opt_val)
167                    },
168                    _ => builder.append_null(),
169                })
170        }
171        Ok(builder.finish())
172    }
173
174    fn lst_max(&self) -> PolarsResult<Series> {
175        list_max_function(self.as_list())
176    }
177
178    #[cfg(feature = "list_any_all")]
179    fn lst_all(&self) -> PolarsResult<Series> {
180        let ca = self.as_list();
181        list_all(ca)
182    }
183
184    #[cfg(feature = "list_any_all")]
185    fn lst_any(&self) -> PolarsResult<Series> {
186        let ca = self.as_list();
187        list_any(ca)
188    }
189
190    fn lst_min(&self) -> PolarsResult<Series> {
191        list_min_function(self.as_list())
192    }
193
194    fn lst_sum(&self) -> PolarsResult<Series> {
195        let ca = self.as_list();
196
197        if has_inner_nulls(ca) {
198            return sum_with_nulls(ca, ca.inner_dtype());
199        };
200
201        match ca.inner_dtype() {
202            DataType::Boolean => Ok(count_boolean_bits(ca).into_series()),
203            dt if dt.is_primitive_numeric() => Ok(sum_list_numerical(ca, dt)),
204            dt => sum_with_nulls(ca, dt),
205        }
206    }
207
208    fn lst_mean(&self) -> Series {
209        let ca = self.as_list();
210
211        if has_inner_nulls(ca) {
212            return sum_mean::mean_with_nulls(ca);
213        };
214
215        match ca.inner_dtype() {
216            dt if dt.is_primitive_numeric() => mean_list_numerical(ca, dt),
217            _ => sum_mean::mean_with_nulls(ca),
218        }
219    }
220
221    fn lst_median(&self) -> Series {
222        let ca = self.as_list();
223        dispersion::median_with_nulls(ca)
224    }
225
226    fn lst_std(&self, ddof: u8) -> Series {
227        let ca = self.as_list();
228        dispersion::std_with_nulls(ca, ddof)
229    }
230
231    fn lst_var(&self, ddof: u8) -> Series {
232        let ca = self.as_list();
233        dispersion::var_with_nulls(ca, ddof)
234    }
235
236    fn same_type(&self, out: ListChunked) -> ListChunked {
237        let ca = self.as_list();
238        let dtype = ca.dtype();
239        if out.dtype() != dtype {
240            out.cast(ca.dtype()).unwrap().list().unwrap().clone()
241        } else {
242            out
243        }
244    }
245
246    fn lst_sort(&self, options: SortOptions) -> PolarsResult<ListChunked> {
247        let ca = self.as_list();
248        let out = ca.try_apply_amortized(|s| s.as_ref().sort_with(options))?;
249        Ok(self.same_type(out))
250    }
251
252    #[must_use]
253    fn lst_reverse(&self) -> ListChunked {
254        let ca = self.as_list();
255        let out = ca.apply_amortized(|s| s.as_ref().reverse());
256        self.same_type(out)
257    }
258
259    fn lst_n_unique(&self) -> PolarsResult<IdxCa> {
260        let ca = self.as_list();
261        ca.try_apply_amortized_generic(|s| {
262            let opt_v = s.map(|s| s.as_ref().n_unique()).transpose()?;
263            Ok(opt_v.map(|idx| idx as IdxSize))
264        })
265    }
266
267    fn lst_unique(&self) -> PolarsResult<ListChunked> {
268        let ca = self.as_list();
269        let out = ca.try_apply_amortized(|s| s.as_ref().unique())?;
270        Ok(self.same_type(out))
271    }
272
273    fn lst_unique_stable(&self) -> PolarsResult<ListChunked> {
274        let ca = self.as_list();
275        let out = ca.try_apply_amortized(|s| s.as_ref().unique_stable())?;
276        Ok(self.same_type(out))
277    }
278
279    fn lst_arg_min(&self) -> IdxCa {
280        let ca = self.as_list();
281        ca.apply_amortized_generic(|opt_s| {
282            opt_s.and_then(|s| s.as_ref().arg_min().map(|idx| idx as IdxSize))
283        })
284    }
285
286    fn lst_arg_max(&self) -> IdxCa {
287        let ca = self.as_list();
288        ca.apply_amortized_generic(|opt_s| {
289            opt_s.and_then(|s| s.as_ref().arg_max().map(|idx| idx as IdxSize))
290        })
291    }
292
293    #[cfg(feature = "diff")]
294    fn lst_diff(&self, n: i64, null_behavior: NullBehavior) -> PolarsResult<ListChunked> {
295        let ca = self.as_list();
296        ca.try_apply_amortized(|s| diff(s.as_ref(), n, null_behavior))
297    }
298
299    fn lst_shift(&self, periods: &Column) -> PolarsResult<ListChunked> {
300        let ca = self.as_list();
301        let periods_s = periods.cast(&DataType::Int64)?;
302        let periods = periods_s.i64()?;
303        let out = match periods.len() {
304            1 => {
305                if let Some(periods) = periods.get(0) {
306                    ca.apply_amortized(|s| s.as_ref().shift(periods))
307                } else {
308                    ListChunked::full_null_with_dtype(ca.name().clone(), ca.len(), ca.inner_dtype())
309                }
310            },
311            _ => ca.zip_and_apply_amortized(periods, |opt_s, opt_periods| {
312                match (opt_s, opt_periods) {
313                    (Some(s), Some(periods)) => Some(s.as_ref().shift(periods)),
314                    _ => None,
315                }
316            }),
317        };
318        Ok(self.same_type(out))
319    }
320
321    fn lst_slice(&self, offset: i64, length: usize) -> ListChunked {
322        let ca = self.as_list();
323        let out = ca.apply_amortized(|s| s.as_ref().slice(offset, length));
324        self.same_type(out)
325    }
326
327    fn lst_lengths(&self) -> IdxCa {
328        let ca = self.as_list();
329
330        let ca_validity = ca.rechunk_validity();
331
332        if ca_validity.as_ref().is_some_and(|x| x.set_bits() == 0) {
333            return IdxCa::full_null(ca.name().clone(), ca.len());
334        }
335
336        let mut lengths = Vec::with_capacity(ca.len());
337        ca.downcast_iter().for_each(|arr| {
338            let offsets = arr.offsets().as_slice();
339            let mut last = offsets[0];
340            for o in &offsets[1..] {
341                lengths.push((*o - last) as IdxSize);
342                last = *o;
343            }
344        });
345
346        let arr = IdxArr::from_vec(lengths).with_validity(ca_validity);
347        IdxCa::with_chunk(ca.name().clone(), arr)
348    }
349
350    /// Get the value by index in the sublists.
351    /// So index `0` would return the first item of every sublist
352    /// and index `-1` would return the last item of every sublist
353    /// if an index is out of bounds, it will return a `None`.
354    fn lst_get(&self, idx: i64, null_on_oob: bool) -> PolarsResult<Series> {
355        let ca = self.as_list();
356        if !null_on_oob && ca.downcast_iter().any(|arr| index_is_oob(arr, idx)) {
357            polars_bail!(ComputeError: "get index is out of bounds");
358        }
359
360        let chunks = ca
361            .downcast_iter()
362            .map(|arr| sublist_get(arr, idx))
363            .collect::<Vec<_>>();
364
365        let s = Series::try_from((ca.name().clone(), chunks)).unwrap();
366        // SAFETY: every element in list has dtype equal to its inner type
367        unsafe { s.from_physical_unchecked(ca.inner_dtype()) }
368    }
369
370    #[cfg(feature = "list_gather")]
371    fn lst_gather_every(&self, n: &IdxCa, offset: &IdxCa) -> PolarsResult<Series> {
372        let list_ca = self.as_list();
373        let out = match (n.len(), offset.len()) {
374            (1, 1) => match (n.get(0), offset.get(0)) {
375                (Some(n), Some(offset)) => list_ca.try_apply_amortized(|s| {
376                    s.as_ref().gather_every(n as usize, offset as usize)
377                })?,
378                _ => ListChunked::full_null_with_dtype(
379                    list_ca.name().clone(),
380                    list_ca.len(),
381                    list_ca.inner_dtype(),
382                ),
383            },
384            (1, len_offset) if len_offset == list_ca.len() => {
385                if let Some(n) = n.get(0) {
386                    list_ca.try_zip_and_apply_amortized(offset, |opt_s, opt_offset| {
387                        match (opt_s, opt_offset) {
388                            (Some(s), Some(offset)) => {
389                                Ok(Some(s.as_ref().gather_every(n as usize, offset as usize)?))
390                            },
391                            _ => Ok(None),
392                        }
393                    })?
394                } else {
395                    ListChunked::full_null_with_dtype(
396                        list_ca.name().clone(),
397                        list_ca.len(),
398                        list_ca.inner_dtype(),
399                    )
400                }
401            },
402            (len_n, 1) if len_n == list_ca.len() => {
403                if let Some(offset) = offset.get(0) {
404                    list_ca.try_zip_and_apply_amortized(n, |opt_s, opt_n| match (opt_s, opt_n) {
405                        (Some(s), Some(n)) => {
406                            Ok(Some(s.as_ref().gather_every(n as usize, offset as usize)?))
407                        },
408                        _ => Ok(None),
409                    })?
410                } else {
411                    ListChunked::full_null_with_dtype(
412                        list_ca.name().clone(),
413                        list_ca.len(),
414                        list_ca.inner_dtype(),
415                    )
416                }
417            },
418            (len_n, len_offset) if len_n == len_offset && len_n == list_ca.len() => list_ca
419                .try_binary_zip_and_apply_amortized(
420                    n,
421                    offset,
422                    |opt_s, opt_n, opt_offset| match (opt_s, opt_n, opt_offset) {
423                        (Some(s), Some(n), Some(offset)) => {
424                            Ok(Some(s.as_ref().gather_every(n as usize, offset as usize)?))
425                        },
426                        _ => Ok(None),
427                    },
428                )?,
429            _ => {
430                polars_bail!(ComputeError: "The lengths of `n` and `offset` should be 1 or equal to the length of list.")
431            },
432        };
433        Ok(out.into_series())
434    }
435
436    #[cfg(feature = "list_gather")]
437    fn lst_gather(&self, idx: &Series, null_on_oob: bool) -> PolarsResult<Series> {
438        let list_ca = self.as_list();
439
440        let index_typed_index = |idx: &Series| {
441            let idx = idx.cast(&IDX_DTYPE).unwrap();
442            {
443                list_ca
444                    .amortized_iter()
445                    .map(|s| {
446                        s.map(|s| {
447                            let s = s.as_ref();
448                            take_series(s, idx.clone(), null_on_oob)
449                        })
450                        .transpose()
451                    })
452                    .collect::<PolarsResult<ListChunked>>()
453                    .map(|mut ca| {
454                        ca.rename(list_ca.name().clone());
455                        ca.into_series()
456                    })
457            }
458        };
459
460        use DataType::*;
461        match idx.dtype() {
462            List(boxed_dt) if boxed_dt.is_integer() => {
463                let idx_ca = idx.list().unwrap();
464                let mut out = {
465                    list_ca
466                        .amortized_iter()
467                        .zip(idx_ca)
468                        .map(|(opt_s, opt_idx)| {
469                            {
470                                match (opt_s, opt_idx) {
471                                    (Some(s), Some(idx)) => {
472                                        Some(take_series(s.as_ref(), idx, null_on_oob))
473                                    },
474                                    _ => None,
475                                }
476                            }
477                            .transpose()
478                        })
479                        .collect::<PolarsResult<ListChunked>>()?
480                };
481                out.rename(list_ca.name().clone());
482
483                Ok(out.into_series())
484            },
485            UInt32 | UInt64 => index_typed_index(idx),
486            dt if dt.is_signed_integer() => {
487                if let Some(min) = idx.min::<i64>().unwrap() {
488                    if min >= 0 {
489                        index_typed_index(idx)
490                    } else {
491                        let mut out = {
492                            list_ca
493                                .amortized_iter()
494                                .map(|opt_s| {
495                                    opt_s
496                                        .map(|s| take_series(s.as_ref(), idx.clone(), null_on_oob))
497                                        .transpose()
498                                })
499                                .collect::<PolarsResult<ListChunked>>()?
500                        };
501                        out.rename(list_ca.name().clone());
502                        Ok(out.into_series())
503                    }
504                } else {
505                    polars_bail!(ComputeError: "all indices are null");
506                }
507            },
508            dt => polars_bail!(ComputeError: "cannot use dtype `{}` as an index", dt),
509        }
510    }
511
512    #[cfg(feature = "list_drop_nulls")]
513    fn lst_drop_nulls(&self) -> ListChunked {
514        let list_ca = self.as_list();
515
516        list_ca.apply_amortized(|s| s.as_ref().drop_nulls())
517    }
518
519    #[cfg(feature = "list_sample")]
520    fn lst_sample_n(
521        &self,
522        n: &Series,
523        with_replacement: bool,
524        shuffle: bool,
525        seed: Option<u64>,
526    ) -> PolarsResult<ListChunked> {
527        let ca = self.as_list();
528
529        let n_s = n.cast(&IDX_DTYPE)?;
530        let n = n_s.idx()?;
531
532        let out = match n.len() {
533            1 => {
534                if let Some(n) = n.get(0) {
535                    ca.try_apply_amortized(|s| {
536                        s.as_ref()
537                            .sample_n(n as usize, with_replacement, shuffle, seed)
538                    })
539                } else {
540                    Ok(ListChunked::full_null_with_dtype(
541                        ca.name().clone(),
542                        ca.len(),
543                        ca.inner_dtype(),
544                    ))
545                }
546            },
547            _ => ca.try_zip_and_apply_amortized(n, |opt_s, opt_n| match (opt_s, opt_n) {
548                (Some(s), Some(n)) => s
549                    .as_ref()
550                    .sample_n(n as usize, with_replacement, shuffle, seed)
551                    .map(Some),
552                _ => Ok(None),
553            }),
554        };
555        out.map(|ok| self.same_type(ok))
556    }
557
558    #[cfg(feature = "list_sample")]
559    fn lst_sample_fraction(
560        &self,
561        fraction: &Series,
562        with_replacement: bool,
563        shuffle: bool,
564        seed: Option<u64>,
565    ) -> PolarsResult<ListChunked> {
566        let ca = self.as_list();
567
568        let fraction_s = fraction.cast(&DataType::Float64)?;
569        let fraction = fraction_s.f64()?;
570
571        let out = match fraction.len() {
572            1 => {
573                if let Some(fraction) = fraction.get(0) {
574                    ca.try_apply_amortized(|s| {
575                        let n = (s.as_ref().len() as f64 * fraction) as usize;
576                        s.as_ref().sample_n(n, with_replacement, shuffle, seed)
577                    })
578                } else {
579                    Ok(ListChunked::full_null_with_dtype(
580                        ca.name().clone(),
581                        ca.len(),
582                        ca.inner_dtype(),
583                    ))
584                }
585            },
586            _ => ca.try_zip_and_apply_amortized(fraction, |opt_s, opt_n| match (opt_s, opt_n) {
587                (Some(s), Some(fraction)) => {
588                    let n = (s.as_ref().len() as f64 * fraction) as usize;
589                    s.as_ref()
590                        .sample_n(n, with_replacement, shuffle, seed)
591                        .map(Some)
592                },
593                _ => Ok(None),
594            }),
595        };
596        out.map(|ok| self.same_type(ok))
597    }
598
599    fn lst_concat(&self, other: &[Column]) -> PolarsResult<ListChunked> {
600        let ca = self.as_list();
601        let other_len = other.len();
602        let length = ca.len();
603        let mut other = other.to_vec();
604        let mut inner_super_type = ca.inner_dtype().clone();
605
606        for s in &other {
607            match s.dtype() {
608                DataType::List(inner_type) => {
609                    inner_super_type = try_get_supertype(&inner_super_type, inner_type)?;
610                    #[cfg(feature = "dtype-categorical")]
611                    if matches!(
612                        &inner_super_type,
613                        DataType::Categorical(_, _) | DataType::Enum(_, _)
614                    ) {
615                        inner_super_type = merge_dtypes(&inner_super_type, inner_type)?;
616                    }
617                },
618                dt => {
619                    inner_super_type = try_get_supertype(&inner_super_type, dt)?;
620                    #[cfg(feature = "dtype-categorical")]
621                    if matches!(
622                        &inner_super_type,
623                        DataType::Categorical(_, _) | DataType::Enum(_, _)
624                    ) {
625                        inner_super_type = merge_dtypes(&inner_super_type, dt)?;
626                    }
627                },
628            }
629        }
630
631        // cast lhs
632        let dtype = &DataType::List(Box::new(inner_super_type.clone()));
633        let ca = ca.cast(dtype)?;
634        let ca = ca.list().unwrap();
635
636        // broadcasting path in case all unit length
637        // this path will not expand the series, so saves memory
638        let out = if other.iter().all(|s| s.len() == 1) && ca.len() != 1 {
639            cast_rhs(&mut other, &inner_super_type, dtype, length, false)?;
640            let to_append = other
641                .iter()
642                .flat_map(|s| {
643                    let lst = s.list().unwrap();
644                    lst.get_as_series(0)
645                })
646                .collect::<Vec<_>>();
647            // there was a None, so all values will be None
648            if to_append.len() != other_len {
649                return Ok(ListChunked::full_null_with_dtype(
650                    ca.name().clone(),
651                    length,
652                    &inner_super_type,
653                ));
654            }
655
656            let vals_size_other = other
657                .iter()
658                .map(|s| s.list().unwrap().get_values_size())
659                .sum::<usize>();
660
661            let mut builder = get_list_builder(
662                &inner_super_type,
663                ca.get_values_size() + vals_size_other + 1,
664                length,
665                ca.name().clone(),
666            );
667            ca.into_iter().for_each(|opt_s| {
668                let opt_s = opt_s.map(|mut s| {
669                    for append in &to_append {
670                        s.append(append).unwrap();
671                    }
672                    match inner_super_type {
673                        // structs don't have chunks, so we must first rechunk the underlying series
674                        #[cfg(feature = "dtype-struct")]
675                        DataType::Struct(_) => s = s.rechunk(),
676                        // nothing
677                        _ => {},
678                    }
679                    s
680                });
681                builder.append_opt_series(opt_s.as_ref()).unwrap();
682            });
683            builder.finish()
684        } else {
685            // normal path which may contain same length list or unit length lists
686            cast_rhs(&mut other, &inner_super_type, dtype, length, true)?;
687
688            let vals_size_other = other
689                .iter()
690                .map(|s| s.list().unwrap().get_values_size())
691                .sum::<usize>();
692            let mut iters = Vec::with_capacity(other_len + 1);
693
694            for s in other.iter_mut() {
695                iters.push(s.list()?.amortized_iter())
696            }
697            let mut first_iter: Box<dyn PolarsIterator<Item = Option<Series>>> = ca.into_iter();
698            let mut builder = get_list_builder(
699                &inner_super_type,
700                ca.get_values_size() + vals_size_other + 1,
701                length,
702                ca.name().clone(),
703            );
704
705            for _ in 0..ca.len() {
706                let mut acc = match first_iter.next().unwrap() {
707                    Some(s) => s,
708                    None => {
709                        builder.append_null();
710                        // make sure that the iterators advance before we continue
711                        for it in &mut iters {
712                            it.next().unwrap();
713                        }
714                        continue;
715                    },
716                };
717
718                let mut has_nulls = false;
719                for it in &mut iters {
720                    match it.next().unwrap() {
721                        Some(s) => {
722                            if !has_nulls {
723                                acc.append(s.as_ref())?;
724                            }
725                        },
726                        None => {
727                            has_nulls = true;
728                        },
729                    }
730                }
731                if has_nulls {
732                    builder.append_null();
733                    continue;
734                }
735
736                match inner_super_type {
737                    // structs don't have chunks, so we must first rechunk the underlying series
738                    #[cfg(feature = "dtype-struct")]
739                    DataType::Struct(_) => acc = acc.rechunk(),
740                    // nothing
741                    _ => {},
742                }
743                builder.append_series(&acc).unwrap();
744            }
745            builder.finish()
746        };
747        Ok(out)
748    }
749}
750
751impl ListNameSpaceImpl for ListChunked {}
752
753#[cfg(feature = "list_gather")]
754fn take_series(s: &Series, idx: Series, null_on_oob: bool) -> PolarsResult<Series> {
755    let len = s.len();
756    let idx = cast_index(idx, len, null_on_oob)?;
757    let idx = idx.idx().unwrap();
758    s.take(idx)
759}
760
761#[cfg(feature = "list_gather")]
762fn cast_signed_index_ca<T: PolarsNumericType>(idx: &ChunkedArray<T>, len: usize) -> Series
763where
764    T::Native: Copy + PartialOrd + PartialEq + NumCast + Signed + Zero,
765{
766    idx.iter()
767        .map(|opt_idx| opt_idx.and_then(|idx| idx.negative_to_usize(len).map(|idx| idx as IdxSize)))
768        .collect::<IdxCa>()
769        .into_series()
770}
771
772#[cfg(feature = "list_gather")]
773fn cast_unsigned_index_ca<T: PolarsNumericType>(idx: &ChunkedArray<T>, len: usize) -> Series
774where
775    T::Native: Copy + PartialOrd + ToPrimitive,
776{
777    idx.iter()
778        .map(|opt_idx| {
779            opt_idx.and_then(|idx| {
780                let idx = idx.to_usize().unwrap();
781                if idx >= len {
782                    None
783                } else {
784                    Some(idx as IdxSize)
785                }
786            })
787        })
788        .collect::<IdxCa>()
789        .into_series()
790}
791
792#[cfg(feature = "list_gather")]
793fn cast_index(idx: Series, len: usize, null_on_oob: bool) -> PolarsResult<Series> {
794    let idx_null_count = idx.null_count();
795    use DataType::*;
796    let out = match idx.dtype() {
797        #[cfg(feature = "big_idx")]
798        UInt32 => {
799            if null_on_oob {
800                let a = idx.u32().unwrap();
801                cast_unsigned_index_ca(a, len)
802            } else {
803                idx.cast(&IDX_DTYPE).unwrap()
804            }
805        },
806        #[cfg(feature = "big_idx")]
807        UInt64 => {
808            if null_on_oob {
809                let a = idx.u64().unwrap();
810                cast_unsigned_index_ca(a, len)
811            } else {
812                idx
813            }
814        },
815        #[cfg(not(feature = "big_idx"))]
816        UInt64 => {
817            if null_on_oob {
818                let a = idx.u64().unwrap();
819                cast_unsigned_index_ca(a, len)
820            } else {
821                idx.cast(&IDX_DTYPE).unwrap()
822            }
823        },
824        #[cfg(not(feature = "big_idx"))]
825        UInt32 => {
826            if null_on_oob {
827                let a = idx.u32().unwrap();
828                cast_unsigned_index_ca(a, len)
829            } else {
830                idx
831            }
832        },
833        dt if dt.is_unsigned_integer() => idx.cast(&IDX_DTYPE).unwrap(),
834        Int8 => {
835            let a = idx.i8().unwrap();
836            cast_signed_index_ca(a, len)
837        },
838        Int16 => {
839            let a = idx.i16().unwrap();
840            cast_signed_index_ca(a, len)
841        },
842        Int32 => {
843            let a = idx.i32().unwrap();
844            cast_signed_index_ca(a, len)
845        },
846        Int64 => {
847            let a = idx.i64().unwrap();
848            cast_signed_index_ca(a, len)
849        },
850        _ => {
851            unreachable!()
852        },
853    };
854    polars_ensure!(
855        out.null_count() == idx_null_count || null_on_oob,
856        OutOfBounds: "gather indices are out of bounds"
857    );
858    Ok(out)
859}
860
861// TODO: implement the above for ArrayChunked as well?