Skip to main content

polars_ops/chunked_array/list/
namespace.rs

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