Skip to main content

polars_ops/chunked_array/list/
namespace.rs

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