1#[cfg(feature = "dtype-struct")]
2use polars_core::chunked_array::ops::row_encode::_get_rows_encoded_ca;
3use polars_core::prelude::arity::unary_elementwise_values;
4use polars_core::prelude::*;
5use polars_core::series::IsSorted;
6use polars_core::with_match_physical_numeric_polars_type;
7#[cfg(feature = "hash")]
8use polars_utils::aliases::PlSeedableRandomStateQuality;
9use polars_utils::total_ord::TotalOrd;
10
11use crate::series::ops::SeriesSealed;
12
13pub trait SeriesMethods: SeriesSealed {
14 fn value_counts(
17 &self,
18 sort: bool,
19 parallel: bool,
20 name: PlSmallStr,
21 normalize: bool,
22 ) -> PolarsResult<DataFrame> {
23 let s = self.as_series();
24 polars_ensure!(
25 s.name() != &name,
26 Duplicate: "using `value_counts` on a column/series named '{}' would lead to duplicate \
27 column names; change `name` to fix", name,
28 );
29 let groups = s.group_tuples(parallel, sort)?;
30 let values = unsafe { s.agg_first(&groups) }
31 .with_name(s.name().clone())
32 .into();
33 let counts = groups.group_count().with_name(name.clone());
34
35 let counts = if normalize {
36 let len = s.len() as f64;
37 let counts: Float64Chunked =
38 unary_elementwise_values(&counts, |count| count as f64 / len);
39 counts.into_column()
40 } else {
41 counts.into_column()
42 };
43
44 let height = counts.len();
45 let cols = vec![values, counts];
46 let df = unsafe { DataFrame::new_unchecked(height, cols) };
47 if sort {
48 df.sort(
49 [name],
50 SortMultipleOptions::default()
51 .with_order_descending(true)
52 .with_multithreaded(parallel),
53 )
54 } else {
55 Ok(df)
56 }
57 }
58
59 #[cfg(feature = "hash")]
60 fn hash(&self, build_hasher: PlSeedableRandomStateQuality) -> UInt64Chunked {
61 let s = self.as_series();
62 let mut h = vec![];
63 s.0.vec_hash(build_hasher, &mut h).unwrap();
64 UInt64Chunked::from_vec(s.name().clone(), h)
65 }
66
67 fn ensure_sorted_arg(&self, operation: &str) -> PolarsResult<()> {
68 polars_ensure!(
69 self.is_sorted(SortOptions::default())?,
70 InvalidOperation: "argument in operation '{}' is not sorted, please sort the 'expr/series/column' first",
71 operation
72 );
73 Ok(())
74 }
75
76 fn is_sorted(&self, options: SortOptions) -> PolarsResult<bool> {
80 is_sorted_impl(self.as_series(), options)
81 }
82
83 fn is_sorted_any(
84 &self,
85 descending: Option<bool>,
86 nulls_last: Option<bool>,
87 ) -> PolarsResult<bool> {
88 let s = self.as_series();
89 let (descending, nulls_last) = resolve_sort_options(s, descending, nulls_last)?;
90 let options = SortOptions {
93 descending: descending.unwrap_or(false),
94 nulls_last: nulls_last.unwrap_or(false),
95 ..Default::default()
96 };
97 is_sorted_impl(s, options)
98 }
99}
100
101fn is_sorted_impl(s: &Series, options: SortOptions) -> PolarsResult<bool> {
102 let null_count = s.null_count();
103
104 if (options.descending
105 && (options.nulls_last || null_count == 0)
106 && matches!(s.is_sorted_flag(), IsSorted::Descending))
107 || (!options.descending
108 && (!options.nulls_last || null_count == 0)
109 && matches!(s.is_sorted_flag(), IsSorted::Ascending))
110 {
111 return Ok(true);
112 }
113
114 #[cfg(feature = "dtype-struct")]
115 if matches!(s.dtype(), DataType::Struct(_)) {
116 let encoded = _get_rows_encoded_ca(
117 PlSmallStr::EMPTY,
118 &[s.clone().into()],
119 &[options.descending],
120 &[options.nulls_last],
121 false,
122 )?;
123 let options = SortOptions {
124 descending: false,
125 nulls_last: false,
126 ..options
127 };
128 return is_sorted_impl(&encoded.into_series(), options);
129 }
130
131 let s_len = s.len();
132 if null_count == s_len {
133 return Ok(true);
135 }
136 if null_count > 0 {
138 if options.nulls_last {
139 if s.slice((s_len - null_count) as i64, null_count)
140 .null_count()
141 != null_count
142 {
143 return Ok(false);
144 }
145 } else if s.slice(0, null_count).null_count() != null_count {
146 return Ok(false);
147 }
148 }
149
150 if s.dtype().is_primitive_numeric() {
151 with_match_physical_numeric_polars_type!(s.dtype(), |$T| {
152 let ca: &ChunkedArray<$T> = s.as_ref().as_ref().as_ref();
153 return Ok(is_sorted_ca_num::<$T>(ca, options))
154 })
155 }
156
157 let non_null_len = s_len - null_count;
167 if non_null_len <= 1 {
168 return Ok(true);
169 }
170
171 let offset = (!options.nulls_last as i64) * (null_count as i64);
172 let non_null = s.slice(offset, non_null_len);
173 debug_assert_eq!(
174 non_null.null_count(),
175 0,
176 "internal error: `is_sorted` non-null slice contains nulls"
177 );
178
179 #[cfg(feature = "dtype-categorical")]
180 if matches!(non_null.dtype(), DataType::Categorical(_, _)) {
181 return is_sorted_categorical_lexical_adjacent(&non_null, options);
182 }
183
184 let phys = non_null.to_physical_repr();
185 let s_phys = phys.as_ref();
186 if s_phys.dtype().is_primitive_numeric() {
187 with_match_physical_numeric_polars_type!(s_phys.dtype(), |$T| {
188 let ca: &ChunkedArray<$T> = s_phys.as_ref().as_ref().as_ref();
189 return Ok(is_sorted_ca_num::<$T>(ca, options))
190 })
191 }
192
193 match s_phys.dtype() {
194 DataType::Boolean => {
195 let ca = s_phys.bool()?;
196 Ok(is_sorted_ca_bool(ca, options.descending))
197 },
198 DataType::String => {
199 let ca = s_phys.str()?;
200 Ok(is_sorted_adjacent_total_ord(
201 ca.no_null_iter(),
202 options.descending,
203 ))
204 },
205 DataType::Binary => {
206 let ca = s_phys.binary()?;
207 Ok(is_sorted_adjacent_total_ord(
208 ca.no_null_iter(),
209 options.descending,
210 ))
211 },
212 DataType::BinaryOffset => {
213 let ca = s_phys.binary_offset()?;
214 Ok(is_sorted_adjacent_total_ord(
215 ca.no_null_iter(),
216 options.descending,
217 ))
218 },
219 _ => {
220 let cmp_len = non_null_len - 1;
222 let s1 = non_null.slice(0, cmp_len);
223 let s2 = non_null.slice(1, cmp_len);
224 let cmp_op = if options.descending {
225 Series::gt_eq
226 } else {
227 Series::lt_eq
228 };
229 Ok(cmp_op(&s1, &s2)?.all())
230 },
231 }
232}
233
234fn is_sorted_adjacent_total_ord<T: TotalOrd>(
240 it: impl Iterator<Item = T>,
241 descending: bool,
242) -> bool {
243 let mut it = it;
244 let Some(mut prev) = it.next() else {
246 return true;
247 };
248 if descending {
249 for v in it {
250 if !prev.tot_ge(&v) {
251 return false;
252 }
253 prev = v;
254 }
255 } else {
256 for v in it {
257 if !prev.tot_le(&v) {
258 return false;
259 }
260 prev = v;
261 }
262 }
263 true
264}
265
266#[cfg(feature = "dtype-categorical")]
270fn is_sorted_categorical_lexical_adjacent(s: &Series, options: SortOptions) -> PolarsResult<bool> {
271 polars_ensure!(
272 matches!(s.dtype(), DataType::Categorical(_, _)),
273 ComputeError: "internal error: expected Categorical in lexical `is_sorted` path",
274 );
275
276 with_match_categorical_physical_type!(s.dtype().cat_physical().unwrap(), |$C| {
277 let ca = s.cat::<$C>()?;
278
279 Ok(is_sorted_adjacent_total_ord(
281 ca.iter_str().map(|opt| {
282 opt.expect(
283 "`iter_str` produced None while categorical null_count reported 0 (`is_sorted`)"
284 )
285 }),
286 options.descending,
287 ))
288 })
289}
290
291fn is_sorted_ca_bool(ca: &BooleanChunked, descending: bool) -> bool {
299 let len = ca.len();
300 if len <= 1 {
301 return true;
302 }
303 debug_assert_eq!(
304 ca.null_count(),
305 0,
306 "internal error: `is_sorted_ca_bool` expects a non-null boolean slice"
307 );
308 if descending {
309 let Some(idx) = ca.first_false_idx() else {
310 return true;
311 };
312 !ca.slice(idx as i64, ca.len() - idx).any()
313 } else {
314 let Some(idx) = ca.first_true_idx() else {
315 return true;
316 };
317 ca.slice(idx as i64, ca.len() - idx).all()
318 }
319}
320
321pub fn resolve_sort_options(
333 s: &Series,
334 descending: Option<bool>,
335 nulls_last: Option<bool>,
336) -> PolarsResult<(Option<bool>, Option<bool>)> {
337 let nulls_last = match nulls_last {
338 Some(n) => Some(n),
339 None => infer_nulls_last(s),
340 };
341
342 let descending = match descending {
343 Some(d) => Some(d),
344 None => infer_descending(s, nulls_last.unwrap_or(false))?,
345 };
346
347 Ok((descending, nulls_last))
348}
349
350fn infer_nulls_last(s: &Series) -> Option<bool> {
354 let null_count = s.null_count();
355 let s_len = s.len();
356
357 if null_count == 0 || null_count == s_len {
358 return None;
359 }
360
361 if s.slice((s_len - null_count) as i64, null_count)
362 .null_count()
363 == null_count
364 {
365 Some(true)
366 } else if s.slice(0, null_count).null_count() == null_count {
367 Some(false)
368 } else {
369 None
370 }
371}
372
373fn infer_descending(s: &Series, nulls_last: bool) -> PolarsResult<Option<bool>> {
374 let null_count = s.null_count();
375 let non_null_len = s.len() - null_count;
376 if non_null_len < 2 {
377 return Ok(None);
378 }
379
380 let non_null_start = if nulls_last { 0 } else { null_count };
381 let non_null = s.slice(non_null_start as i64, non_null_len);
382
383 let a = non_null.slice(0, non_null_len - 1);
384 let b = non_null.slice(1, non_null_len - 1);
385
386 let lt = a.lt(&b)?;
387 let gt = a.gt(&b)?;
388
389 let lt_first = lt.iter().position(|v| v == Some(true));
390 let gt_first = gt.iter().position(|v| v == Some(true));
391
392 Ok(match (lt_first, gt_first) {
393 (None, None) => None,
394 (Some(_), None) => Some(false),
395 (None, Some(_)) => Some(true),
396 (Some(l), Some(g)) => Some(g < l),
397 })
398}
399
400fn check_cmp<T: NumericNative, Cmp: Fn(&T, &T) -> bool>(
401 vals: &[T],
402 f: Cmp,
403 previous: &mut T,
404) -> bool {
405 let mut sorted = true;
406 for c in vals.chunks(1024) {
407 for v in c {
408 sorted &= f(previous, v);
409 *previous = *v;
410 }
411 if !sorted {
412 return false;
413 }
414 }
415 sorted
416}
417
418fn is_sorted_ca_num<T: PolarsNumericType>(ca: &ChunkedArray<T>, options: SortOptions) -> bool {
419 if let Ok(vals) = ca.cont_slice() {
420 let Some(mut previous) = vals.first().copied() else {
421 return true;
422 };
423 return if options.descending {
424 check_cmp(vals, |prev, c| prev.tot_ge(c), &mut previous)
425 } else {
426 check_cmp(vals, |prev, c| prev.tot_le(c), &mut previous)
427 };
428 };
429
430 if ca.null_count() == 0 {
431 let Some(mut previous) = ca
432 .downcast_iter()
433 .find_map(|arr| arr.values().first().copied())
434 else {
435 return true;
436 };
437 for arr in ca.downcast_iter() {
438 let vals = arr.values();
439 let sorted = if options.descending {
440 check_cmp(vals, |prev, c| prev.tot_ge(c), &mut previous)
441 } else {
442 check_cmp(vals, |prev, c| prev.tot_le(c), &mut previous)
443 };
444 if !sorted {
445 return false;
446 }
447 }
448 return true;
449 };
450
451 let null_count = ca.null_count();
452 if options.nulls_last {
453 let ca = ca.slice(0, ca.len() - null_count);
454 is_sorted_ca_num(&ca, options)
455 } else {
456 let ca = ca.slice(null_count as i64, ca.len() - null_count);
457 is_sorted_ca_num(&ca, options)
458 }
459}
460
461impl SeriesMethods for Series {}