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        use std::borrow::Cow;
554
555        let ca = self.as_list();
556
557        let n_s = n.strict_cast(&IDX_DTYPE)?;
558        let n = n_s.idx()?;
559
560        polars_ensure!(
561            ca.len() == n.len() || ca.len() == 1 || n.len() == 1,
562            length_mismatch = "list.sample(n)",
563            ca.len(),
564            n.len()
565        );
566
567        // Broadcast `self`
568        let mut ca = Cow::Borrowed(ca);
569        if ca.len() == 1 && n.len() != 1 {
570            // Optimize: Don't broadcast and instead have a special path.
571            ca = Cow::Owned(ca.new_from_index(0, n.len()));
572        }
573        let ca = ca.as_ref();
574
575        let out = match n.len() {
576            1 => {
577                if let Some(n) = n.get(0) {
578                    unsafe {
579                        // SAFETY: `sample_n` doesn't change the dtype
580                        ca.try_apply_amortized_same_type(|s| {
581                            s.as_ref()
582                                .sample_n(n as usize, with_replacement, shuffle, seed)
583                        })
584                    }
585                } else {
586                    Ok(ListChunked::full_null_with_dtype(
587                        ca.name().clone(),
588                        ca.len(),
589                        ca.inner_dtype(),
590                    ))
591                }
592            },
593            _ => ca.try_zip_and_apply_amortized(n, |opt_s, opt_n| match (opt_s, opt_n) {
594                (Some(s), Some(n)) => s
595                    .as_ref()
596                    .sample_n(n as usize, with_replacement, shuffle, seed)
597                    .map(Some),
598                _ => Ok(None),
599            }),
600        };
601        out.map(|ok| self.same_type(ok))
602    }
603
604    #[cfg(feature = "list_sample")]
605    fn lst_sample_fraction(
606        &self,
607        fraction: &Series,
608        with_replacement: bool,
609        shuffle: bool,
610        seed: Option<u64>,
611    ) -> PolarsResult<ListChunked> {
612        use std::borrow::Cow;
613
614        let ca = self.as_list();
615
616        let fraction_s = fraction.cast(&DataType::Float64)?;
617        let fraction = fraction_s.f64()?;
618
619        if !with_replacement {
620            for frac in fraction.iter().flatten() {
621                polars_ensure!(
622                    (0.0..=1.0).contains(&frac),
623                    ComputeError: "fraction must be between 0.0 and 1.0, got: {}", frac
624                )
625            }
626        }
627
628        polars_ensure!(
629            ca.len() == fraction.len() || ca.len() == 1 || fraction.len() == 1,
630            length_mismatch = "list.sample(fraction)",
631            ca.len(),
632            fraction.len()
633        );
634
635        // Broadcast `self`
636        let mut ca = Cow::Borrowed(ca);
637        if ca.len() == 1 && fraction.len() != 1 {
638            // Optimize: Don't broadcast and instead have a special path.
639            ca = Cow::Owned(ca.new_from_index(0, fraction.len()));
640        }
641        let ca = ca.as_ref();
642
643        let out = match fraction.len() {
644            1 => {
645                if let Some(fraction) = fraction.get(0) {
646                    unsafe {
647                        // SAFETY: `sample_n` doesn't change the dtype
648                        ca.try_apply_amortized_same_type(|s| {
649                            let n = (s.as_ref().len() as f64 * fraction) as usize;
650                            s.as_ref().sample_n(n, with_replacement, shuffle, seed)
651                        })
652                    }
653                } else {
654                    Ok(ListChunked::full_null_with_dtype(
655                        ca.name().clone(),
656                        ca.len(),
657                        ca.inner_dtype(),
658                    ))
659                }
660            },
661            _ => ca.try_zip_and_apply_amortized(fraction, |opt_s, opt_n| match (opt_s, opt_n) {
662                (Some(s), Some(fraction)) => {
663                    let n = (s.as_ref().len() as f64 * fraction) as usize;
664                    s.as_ref()
665                        .sample_n(n, with_replacement, shuffle, seed)
666                        .map(Some)
667                },
668                _ => Ok(None),
669            }),
670        };
671        out.map(|ok| self.same_type(ok))
672    }
673
674    fn lst_concat(&self, other: &[Column]) -> PolarsResult<ListChunked> {
675        let ca = self.as_list();
676        let other_len = other.len();
677        let length = ca.len();
678        let mut other = other.to_vec();
679        let mut inner_super_type = ca.inner_dtype().clone();
680
681        for s in &other {
682            match s.dtype() {
683                DataType::List(inner_type) => {
684                    inner_super_type = try_get_supertype(&inner_super_type, inner_type)?;
685                },
686                dt => {
687                    inner_super_type = try_get_supertype(&inner_super_type, dt)?;
688                },
689            }
690        }
691
692        // cast lhs
693        let dtype = &DataType::List(Box::new(inner_super_type.clone()));
694        let ca = ca.cast(dtype)?;
695        let ca = ca.list().unwrap();
696
697        // broadcasting path in case all unit length
698        // this path will not expand the series, so saves memory
699        let out = if other.iter().all(|s| s.len() == 1) && ca.len() != 1 {
700            cast_rhs(&mut other, &inner_super_type, dtype, length, false)?;
701            let to_append = other
702                .iter()
703                .filter_map(|s| {
704                    let lst = s.list().unwrap();
705                    // SAFETY: previous rhs_cast ensures the type is correct
706                    unsafe {
707                        lst.get_as_series(0)
708                            .map(|s| s.from_physical_unchecked(&inner_super_type).unwrap())
709                    }
710                })
711                .collect::<Vec<_>>();
712
713            // there was a None, so all values will be None
714            if to_append.len() != other_len {
715                return Ok(ListChunked::full_null_with_dtype(
716                    ca.name().clone(),
717                    length,
718                    &inner_super_type,
719                ));
720            }
721
722            let vals_size_other = other
723                .iter()
724                .map(|s| s.list().unwrap().get_values_size())
725                .sum::<usize>();
726
727            let mut builder = get_list_builder(
728                &inner_super_type,
729                ca.get_values_size() + vals_size_other + 1,
730                length,
731                ca.name().clone(),
732            );
733            ca.series_iter().for_each(|opt_s| {
734                let opt_s = opt_s.map(|mut s| {
735                    for append in &to_append {
736                        s.append(append).unwrap();
737                    }
738                    match inner_super_type {
739                        // structs don't have chunks, so we must first rechunk the underlying series
740                        #[cfg(feature = "dtype-struct")]
741                        DataType::Struct(_) => s = s.rechunk(),
742                        // nothing
743                        _ => {},
744                    }
745                    s
746                });
747                builder.append_opt_series(opt_s.as_ref()).unwrap();
748            });
749            builder.finish()
750        } else {
751            // normal path which may contain same length list or unit length lists
752            cast_rhs(&mut other, &inner_super_type, dtype, length, true)?;
753
754            let vals_size_other = other
755                .iter()
756                .map(|s| s.list().unwrap().get_values_size())
757                .sum::<usize>();
758            let mut iters = Vec::with_capacity(other_len + 1);
759
760            for s in other.iter_mut() {
761                iters.push(s.list()?.amortized_iter())
762            }
763            let mut first_iter = ca.series_iter();
764            let mut builder = get_list_builder(
765                &inner_super_type,
766                ca.get_values_size() + vals_size_other + 1,
767                length,
768                ca.name().clone(),
769            );
770
771            for _ in 0..ca.len() {
772                let mut acc = match first_iter.next().unwrap() {
773                    Some(s) => s,
774                    None => {
775                        builder.append_null();
776                        // make sure that the iterators advance before we continue
777                        for it in &mut iters {
778                            it.next().unwrap();
779                        }
780                        continue;
781                    },
782                };
783
784                let mut has_nulls = false;
785                for it in &mut iters {
786                    match it.next().unwrap() {
787                        Some(s) => {
788                            if !has_nulls {
789                                acc.append(s.as_ref())?;
790                            }
791                        },
792                        None => {
793                            has_nulls = true;
794                        },
795                    }
796                }
797                if has_nulls {
798                    builder.append_null();
799                    continue;
800                }
801
802                match inner_super_type {
803                    // structs don't have chunks, so we must first rechunk the underlying series
804                    #[cfg(feature = "dtype-struct")]
805                    DataType::Struct(_) => acc = acc.rechunk(),
806                    // nothing
807                    _ => {},
808                }
809                builder.append_series(&acc).unwrap();
810            }
811            builder.finish()
812        };
813        Ok(out)
814    }
815}
816
817impl ListNameSpaceImpl for ListChunked {}
818
819#[cfg(feature = "list_gather")]
820fn take_series(s: &Series, idx: Series, null_on_oob: bool) -> PolarsResult<Series> {
821    let len = s.len();
822    let idx = convert_and_bound_index(&idx, len, null_on_oob)?;
823    s.take(&idx)
824}
825
826pub fn slice_broadcast_list(
827    single_list: Option<Series>,
828    offsets: &Int64Chunked,
829    lengths: &Int64Chunked,
830    target_len: usize,
831    name: PlSmallStr,
832    inner_dtype: &DataType,
833) -> ListChunked {
834    debug_assert!(target_len == offsets.len().max(lengths.len()));
835
836    let Some(single_list) = single_list else {
837        return ListChunked::full_null_with_dtype(name, target_len, inner_dtype);
838    };
839
840    let iter = (0..target_len).map(|index| {
841        let opt_offset = offsets.get(if offsets.len() == 1 { 0 } else { index });
842        let opt_length = lengths.get(if lengths.len() == 1 { 0 } else { index });
843        match (opt_offset, opt_length) {
844            (Some(offset), Some(length)) => Some(single_list.slice(offset, length as usize)),
845            _ => None,
846        }
847    });
848
849    let mut out: ListChunked = iter.collect_trusted();
850    out.rename(name);
851    out
852}
853
854fn shift_broadcast_list(
855    single_list: Option<Series>,
856    periods: &Int64Chunked,
857    target_len: usize,
858    name: PlSmallStr,
859    inner_dtype: &DataType,
860) -> ListChunked {
861    debug_assert!(target_len == periods.len());
862
863    let Some(single_list) = single_list else {
864        return ListChunked::full_null_with_dtype(name, target_len, inner_dtype);
865    };
866
867    let iter = (0..target_len).map(|index| {
868        let opt_period = periods.get(index);
869        opt_period.map(|period| single_list.shift(period))
870    });
871
872    let mut out: ListChunked = iter.collect_trusted();
873    out.rename(name);
874    out
875}