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 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 *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 *s = s.new_from_index(0, length)
66 }
67 }
69 }
70 Ok(())
71}
72
73pub trait ListNameSpaceImpl: AsList {
74 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 let dtype = &DataType::List(Box::new(inner_super_type.clone()));
706 let ca = ca.cast(dtype)?;
707 let ca = ca.list().unwrap();
708
709 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 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 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 #[cfg(feature = "dtype-struct")]
753 DataType::Struct(_) => s = s.rechunk(),
754 _ => {},
756 }
757 s
758 });
759 builder.append_opt_series(opt_s.as_ref()).unwrap();
760 });
761 builder.finish()
762 } else {
763 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 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 #[cfg(feature = "dtype-struct")]
817 DataType::Struct(_) => acc = acc.rechunk(),
818 _ => {},
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}