1use std::borrow::Cow;
2
3use polars_arrow::bitmap::{Bitmap, BitmapBuilder};
4use polars_arrow::compute::utils::{combine_validities_and, combine_validities_and_not};
5use polars_compute::if_then_else::{IfThenElseKernel, if_then_else_validity};
6use polars_error::PolarsContext;
7use polars_utils::broadcast::broadcast_len;
8
9#[cfg(feature = "object")]
10use crate::chunked_array::object::ObjectArray;
11use crate::prelude::*;
12use crate::utils::{align_chunks_binary, align_chunks_ternary};
13
14const SHAPE_MISMATCH_STR: &str =
15 "shapes of `self`, `mask` and `other` are not suitable for `zip_with` operation";
16
17fn if_then_else_broadcast_mask<T: PolarsDataType>(
18 mask: bool,
19 if_true: &ChunkedArray<T>,
20 if_false: &ChunkedArray<T>,
21) -> PolarsResult<ChunkedArray<T>>
22where
23 ChunkedArray<T>: ChunkExpandAtIndex<T>,
24{
25 let src = if mask { if_true } else { if_false };
26 let other = if mask { if_false } else { if_true };
27 let len = broadcast_len([src.len(), other.len()]).context(SHAPE_MISMATCH_STR)?;
28 let ret = src.broadcast_to(len)?.into_owned();
29 Ok(ret.with_name(if_true.name().clone()))
30}
31
32fn bool_null_to_false(mask: &BooleanArray) -> Bitmap {
33 if mask.null_count() == 0 {
34 mask.values().clone()
35 } else {
36 mask.values() & mask.validity().unwrap()
37 }
38}
39
40fn combine_validities_chunked<
44 T: PolarsDataType,
45 F: Fn(Option<&Bitmap>, Option<&Bitmap>) -> Option<Bitmap>,
46>(
47 ca: &ChunkedArray<T>,
48 mask: &BooleanChunked,
49 combiner: F,
50) -> ChunkedArray<T> {
51 let (ca_al, mask_al) = align_chunks_binary(ca, mask);
52 let chunks = ca_al
53 .downcast_iter()
54 .zip(mask_al.downcast_iter())
55 .map(|(a, m)| {
56 let bm = bool_null_to_false(m);
57 let validity = combiner(a.validity(), Some(&bm));
58 a.clone().with_validity_typed(validity)
59 });
60 ChunkedArray::from_chunk_iter_like(ca, chunks)
61}
62
63impl<T> ChunkZip<T> for ChunkedArray<T>
64where
65 T: PolarsDataType<IsStruct = FalseT>,
66 T::Array: for<'a> IfThenElseKernel<Scalar<'a> = T::Physical<'a>>,
67 ChunkedArray<T>: ChunkExpandAtIndex<T>,
68{
69 fn zip_with(
70 &self,
71 mask: &BooleanChunked,
72 other: &ChunkedArray<T>,
73 ) -> PolarsResult<ChunkedArray<T>> {
74 let if_true = self;
75 let if_false = other;
76
77 if mask.len() == 1 {
79 return if_then_else_broadcast_mask(mask.get(0).unwrap_or(false), if_true, if_false);
80 }
81
82 let ret = if if_true.len() == 1 && if_false.len() == 1 {
84 match (if_true.get(0), if_false.get(0)) {
85 (None, None) => ChunkedArray::full_null_like(if_true, mask.len()),
86 (None, Some(_)) => combine_validities_chunked(
87 &if_false.new_from_index(0, mask.len()),
88 mask,
89 combine_validities_and_not,
90 ),
91 (Some(_), None) => combine_validities_chunked(
92 &if_true.new_from_index(0, mask.len()),
93 mask,
94 combine_validities_and,
95 ),
96 (Some(t), Some(f)) => {
97 let dtype = if_true.downcast_iter().next().unwrap().dtype();
98 let chunks = mask.downcast_iter().map(|m| {
99 let bm = bool_null_to_false(m);
100 let t = t.clone();
101 let f = f.clone();
102 IfThenElseKernel::if_then_else_broadcast_both(dtype.clone(), &bm, t, f)
103 });
104 ChunkedArray::from_chunk_iter_like(if_true, chunks)
105 },
106 }
107
108 } else if if_true.len() == if_false.len() {
110 polars_ensure!(mask.len() == if_true.len(), ShapeMismatch: SHAPE_MISMATCH_STR);
111 let (mask_al, if_true_al, if_false_al) = align_chunks_ternary(mask, if_true, if_false);
112 let chunks = mask_al
113 .downcast_iter()
114 .zip(if_true_al.downcast_iter())
115 .zip(if_false_al.downcast_iter())
116 .map(|((m, t), f)| IfThenElseKernel::if_then_else(&bool_null_to_false(m), t, f));
117 ChunkedArray::from_chunk_iter_like(if_true, chunks)
118
119 } else if if_true.len() == 1 {
121 polars_ensure!(mask.len() == if_false.len(), ShapeMismatch: SHAPE_MISMATCH_STR);
122 if let Some(true_scalar) = if_true.get(0) {
123 let (mask_al, if_false_al) = align_chunks_binary(mask, if_false);
124 let chunks = mask_al
125 .downcast_iter()
126 .zip(if_false_al.downcast_iter())
127 .map(|(m, f)| {
128 let bm = bool_null_to_false(m);
129 let t = true_scalar.clone();
130 IfThenElseKernel::if_then_else_broadcast_true(&bm, t, f)
131 });
132 ChunkedArray::from_chunk_iter_like(if_true, chunks)
133 } else {
134 combine_validities_chunked(if_false, mask, combine_validities_and_not)
135 }
136
137 } else if if_false.len() == 1 {
139 polars_ensure!(mask.len() == if_true.len(), ShapeMismatch: SHAPE_MISMATCH_STR);
140 if let Some(false_scalar) = if_false.get(0) {
141 let (mask_al, if_true_al) = align_chunks_binary(mask, if_true);
142 let chunks =
143 mask_al
144 .downcast_iter()
145 .zip(if_true_al.downcast_iter())
146 .map(|(m, t)| {
147 let bm = bool_null_to_false(m);
148 let f = false_scalar.clone();
149 IfThenElseKernel::if_then_else_broadcast_false(&bm, t, f)
150 });
151 ChunkedArray::from_chunk_iter_like(if_false, chunks)
152 } else {
153 combine_validities_chunked(if_true, mask, combine_validities_and)
154 }
155 } else {
156 polars_bail!(ShapeMismatch: SHAPE_MISMATCH_STR)
157 };
158
159 Ok(ret.with_name(if_true.name().clone()))
160 }
161}
162
163#[cfg(feature = "object")]
165impl<T: PolarsObject> IfThenElseKernel for ObjectArray<T> {
166 type Scalar<'a> = &'a T;
167
168 fn if_then_else(mask: &Bitmap, if_true: &Self, if_false: &Self) -> Self {
169 mask.iter()
170 .zip(if_true.iter())
171 .zip(if_false.iter())
172 .map(|((m, t), f)| if m { t } else { f })
173 .collect_arr()
174 }
175
176 fn if_then_else_broadcast_true(
177 mask: &Bitmap,
178 if_true: Self::Scalar<'_>,
179 if_false: &Self,
180 ) -> Self {
181 mask.iter()
182 .zip(if_false.iter())
183 .map(|(m, f)| if m { Some(if_true) } else { f })
184 .collect_arr()
185 }
186
187 fn if_then_else_broadcast_false(
188 mask: &Bitmap,
189 if_true: &Self,
190 if_false: Self::Scalar<'_>,
191 ) -> Self {
192 mask.iter()
193 .zip(if_true.iter())
194 .map(|(m, t)| if m { t } else { Some(if_false) })
195 .collect_arr()
196 }
197
198 fn if_then_else_broadcast_both(
199 _dtype: ArrowDataType,
200 mask: &Bitmap,
201 if_true: Self::Scalar<'_>,
202 if_false: Self::Scalar<'_>,
203 ) -> Self {
204 mask.iter()
205 .map(|m| if m { if_true } else { if_false })
206 .collect_arr()
207 }
208}
209
210#[cfg(feature = "dtype-struct")]
211impl ChunkZip<StructType> for StructChunked {
212 fn zip_with(
213 &self,
214 mask: &BooleanChunked,
215 other: &ChunkedArray<StructType>,
216 ) -> PolarsResult<ChunkedArray<StructType>> {
217 let min_length = self.length.min(mask.length).min(other.length);
218 let max_length = self.length.max(mask.length).max(other.length);
219
220 let length = if min_length == 0 { 0 } else { max_length };
221
222 debug_assert!(self.length == 1 || self.length == length);
223 debug_assert!(mask.length == 1 || mask.length == length);
224 debug_assert!(other.length == 1 || other.length == length);
225
226 let mut if_true: Cow<ChunkedArray<StructType>> = Cow::Borrowed(self);
227 let mut if_false: Cow<ChunkedArray<StructType>> = Cow::Borrowed(other);
228
229 if mask.length == 1 {
232 let is_true = mask.get(0).unwrap_or(false);
234 return Ok(if is_true {
235 self.broadcast_to(length)?.into_owned()
236 } else {
237 other
238 .broadcast_to(length)?
239 .into_owned()
240 .with_name(self.name().clone())
241 });
242 }
243
244 let needs_broadcast =
250 if_true.chunks().len() > 1 || if_false.chunks().len() > 1 || mask.chunks().len() > 1;
251 if needs_broadcast && length > 1 {
252 if_true = self.broadcast_to(length)?;
253 if_false = other.broadcast_to(length)?;
254 }
255
256 let if_true = if_true.as_ref();
257 let if_false = if_false.as_ref();
258
259 let (if_true, if_false, mask) = align_chunks_ternary(if_true, if_false, mask);
260
261 let mut mask = mask.into_owned();
266 unsafe {
267 for arr in mask.downcast_iter_mut() {
268 let bm = bool_null_to_false(arr);
269 *arr = BooleanArray::from_data_default(bm, None);
270 }
271 mask.set_null_count(0);
272 }
273
274 let fields = if_true
276 .fields_as_series()
277 .iter()
278 .zip(if_false.fields_as_series())
279 .map(|(lhs, rhs)| lhs.zip_with_same_type(&mask, &rhs))
280 .collect::<PolarsResult<Vec<_>>>()?;
281
282 let mut out = StructChunked::from_series(self.name().clone(), length, fields.iter())?;
283
284 fn rechunk_bitmaps(
285 total_length: usize,
286 iter: impl Iterator<Item = (usize, Option<Bitmap>)>,
287 ) -> Option<Bitmap> {
288 let mut rechunked_length = 0;
289 let mut rechunked_validity = None;
290 for (chunk_length, validity) in iter {
291 if let Some(validity) = validity {
292 if validity.unset_bits() > 0 {
293 let v = rechunked_validity.get_or_insert_with(|| {
294 let mut bm = BitmapBuilder::with_capacity(total_length);
295 bm.extend_constant(rechunked_length, true);
296 bm
297 });
298 v.extend_constant(rechunked_length - v.len(), true);
299 v.extend_from_bitmap(&validity);
300 }
301 }
302
303 rechunked_length += chunk_length;
304 }
305
306 if let Some(rechunked_validity) = rechunked_validity.as_mut() {
307 rechunked_validity.extend_constant(total_length - rechunked_validity.len(), true);
308 }
309
310 rechunked_validity.map(BitmapBuilder::freeze)
311 }
312
313 if (if_true.null_count + if_false.null_count) > 0 {
319 let rechunked_validity = match (if_true.len(), if_false.len()) {
321 (1, 1) if length != 1 => {
322 match (if_true.null_count() == 0, if_false.null_count() == 0) {
323 (true, true) => None,
324 (false, true) => {
325 if mask.chunks().len() == 1 {
326 let m = mask.chunks()[0]
327 .as_any()
328 .downcast_ref::<BooleanArray>()
329 .unwrap()
330 .values();
331 Some(!m)
332 } else {
333 rechunk_bitmaps(
334 length,
335 mask.downcast_iter()
336 .map(|m| (m.len(), Some(m.values().clone()))),
337 )
338 }
339 },
340 (true, false) => {
341 if mask.chunks().len() == 1 {
342 let m = mask.chunks()[0]
343 .as_any()
344 .downcast_ref::<BooleanArray>()
345 .unwrap()
346 .values();
347 Some(m.clone())
348 } else {
349 rechunk_bitmaps(
350 length,
351 mask.downcast_iter().map(|m| (m.len(), Some(!m.values()))),
352 )
353 }
354 },
355 (false, false) => Some(Bitmap::new_zeroed(length)),
356 }
357 },
358 (1, _) if length != 1 => {
359 debug_assert!(
360 if_false
361 .chunk_lengths()
362 .zip(mask.chunk_lengths())
363 .all(|(r, m)| r == m)
364 );
365
366 let combine = if if_true.null_count() == 0 {
367 |if_false: Option<&Bitmap>, m: &Bitmap| {
368 if_false.map(|v| polars_arrow::bitmap::or(v, m))
369 }
370 } else {
371 |if_false: Option<&Bitmap>, m: &Bitmap| {
372 Some(
373 if_false
374 .map_or_else(|| !m, |v| polars_arrow::bitmap::and_not(v, m)),
375 )
376 }
377 };
378
379 if if_false.chunks().len() == 1 {
380 let if_false = if_false.chunks()[0].validity();
381 let m = mask.chunks()[0]
382 .as_any()
383 .downcast_ref::<BooleanArray>()
384 .unwrap()
385 .values();
386
387 let validity = combine(if_false, m);
388 validity.filter(|v| v.unset_bits() > 0)
389 } else {
390 rechunk_bitmaps(
391 length,
392 if_false.chunks().iter().zip(mask.downcast_iter()).map(
393 |(chunk, mask)| {
394 (mask.len(), combine(chunk.validity(), mask.values()))
395 },
396 ),
397 )
398 }
399 },
400 (_, 1) if length != 1 => {
401 debug_assert!(
402 if_true
403 .chunk_lengths()
404 .zip(mask.chunk_lengths())
405 .all(|(l, m)| l == m)
406 );
407
408 let combine = if if_false.null_count() == 0 {
409 |if_true: Option<&Bitmap>, m: &Bitmap| {
410 if_true.map(|v| polars_arrow::bitmap::or_not(v, m))
411 }
412 } else {
413 |if_true: Option<&Bitmap>, m: &Bitmap| {
414 Some(
415 if_true
416 .map_or_else(|| m.clone(), |v| polars_arrow::bitmap::and(v, m)),
417 )
418 }
419 };
420
421 if if_true.chunks().len() == 1 {
422 let if_true = if_true.chunks()[0].validity();
423 let m = mask.chunks()[0]
424 .as_any()
425 .downcast_ref::<BooleanArray>()
426 .unwrap()
427 .values();
428
429 let validity = combine(if_true, m);
430 validity.filter(|v| v.unset_bits() > 0)
431 } else {
432 rechunk_bitmaps(
433 length,
434 if_true.chunks().iter().zip(mask.downcast_iter()).map(
435 |(chunk, mask)| {
436 (mask.len(), combine(chunk.validity(), mask.values()))
437 },
438 ),
439 )
440 }
441 },
442 (_, _) => {
443 debug_assert!(
444 if_true
445 .chunk_lengths()
446 .zip(if_false.chunk_lengths())
447 .all(|(l, r)| l == r)
448 );
449 debug_assert!(
450 if_true
451 .chunk_lengths()
452 .zip(mask.chunk_lengths())
453 .all(|(l, r)| l == r)
454 );
455
456 let validities = if_true
457 .chunks()
458 .iter()
459 .zip(if_false.chunks())
460 .map(|(l, r)| (l.validity(), r.validity()));
461
462 rechunk_bitmaps(
463 length,
464 validities
465 .zip(mask.downcast_iter())
466 .map(|((if_true, if_false), mask)| {
467 (
468 mask.len(),
469 if_then_else_validity(mask.values(), if_true, if_false),
470 )
471 }),
472 )
473 },
474 };
475
476 if let Some(mut rechunked_validity) = rechunked_validity {
478 assert_eq!(rechunked_validity.len(), out.len());
479
480 let num_chunks = out.chunks().len();
481 let null_count = rechunked_validity.unset_bits();
482
483 let chunks = unsafe { out.chunks_mut() };
486
487 if num_chunks == 1 {
488 chunks[0] = chunks[0].with_validity(Some(rechunked_validity));
489 } else {
490 for chunk in chunks {
491 let chunk_len = chunk.len();
492 let chunk_validity;
493
494 (chunk_validity, rechunked_validity) =
496 unsafe { rechunked_validity.split_at_unchecked(chunk_len) };
497 *chunk = chunk.with_validity(
498 (chunk_validity.unset_bits() > 0).then_some(chunk_validity),
499 );
500 }
501 }
502
503 out.null_count = null_count;
504 } else {
505 let chunks = unsafe { out.chunks_mut() };
508
509 for chunk in chunks {
510 *chunk = chunk.with_validity(None);
511 }
512
513 out.null_count = 0;
514 }
515 }
516
517 if cfg!(debug_assertions) {
518 let start_length = out.len();
519 let start_null_count = out.null_count();
520
521 out.compute_len();
522
523 assert_eq!(start_length, out.len());
524 assert_eq!(start_null_count, out.null_count());
525 }
526 Ok(out)
527 }
528}