1use std::borrow::Cow;
2
3use arrow::bitmap::{Bitmap, BitmapBuilder};
4use 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| arrow::bitmap::or(v, m))
369 }
370 } else {
371 |if_false: Option<&Bitmap>, m: &Bitmap| {
372 Some(if_false.map_or_else(|| !m, |v| arrow::bitmap::and_not(v, m)))
373 }
374 };
375
376 if if_false.chunks().len() == 1 {
377 let if_false = if_false.chunks()[0].validity();
378 let m = mask.chunks()[0]
379 .as_any()
380 .downcast_ref::<BooleanArray>()
381 .unwrap()
382 .values();
383
384 let validity = combine(if_false, m);
385 validity.filter(|v| v.unset_bits() > 0)
386 } else {
387 rechunk_bitmaps(
388 length,
389 if_false.chunks().iter().zip(mask.downcast_iter()).map(
390 |(chunk, mask)| {
391 (mask.len(), combine(chunk.validity(), mask.values()))
392 },
393 ),
394 )
395 }
396 },
397 (_, 1) if length != 1 => {
398 debug_assert!(
399 if_true
400 .chunk_lengths()
401 .zip(mask.chunk_lengths())
402 .all(|(l, m)| l == m)
403 );
404
405 let combine = if if_false.null_count() == 0 {
406 |if_true: Option<&Bitmap>, m: &Bitmap| {
407 if_true.map(|v| arrow::bitmap::or_not(v, m))
408 }
409 } else {
410 |if_true: Option<&Bitmap>, m: &Bitmap| {
411 Some(if_true.map_or_else(|| m.clone(), |v| arrow::bitmap::and(v, m)))
412 }
413 };
414
415 if if_true.chunks().len() == 1 {
416 let if_true = if_true.chunks()[0].validity();
417 let m = mask.chunks()[0]
418 .as_any()
419 .downcast_ref::<BooleanArray>()
420 .unwrap()
421 .values();
422
423 let validity = combine(if_true, m);
424 validity.filter(|v| v.unset_bits() > 0)
425 } else {
426 rechunk_bitmaps(
427 length,
428 if_true.chunks().iter().zip(mask.downcast_iter()).map(
429 |(chunk, mask)| {
430 (mask.len(), combine(chunk.validity(), mask.values()))
431 },
432 ),
433 )
434 }
435 },
436 (_, _) => {
437 debug_assert!(
438 if_true
439 .chunk_lengths()
440 .zip(if_false.chunk_lengths())
441 .all(|(l, r)| l == r)
442 );
443 debug_assert!(
444 if_true
445 .chunk_lengths()
446 .zip(mask.chunk_lengths())
447 .all(|(l, r)| l == r)
448 );
449
450 let validities = if_true
451 .chunks()
452 .iter()
453 .zip(if_false.chunks())
454 .map(|(l, r)| (l.validity(), r.validity()));
455
456 rechunk_bitmaps(
457 length,
458 validities
459 .zip(mask.downcast_iter())
460 .map(|((if_true, if_false), mask)| {
461 (
462 mask.len(),
463 if_then_else_validity(mask.values(), if_true, if_false),
464 )
465 }),
466 )
467 },
468 };
469
470 if let Some(mut rechunked_validity) = rechunked_validity {
472 assert_eq!(rechunked_validity.len(), out.len());
473
474 let num_chunks = out.chunks().len();
475 let null_count = rechunked_validity.unset_bits();
476
477 let chunks = unsafe { out.chunks_mut() };
480
481 if num_chunks == 1 {
482 chunks[0] = chunks[0].with_validity(Some(rechunked_validity));
483 } else {
484 for chunk in chunks {
485 let chunk_len = chunk.len();
486 let chunk_validity;
487
488 (chunk_validity, rechunked_validity) =
490 unsafe { rechunked_validity.split_at_unchecked(chunk_len) };
491 *chunk = chunk.with_validity(
492 (chunk_validity.unset_bits() > 0).then_some(chunk_validity),
493 );
494 }
495 }
496
497 out.null_count = null_count;
498 } else {
499 let chunks = unsafe { out.chunks_mut() };
502
503 for chunk in chunks {
504 *chunk = chunk.with_validity(None);
505 }
506
507 out.null_count = 0;
508 }
509 }
510
511 if cfg!(debug_assertions) {
512 let start_length = out.len();
513 let start_null_count = out.null_count();
514
515 out.compute_len();
516
517 assert_eq!(start_length, out.len());
518 assert_eq!(start_null_count, out.null_count());
519 }
520 Ok(out)
521 }
522}