1use polars_arrow::bitmap::and_not;
2use polars_arrow::bitmap::bitmask::BitMask;
3
4use super::*;
5use crate::chunked_array::cast::CastOptions;
6use crate::chunked_array::from_iterator_par::{collect_bool_opt_par, collect_bool_par};
7
8pub fn _agg_helper_idx_bool<F>(groups: &GroupsIdx, f: F) -> Series
9where
10 F: Fn((IdxSize, &IdxVec)) -> Option<bool> + Send + Sync,
11{
12 let ca: BooleanChunked = RAYON.install(|| {
13 let groups_len = groups.len();
14 let first = groups.first();
15 let all = groups.all();
16 collect_bool_opt_par(groups_len, |g| f((first[g], &all[g])))
17 });
18 ca.into_series()
19}
20
21pub fn _agg_helper_slice_bool<F>(groups: &[[IdxSize; 2]], f: F) -> Series
22where
23 F: Fn([IdxSize; 2]) -> Option<bool> + Send + Sync,
24{
25 let ca: BooleanChunked = RAYON.install(|| collect_bool_opt_par(groups.len(), |g| f(groups[g])));
26 ca.into_series()
27}
28
29unsafe fn arg_first_in_slices(arr: &BooleanArray, groups: &[[IdxSize; 2]], target: bool) -> Series {
35 if arr.null_count() == arr.len() {
36 return IdxCa::full_null(PlSmallStr::EMPTY, groups.len()).into_series();
37 }
38 let valid_targets;
40 let (targets, find_false) = match arr.validity() {
41 None => (BitMask::from_bitmap(arr.values()), !target),
42 Some(validity) => {
43 valid_targets = if target {
44 arr.values() & validity
45 } else {
46 and_not(validity, arr.values())
47 };
48 (BitMask::from_bitmap(&valid_targets), false)
49 },
50 };
51 let validity = arr.validity().map(BitMask::from_bitmap);
52 _agg_helper_slice::<IdxType, _>(groups, |[first, len]| {
54 let (first, len) = (first as usize, len as usize);
55 if len == 1 {
56 return validity.is_none_or(|v| v.get(first)).then_some(0);
57 }
58 let window = unsafe { targets.sliced_unchecked(first, len) };
59 let i = if find_false {
60 window.leading_ones()
61 } else {
62 window.leading_zeros()
63 };
64 if i < len {
65 return Some(i as IdxSize);
66 }
67 let i = validity.map_or(0, |v| {
68 unsafe { v.sliced_unchecked(first, len) }.leading_zeros()
69 });
70 (i < len).then_some(i as IdxSize)
71 })
72}
73
74#[cfg(feature = "bitwise")]
75impl BooleanChunked {
76 pub(crate) unsafe fn agg_and(&self, groups: &GroupsType) -> BooleanChunked {
77 self.agg_all(groups, true)
78 }
79
80 pub(crate) unsafe fn agg_or(&self, groups: &GroupsType) -> BooleanChunked {
81 self.agg_any(groups, true)
82 }
83
84 pub(crate) unsafe fn agg_xor(&self, groups: &GroupsType) -> BooleanChunked {
85 self.bool_agg(
86 groups,
87 true,
88 |values, idxs| {
89 idxs.iter()
90 .map(|i| {
91 <IdxSize as From<bool>>::from(unsafe {
92 values.get_bit_unchecked(*i as usize)
93 })
94 })
95 .sum::<IdxSize>()
96 % 2
97 == 1
98 },
99 |values, validity, idxs| {
100 idxs.iter()
101 .map(|i| {
102 <IdxSize as From<bool>>::from(unsafe {
103 validity.get_bit_unchecked(*i as usize)
104 & values.get_bit_unchecked(*i as usize)
105 })
106 })
107 .sum::<IdxSize>()
108 % 2
109 == 1
110 },
111 |_, _, _| unreachable!(),
112 |values, start, length| {
113 unsafe { values.sliced_unchecked(start as usize, length as usize) }.set_bits() % 2
114 == 1
115 },
116 |values, validity, start, length| {
117 let values = unsafe { values.sliced_unchecked(start as usize, length as usize) };
118 let validity =
119 unsafe { validity.sliced_unchecked(start as usize, length as usize) };
120 values.num_intersections_with(validity) % 2 == 1
121 },
122 |_, _, _, _| unreachable!(),
123 )
124 }
125}
126
127impl BooleanChunked {
128 pub(crate) unsafe fn agg_min(&self, groups: &GroupsType) -> Series {
129 if !self.has_nulls() || matches!(groups, GroupsType::Slice { .. }) {
131 match self.is_sorted_flag() {
132 IsSorted::Ascending => {
133 return self.clone().into_series().agg_first_non_null(groups);
134 },
135 IsSorted::Descending => {
136 return self.clone().into_series().agg_last_non_null(groups);
137 },
138 _ => {},
139 }
140 }
141 let ca_self = self.rechunk();
142 let arr = ca_self.downcast_iter().next().unwrap();
143 let no_nulls = arr.null_count() == 0;
144 match groups {
145 GroupsType::Idx(groups) => _agg_helper_idx_bool(groups, |(first, idx)| {
146 debug_assert!(idx.len() <= self.len());
147 if idx.is_empty() {
148 None
149 } else if idx.len() == 1 {
150 arr.get(first as usize)
151 } else if no_nulls {
152 take_arg_min_bool_iter_unchecked_no_nulls(arr, idx2usize(idx))
153 .map(|p| arr.value_unchecked(idx[p] as usize))
154 } else {
155 take_arg_min_bool_iter_unchecked_nulls(arr, idx2usize(idx))
156 .map(|p| arr.value_unchecked(idx[p] as usize))
157 }
158 }),
159 GroupsType::Slice {
160 groups: groups_slice,
161 ..
162 } => _agg_helper_slice_bool(groups_slice, |[first, len]| {
163 debug_assert!(len <= self.len() as IdxSize);
164 match len {
165 0 => None,
166 1 => self.get(first as usize),
167 _ => {
168 let arr_group = _slice_from_offsets(self, first, len);
169 arr_group.min()
170 },
171 }
172 }),
173 }
174 }
175 pub(crate) unsafe fn agg_max(&self, groups: &GroupsType) -> Series {
176 if !self.has_nulls() || matches!(groups, GroupsType::Slice { .. }) {
178 match self.is_sorted_flag() {
179 IsSorted::Ascending => return self.clone().into_series().agg_last_non_null(groups),
180 IsSorted::Descending => {
181 return self.clone().into_series().agg_first_non_null(groups);
182 },
183 _ => {},
184 }
185 }
186
187 let ca_self = self.rechunk();
188 let arr = ca_self.downcast_iter().next().unwrap();
189 let no_nulls = arr.null_count() == 0;
190 match groups {
191 GroupsType::Idx(groups) => _agg_helper_idx_bool(groups, |(first, idx)| {
192 debug_assert!(idx.len() <= self.len());
193 if idx.is_empty() {
194 None
195 } else if idx.len() == 1 {
196 self.get(first as usize)
197 } else if no_nulls {
198 take_arg_max_bool_iter_unchecked_no_nulls(arr, idx2usize(idx))
199 .map(|p| arr.value_unchecked(idx[p] as usize))
200 } else {
201 take_arg_max_bool_iter_unchecked_nulls(arr, idx2usize(idx))
202 .map(|p| arr.value_unchecked(idx[p] as usize))
203 }
204 }),
205 GroupsType::Slice {
206 groups: groups_slice,
207 ..
208 } => _agg_helper_slice_bool(groups_slice, |[first, len]| {
209 debug_assert!(len <= self.len() as IdxSize);
210 match len {
211 0 => None,
212 1 => self.get(first as usize),
213 _ => {
214 let arr_group = _slice_from_offsets(self, first, len);
215 arr_group.max()
216 },
217 }
218 }),
219 }
220 }
221
222 pub(crate) unsafe fn agg_arg_min(&self, groups: &GroupsType) -> Series {
223 if !self.has_nulls() || matches!(groups, GroupsType::Slice { .. }) {
225 match self.is_sorted_flag() {
226 IsSorted::Ascending => {
227 return self.clone().into_series().agg_arg_first_non_null(groups);
228 },
229 IsSorted::Descending => {
230 return self.clone().into_series().agg_arg_last_non_null(groups);
231 },
232 _ => {},
233 }
234 }
235
236 let ca_self = self.rechunk();
237 let arr = ca_self.downcast_iter().next().unwrap();
238 let no_nulls = arr.null_count() == 0;
239 match groups {
240 GroupsType::Idx(groups) => agg_helper_idx_on_all::<IdxType, _>(groups, |idx| {
241 debug_assert!(idx.len() <= ca_self.len());
242 if idx.is_empty() {
243 None
244 } else if idx.len() == 1 {
245 arr.get(idx[0] as usize).map(|_| 0)
246 } else if no_nulls {
247 take_arg_min_bool_iter_unchecked_no_nulls(arr, idx2usize(idx))
248 .map(|p| p as IdxSize)
249 } else {
250 take_arg_min_bool_iter_unchecked_nulls(arr, idx2usize(idx))
251 .map(|p| p as IdxSize)
252 }
253 }),
254 GroupsType::Slice {
255 groups: groups_slice,
256 ..
257 } => arg_first_in_slices(arr, groups_slice, false),
258 }
259 }
260
261 pub(crate) unsafe fn agg_arg_max(&self, groups: &GroupsType) -> Series {
262 if !self.has_nulls() || matches!(groups, GroupsType::Slice { .. }) {
264 match self.is_sorted_flag() {
265 IsSorted::Ascending => {
266 return self.clone().into_series().agg_arg_last_non_null(groups);
267 },
268 IsSorted::Descending => {
269 return self.clone().into_series().agg_arg_first_non_null(groups);
270 },
271 _ => {},
272 }
273 }
274
275 let ca_self = self.rechunk();
276 let arr = ca_self.downcast_iter().next().unwrap();
277 let no_nulls = arr.null_count() == 0;
278 match groups {
279 GroupsType::Idx(groups) => agg_helper_idx_on_all::<IdxType, _>(groups, |idx| {
280 debug_assert!(idx.len() <= ca_self.len());
281 if idx.is_empty() {
282 None
283 } else if idx.len() == 1 {
284 arr.get(idx[0] as usize).map(|_| 0)
285 } else if no_nulls {
286 take_arg_max_bool_iter_unchecked_no_nulls(arr, idx2usize(idx))
287 .map(|p| p as IdxSize)
288 } else {
289 take_arg_max_bool_iter_unchecked_nulls(arr, idx2usize(idx))
290 .map(|p| p as IdxSize)
291 }
292 }),
293 GroupsType::Slice {
294 groups: groups_slice,
295 ..
296 } => arg_first_in_slices(arr, groups_slice, true),
297 }
298 }
299
300 pub(crate) unsafe fn agg_sum(&self, groups: &GroupsType) -> Series {
301 self.cast_with_options(&IDX_DTYPE, CastOptions::Overflowing)
302 .unwrap()
303 .agg_sum(groups)
304 }
305
306 #[expect(clippy::too_many_arguments)]
310 unsafe fn bool_agg(
311 &self,
312 groups: &GroupsType,
313 ignore_nulls: bool,
314
315 idx_no_valid: impl Fn(BitMask, &[IdxSize]) -> bool + Send + Sync,
316 idx_validity: impl Fn(BitMask, BitMask, &[IdxSize]) -> bool + Send + Sync,
317 idx_kleene: impl Fn(BitMask, BitMask, &[IdxSize]) -> Option<bool> + Send + Sync,
318
319 slice_no_valid: impl Fn(BitMask, IdxSize, IdxSize) -> bool + Send + Sync,
320 slice_validity: impl Fn(BitMask, BitMask, IdxSize, IdxSize) -> bool + Send + Sync,
321 slice_kleene: impl Fn(BitMask, BitMask, IdxSize, IdxSize) -> Option<bool> + Send + Sync,
322 ) -> BooleanChunked {
323 let name = self.name().clone();
324 let values = self.rechunk();
325 let values = values.downcast_as_array();
326
327 let groups_len = groups.len();
328
329 let ca = RAYON.install(|| {
330 let validity = values
331 .validity()
332 .filter(|v| v.unset_bits() > 0)
333 .map(BitMask::from_bitmap);
334 let values = BitMask::from_bitmap(values.values());
335
336 if !ignore_nulls && let Some(validity) = validity {
337 match groups {
338 GroupsType::Idx(idx) => {
339 let all = idx.all();
340 collect_bool_opt_par(groups_len, |g| idx_kleene(values, validity, &all[g]))
341 },
342 GroupsType::Slice { groups, .. } => collect_bool_opt_par(groups_len, |g| {
343 let [s, l] = groups[g];
344 slice_kleene(values, validity, s, l)
345 }),
346 }
347 } else {
348 match groups {
349 GroupsType::Idx(idx) => {
350 let all = idx.all();
351 match validity {
352 None => collect_bool_par(groups_len, |g| idx_no_valid(values, &all[g])),
353 Some(validity) => collect_bool_par(groups_len, |g| {
354 idx_validity(values, validity, &all[g])
355 }),
356 }
357 },
358 GroupsType::Slice { groups, .. } => match validity {
359 None => collect_bool_par(groups_len, |g| {
360 let [s, l] = groups[g];
361 slice_no_valid(values, s, l)
362 }),
363 Some(validity) => collect_bool_par(groups_len, |g| {
364 let [s, l] = groups[g];
365 slice_validity(values, validity, s, l)
366 }),
367 },
368 }
369 }
370 });
371 ca.with_name(name)
372 }
373
374 pub unsafe fn agg_any(&self, groups: &GroupsType, ignore_nulls: bool) -> BooleanChunked {
378 self.bool_agg(
379 groups,
380 ignore_nulls,
381 |values, idxs| {
382 idxs.iter()
383 .any(|i| unsafe { values.get_bit_unchecked(*i as usize) })
384 },
385 |values, validity, idxs| {
386 idxs.iter().any(|i| unsafe {
387 validity.get_bit_unchecked(*i as usize) & values.get_bit_unchecked(*i as usize)
388 })
389 },
390 |values, validity, idxs| {
391 let mut saw_null = false;
392 for i in idxs.iter() {
393 let is_valid = unsafe { validity.get_bit_unchecked(*i as usize) };
394 let is_true = unsafe { values.get_bit_unchecked(*i as usize) };
395
396 if is_valid & is_true {
397 return Some(true);
398 }
399 saw_null |= !is_valid;
400 }
401 (!saw_null).then_some(false)
402 },
403 |values, start, length| {
404 unsafe { values.sliced_unchecked(start as usize, length as usize) }.leading_zeros()
405 < length as usize
406 },
407 |values, validity, start, length| {
408 let values = unsafe { values.sliced_unchecked(start as usize, length as usize) };
409 let validity =
410 unsafe { validity.sliced_unchecked(start as usize, length as usize) };
411 values.intersects_with(validity)
412 },
413 |values, validity, start, length| {
414 let values = unsafe { values.sliced_unchecked(start as usize, length as usize) };
415 let validity =
416 unsafe { validity.sliced_unchecked(start as usize, length as usize) };
417
418 if values.intersects_with(validity) {
419 Some(true)
420 } else if validity.unset_bits() == 0 {
421 Some(false)
422 } else {
423 None
424 }
425 },
426 )
427 }
428
429 pub unsafe fn agg_all(&self, groups: &GroupsType, ignore_nulls: bool) -> BooleanChunked {
433 self.bool_agg(
434 groups,
435 ignore_nulls,
436 |values, idxs| {
437 idxs.iter()
438 .all(|i| unsafe { values.get_bit_unchecked(*i as usize) })
439 },
440 |values, validity, idxs| {
441 idxs.iter().all(|i| unsafe {
442 !validity.get_bit_unchecked(*i as usize) | values.get_bit_unchecked(*i as usize)
443 })
444 },
445 |values, validity, idxs| {
446 let mut saw_null = false;
447 for i in idxs.iter() {
448 let is_valid = unsafe { validity.get_bit_unchecked(*i as usize) };
449 let is_true = unsafe { values.get_bit_unchecked(*i as usize) };
450
451 if is_valid & !is_true {
452 return Some(false);
453 }
454 saw_null |= !is_valid;
455 }
456 (!saw_null).then_some(true)
457 },
458 |values, start, length| {
459 let values = unsafe { values.sliced_unchecked(start as usize, length as usize) };
460 values.unset_bits() == 0
461 },
462 |values, validity, start, length| {
463 let values = unsafe { values.sliced_unchecked(start as usize, length as usize) };
464 let validity =
465 unsafe { validity.sliced_unchecked(start as usize, length as usize) };
466 values.num_intersections_with(validity) == validity.set_bits()
467 },
468 |values, validity, start, length| {
469 let values = unsafe { values.sliced_unchecked(start as usize, length as usize) };
470 let validity =
471 unsafe { validity.sliced_unchecked(start as usize, length as usize) };
472
473 let num_non_nulls = validity.set_bits();
474
475 if values.num_intersections_with(validity) < num_non_nulls {
476 Some(false)
477 } else if num_non_nulls < values.len() {
478 None
479 } else {
480 Some(true)
481 }
482 },
483 )
484 }
485}