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