Skip to main content

polars_core/chunked_array/ops/
mod.rs

1//! Traits for miscellaneous operations on ChunkedArray
2use arrow::offset::OffsetsBuffer;
3use polars_compute::rolling::QuantileMethod;
4
5use crate::prelude::*;
6
7pub(crate) mod aggregate;
8pub(crate) mod any_value;
9pub(crate) mod append;
10mod apply;
11#[cfg(feature = "approx_unique")]
12mod approx_n_unique;
13pub mod arity;
14mod bit_repr;
15mod bits;
16#[cfg(feature = "bitwise")]
17mod bitwise_reduce;
18pub(crate) mod chunkops;
19pub(crate) mod compare_inner;
20#[cfg(feature = "dtype-decimal")]
21mod decimal;
22pub(crate) mod downcast;
23pub(crate) mod explode;
24mod explode_and_offsets;
25mod extend;
26pub mod fill_null;
27mod filter;
28pub mod float_sorted_arg_max;
29mod for_each;
30pub mod full;
31pub mod gather;
32mod nesting_utils;
33pub(crate) mod nulls;
34mod reverse;
35#[cfg(feature = "rolling_window")]
36pub(crate) mod rolling_window;
37pub mod row_encode;
38pub mod search_sorted;
39mod set;
40mod shift;
41pub mod sort;
42#[cfg(feature = "algorithm_group_by")]
43pub(crate) mod unique;
44#[cfg(feature = "zip_with")]
45pub mod zip;
46
47pub use chunkops::_set_check_length;
48pub use nesting_utils::ChunkNestingUtils;
49#[cfg(feature = "serde-lazy")]
50use serde::{Deserialize, Serialize};
51pub use sort::options::*;
52
53use crate::chunked_array::cast::CastOptions;
54use crate::series::{BitRepr, IsSorted};
55pub trait Reinterpret {
56    fn reinterpret_signed(&self) -> Series {
57        unimplemented!()
58    }
59
60    fn reinterpret_unsigned(&self) -> Series {
61        unimplemented!()
62    }
63}
64
65/// Transmute [`ChunkedArray`] to bit representation.
66/// This is useful in hashing context and reduces no.
67/// of compiled code paths.
68pub(crate) trait ToBitRepr {
69    fn to_bit_repr(&self) -> BitRepr;
70}
71
72pub trait ChunkAnyValue {
73    /// Get a single value. Beware this is slow.
74    /// If you need to use this slightly performant, cast Categorical to UInt32
75    ///
76    /// # Safety
77    /// Does not do any bounds checking.
78    unsafe fn get_any_value_unchecked(&self, index: usize) -> AnyValue<'_>;
79
80    /// Get a single value. Beware this is slow.
81    fn get_any_value(&self, index: usize) -> PolarsResult<AnyValue<'_>>;
82}
83
84pub trait ChunkAnyValueBypassValidity {
85    /// Get a single value bypassing the validity map. Beware this is slow.
86    ///
87    /// # Safety
88    /// Does not do any bounds checking.
89    unsafe fn get_any_value_bypass_validity(&self, index: usize) -> AnyValue<'_>;
90}
91
92#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
93#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
94#[cfg_attr(feature = "dsl-schema", derive(schemars::JsonSchema))]
95pub struct ExplodeOptions {
96    /// Explode an empty list into a `null`.
97    pub empty_as_null: bool,
98    /// Explode a `null` into a `null`.
99    pub keep_nulls: bool,
100}
101
102/// Explode/flatten a List or String Series
103pub trait ChunkExplode {
104    fn explode(&self, options: ExplodeOptions) -> PolarsResult<Series> {
105        self.explode_and_offsets(options).map(|t| t.0)
106    }
107    fn offsets(&self) -> PolarsResult<OffsetsBuffer<i64>>;
108    fn explode_and_offsets(
109        &self,
110        options: ExplodeOptions,
111    ) -> PolarsResult<(Series, OffsetsBuffer<i64>)>;
112}
113
114pub trait ChunkBytes {
115    fn to_byte_slices(&self) -> Vec<&[u8]>;
116}
117
118/// This differs from ChunkWindowCustom and ChunkWindow
119/// by not using a fold aggregator, but reusing a `Series` wrapper and calling `Series` aggregators.
120/// This likely is a bit slower than ChunkWindow
121#[cfg(feature = "rolling_window")]
122pub trait ChunkRollApply: AsRefDataType {
123    fn rolling_map(
124        &self,
125        f: &dyn Fn(&Series) -> PolarsResult<Series>,
126        options: RollingOptionsFixedWindow,
127    ) -> PolarsResult<Series>
128    where
129        Self: Sized;
130}
131
132pub trait ChunkTake<Idx: ?Sized>: ChunkTakeUnchecked<Idx> {
133    /// Gather values from ChunkedArray by index.
134    fn take(&self, indices: &Idx) -> PolarsResult<Self>
135    where
136        Self: Sized;
137}
138
139pub trait ChunkTakeUnchecked<Idx: ?Sized> {
140    /// Gather values from ChunkedArray by index.
141    ///
142    /// # Safety
143    /// The non-null indices must be valid.
144    unsafe fn take_unchecked(&self, indices: &Idx) -> Self;
145}
146
147/// Create a `ChunkedArray` with new values by index or by boolean mask.
148///
149/// Note that these operations clone data. This is however the only way we can modify at mask or
150/// index level as the underlying Arrow arrays are immutable.
151pub trait ChunkSet<'a, A, B> {
152    /// Set the values at indexes `idx` to some optional value `Option<T>`.
153    ///
154    /// # Example
155    ///
156    /// ```rust
157    /// # use polars_core::prelude::*;
158    /// let ca = UInt32Chunked::new("a".into(), &[1, 2, 3]);
159    /// let new = ca.scatter_single(vec![0, 1], Some(10)).unwrap();
160    ///
161    /// assert_eq!(Vec::from(&new), &[Some(10), Some(10), Some(3)]);
162    /// ```
163    fn scatter_single<I: IntoIterator<Item = IdxSize>>(
164        &'a self,
165        idx: I,
166        opt_value: Option<A>,
167    ) -> PolarsResult<Self>
168    where
169        Self: Sized;
170
171    /// Set the values at indexes `idx` by applying a closure to these values.
172    ///
173    /// # Example
174    ///
175    /// ```rust
176    /// # use polars_core::prelude::*;
177    /// let ca = Int32Chunked::new("a".into(), &[1, 2, 3]);
178    /// let new = ca.scatter_with(vec![0, 1], |opt_v| opt_v.map(|v| v - 5)).unwrap();
179    ///
180    /// assert_eq!(Vec::from(&new), &[Some(-4), Some(-3), Some(3)]);
181    /// ```
182    fn scatter_with<I: IntoIterator<Item = IdxSize>, F>(
183        &'a self,
184        idx: I,
185        f: F,
186    ) -> PolarsResult<Self>
187    where
188        Self: Sized,
189        F: Fn(Option<A>) -> Option<B>;
190    /// Set the values where the mask evaluates to `true` to some optional value `Option<T>`.
191    ///
192    /// # Example
193    ///
194    /// ```rust
195    /// # use polars_core::prelude::*;
196    /// let ca = Int32Chunked::new("a".into(), &[1, 2, 3]);
197    /// let mask = BooleanChunked::new("mask".into(), &[false, true, false]);
198    /// let new = ca.set(&mask, Some(5)).unwrap();
199    /// assert_eq!(Vec::from(&new), &[Some(1), Some(5), Some(3)]);
200    /// ```
201    fn set(&'a self, mask: &BooleanChunked, opt_value: Option<A>) -> PolarsResult<Self>
202    where
203        Self: Sized;
204}
205
206/// Cast `ChunkedArray<T>` to `ChunkedArray<N>`
207pub trait ChunkCast {
208    /// Cast a [`ChunkedArray`] to [`DataType`]
209    fn cast(&self, dtype: &DataType) -> PolarsResult<Series> {
210        self.cast_with_options(dtype, CastOptions::NonStrict)
211    }
212
213    /// Cast a [`ChunkedArray`] to [`DataType`]
214    fn cast_with_options(&self, dtype: &DataType, options: CastOptions) -> PolarsResult<Series>;
215
216    /// Does not check if the cast is a valid one and may over/underflow
217    ///
218    /// # Safety
219    /// - This doesn't do utf8 validation checking when casting from binary
220    /// - This doesn't do categorical bound checking when casting from UInt32
221    unsafe fn cast_unchecked(&self, dtype: &DataType) -> PolarsResult<Series>;
222}
223
224/// Fastest way to do elementwise operations on a [`ChunkedArray<T>`] when the operation is cheaper than
225/// branching due to null checking.
226pub trait ChunkApply<'a, T> {
227    type FuncRet;
228
229    /// Apply a closure elementwise. This is fastest when the null check branching is more expensive
230    /// than the closure application. Often it is.
231    ///
232    /// Null values remain null.
233    ///
234    /// # Example
235    ///
236    /// ```
237    /// use polars_core::prelude::*;
238    /// fn double(ca: &UInt32Chunked) -> UInt32Chunked {
239    ///     ca.apply_values(|v| v * 2)
240    /// }
241    /// ```
242    #[must_use]
243    fn apply_values<F>(&'a self, f: F) -> Self
244    where
245        F: Fn(T) -> Self::FuncRet + Copy;
246
247    /// Apply a closure elementwise including null values.
248    #[must_use]
249    fn apply<F>(&'a self, f: F) -> Self
250    where
251        F: Fn(Option<T>) -> Option<Self::FuncRet> + Copy;
252
253    /// Apply a closure elementwise and write results to a mutable slice.
254    fn apply_to_slice<F, S>(&'a self, f: F, slice: &mut [S])
255    // (value of chunkedarray, value of slice) -> value of slice
256    where
257        F: Fn(Option<T>, &S) -> S;
258}
259
260/// Aggregation operations.
261pub trait ChunkAgg<T> {
262    /// Aggregate the sum of the ChunkedArray.
263    /// Returns `None` if not implemented for `T`.
264    /// If the array is empty, `0` is returned
265    fn sum(&self) -> Option<T> {
266        None
267    }
268
269    fn _sum_as_f64(&self) -> f64;
270
271    fn min(&self) -> Option<T> {
272        None
273    }
274
275    /// Returns the maximum value in the array, according to the natural order.
276    /// Returns `None` if the array is empty or only contains null values.
277    fn max(&self) -> Option<T> {
278        None
279    }
280
281    fn min_max(&self) -> Option<(T, T)> {
282        Some((self.min()?, self.max()?))
283    }
284
285    /// Returns the mean value in the array.
286    /// Returns `None` if the array is empty or only contains null values.
287    fn mean(&self) -> Option<f64> {
288        None
289    }
290}
291
292/// Quantile and median aggregation.
293pub trait ChunkQuantile<T> {
294    /// Returns the mean value in the array.
295    /// Returns `None` if the array is empty or only contains null values.
296    fn median(&self) -> Option<T> {
297        None
298    }
299    /// Aggregate a given quantile of the ChunkedArray.
300    /// Returns `None` if the array is empty or only contains null values.
301    fn quantile(&self, _quantile: f64, _method: QuantileMethod) -> PolarsResult<Option<T>> {
302        Ok(None)
303    }
304    /// Aggregate a given set of quantiles of the ChunkedArray.
305    /// Returns `None` if the array is empty or only contains null values.
306    fn quantiles(&self, quantiles: &[f64], _method: QuantileMethod) -> PolarsResult<Vec<Option<T>>>
307    where
308        T: Clone,
309    {
310        Ok(vec![None; quantiles.len()])
311    }
312}
313
314/// Variance and standard deviation aggregation.
315pub trait ChunkVar {
316    /// Compute the variance of this ChunkedArray/Series.
317    fn var(&self, _ddof: u8) -> Option<f64> {
318        None
319    }
320
321    /// Compute the standard deviation of this ChunkedArray/Series.
322    fn std(&self, _ddof: u8) -> Option<f64> {
323        None
324    }
325}
326
327/// Bitwise Reduction Operations.
328#[cfg(feature = "bitwise")]
329pub trait ChunkBitwiseReduce {
330    type Physical;
331
332    fn and_reduce(&self) -> Option<Self::Physical>;
333    fn or_reduce(&self) -> Option<Self::Physical>;
334    fn xor_reduce(&self) -> Option<Self::Physical>;
335}
336
337/// Compare [`Series`] and [`ChunkedArray`]'s and get a `boolean` mask that
338/// can be used to filter rows.
339///
340/// # Example
341///
342/// ```
343/// use polars_core::prelude::*;
344/// fn filter_all_ones(df: &DataFrame) -> PolarsResult<DataFrame> {
345///     let mask = df
346///     .column("column_a")?
347///     .as_materialized_series()
348///     .equal(1)?;
349///
350///     df.filter(&mask)
351/// }
352/// ```
353pub trait ChunkCompareEq<Rhs> {
354    type Item;
355
356    /// Check for equality.
357    fn equal(&self, rhs: Rhs) -> Self::Item;
358
359    /// Check for equality where `None == None`.
360    fn equal_missing(&self, rhs: Rhs) -> Self::Item;
361
362    /// Check for inequality.
363    fn not_equal(&self, rhs: Rhs) -> Self::Item;
364
365    /// Check for inequality where `None == None`.
366    fn not_equal_missing(&self, rhs: Rhs) -> Self::Item;
367}
368
369/// Compare [`Series`] and [`ChunkedArray`]'s using inequality operators (`<`, `>=`, etc.) and get
370/// a `boolean` mask that can be used to filter rows.
371pub trait ChunkCompareIneq<Rhs> {
372    type Item;
373
374    /// Greater than comparison.
375    fn gt(&self, rhs: Rhs) -> Self::Item;
376
377    /// Greater than or equal comparison.
378    fn gt_eq(&self, rhs: Rhs) -> Self::Item;
379
380    /// Less than comparison.
381    fn lt(&self, rhs: Rhs) -> Self::Item;
382
383    /// Less than or equal comparison
384    fn lt_eq(&self, rhs: Rhs) -> Self::Item;
385}
386
387/// Get unique values in a `ChunkedArray`
388pub trait ChunkUnique {
389    // We don't return Self to be able to use AutoRef specialization
390    /// Get unique values of a ChunkedArray
391    fn unique(&self) -> PolarsResult<Self>
392    where
393        Self: Sized;
394
395    /// Get first index of the unique values in a `ChunkedArray`.
396    /// This Vec is sorted.
397    fn arg_unique(&self) -> PolarsResult<IdxCa>;
398
399    /// Number of unique values in the `ChunkedArray`
400    fn n_unique(&self) -> PolarsResult<usize> {
401        self.arg_unique().map(|v| v.len())
402    }
403
404    /// Get dense ids for each unique value.
405    ///
406    /// Returns: (n_unique, unique_ids)
407    fn unique_id(&self) -> PolarsResult<(IdxSize, Vec<IdxSize>)>;
408}
409
410#[cfg(feature = "approx_unique")]
411pub trait ChunkApproxNUnique {
412    fn approx_n_unique(&self) -> IdxSize;
413}
414
415/// Sort operations on `ChunkedArray`.
416pub trait ChunkSort<T: PolarsDataType> {
417    #[allow(unused_variables)]
418    fn sort_with(&self, options: SortOptions) -> ChunkedArray<T>;
419
420    /// Returned a sorted `ChunkedArray`.
421    fn sort(&self, descending: bool) -> ChunkedArray<T>;
422
423    /// Retrieve the indexes needed to sort this array.
424    fn arg_sort(&self, options: SortOptions) -> IdxCa;
425
426    /// Retrieve the indexes need to sort this and the other arrays.
427    #[allow(unused_variables)]
428    fn arg_sort_multiple(
429        &self,
430        by: &[Column],
431        _options: &SortMultipleOptions,
432    ) -> PolarsResult<IdxCa> {
433        polars_bail!(opq = arg_sort_multiple, T::get_static_dtype());
434    }
435}
436
437pub type FillNullLimit = Option<IdxSize>;
438
439#[derive(Copy, Clone, Debug, PartialEq, Hash)]
440#[cfg_attr(feature = "serde-lazy", derive(Serialize, Deserialize))]
441#[cfg_attr(feature = "dsl-schema", derive(schemars::JsonSchema))]
442pub enum FillNullStrategy {
443    /// previous value in array
444    Backward(FillNullLimit),
445    /// next value in array
446    Forward(FillNullLimit),
447    /// mean value of array
448    Mean,
449    /// minimal value in array
450    Min,
451    /// maximum value in array
452    Max,
453    /// replace with the value zero
454    Zero,
455    /// replace with the value one
456    One,
457}
458
459impl FillNullStrategy {
460    pub fn is_elementwise(&self) -> bool {
461        matches!(self, Self::One | Self::Zero)
462    }
463}
464
465/// Replace None values with a value
466pub trait ChunkFillNullValue<T> {
467    /// Replace None values with a give value `T`.
468    fn fill_null_with_values(&self, value: T) -> PolarsResult<Self>
469    where
470        Self: Sized;
471}
472
473/// Fill a ChunkedArray with one value.
474pub trait ChunkFull<T> {
475    /// Create a ChunkedArray with a single value.
476    fn full(name: PlSmallStr, value: T, length: usize) -> Self
477    where
478        Self: Sized;
479}
480
481pub trait ChunkFullNull {
482    fn full_null(_name: PlSmallStr, _length: usize) -> Self
483    where
484        Self: Sized;
485}
486
487/// Reverse a [`ChunkedArray<T>`]
488pub trait ChunkReverse {
489    /// Return a reversed version of this array.
490    fn reverse(&self) -> Self;
491}
492
493/// Filter values by a boolean mask.
494pub trait ChunkFilter<T: PolarsDataType> {
495    /// Filter values in the ChunkedArray with a boolean mask.
496    ///
497    /// ```rust
498    /// # use polars_core::prelude::*;
499    /// let array = Int32Chunked::new("array".into(), &[1, 2, 3]);
500    /// let mask = BooleanChunked::new("mask".into(), &[true, false, true]);
501    ///
502    /// let filtered = array.filter(&mask).unwrap();
503    /// assert_eq!(Vec::from(&filtered), [Some(1), Some(3)])
504    /// ```
505    fn filter(&self, filter: &BooleanChunked) -> PolarsResult<ChunkedArray<T>>
506    where
507        Self: Sized;
508}
509
510/// Create a new ChunkedArray filled with values at that index.
511pub trait ChunkExpandAtIndex<T: PolarsDataType> {
512    /// Create a new ChunkedArray filled with values at that index.
513    fn new_from_index(&self, index: usize, length: usize) -> ChunkedArray<T>;
514}
515
516macro_rules! impl_chunk_expand {
517    ($self:ident, $length:ident, $index:ident) => {{
518        if $self.is_empty() {
519            return $self.clone();
520        }
521        let opt_val = $self.get($index);
522        match opt_val {
523            Some(val) => ChunkedArray::full($self.name().clone(), val, $length),
524            None => ChunkedArray::full_null($self.name().clone(), $length),
525        }
526    }};
527}
528
529impl<T: PolarsNumericType> ChunkExpandAtIndex<T> for ChunkedArray<T>
530where
531    ChunkedArray<T>: ChunkFull<T::Native>,
532{
533    fn new_from_index(&self, index: usize, length: usize) -> ChunkedArray<T> {
534        let mut out = impl_chunk_expand!(self, length, index);
535        out.set_sorted_flag(IsSorted::Ascending);
536        out
537    }
538}
539
540impl ChunkExpandAtIndex<BooleanType> for BooleanChunked {
541    fn new_from_index(&self, index: usize, length: usize) -> BooleanChunked {
542        let mut out = impl_chunk_expand!(self, length, index);
543        out.set_sorted_flag(IsSorted::Ascending);
544        out
545    }
546}
547
548impl ChunkExpandAtIndex<StringType> for StringChunked {
549    fn new_from_index(&self, index: usize, length: usize) -> StringChunked {
550        let mut out = impl_chunk_expand!(self, length, index);
551        out.set_sorted_flag(IsSorted::Ascending);
552        out
553    }
554}
555
556impl ChunkExpandAtIndex<BinaryType> for BinaryChunked {
557    fn new_from_index(&self, index: usize, length: usize) -> BinaryChunked {
558        let mut out = impl_chunk_expand!(self, length, index);
559        out.set_sorted_flag(IsSorted::Ascending);
560        out
561    }
562}
563
564impl ChunkExpandAtIndex<BinaryOffsetType> for BinaryOffsetChunked {
565    fn new_from_index(&self, index: usize, length: usize) -> BinaryOffsetChunked {
566        let mut out = impl_chunk_expand!(self, length, index);
567        out.set_sorted_flag(IsSorted::Ascending);
568        out
569    }
570}
571
572impl ChunkExpandAtIndex<ListType> for ListChunked {
573    fn new_from_index(&self, index: usize, length: usize) -> ListChunked {
574        let opt_val = self.get_as_series(index);
575        match opt_val {
576            Some(val) => {
577                let mut ca = ListChunked::full(self.name().clone(), &val, length);
578                unsafe { ca.to_logical(self.inner_dtype().clone()) };
579                ca
580            },
581            None => {
582                ListChunked::full_null_with_dtype(self.name().clone(), length, self.inner_dtype())
583            },
584        }
585    }
586}
587
588#[cfg(feature = "dtype-struct")]
589impl ChunkExpandAtIndex<StructType> for StructChunked {
590    fn new_from_index(&self, index: usize, length: usize) -> ChunkedArray<StructType> {
591        let (chunk_idx, idx) = self.index_to_chunked_index(index);
592        let chunk = self.downcast_chunks().get(chunk_idx).unwrap();
593        let chunk = if chunk.is_null(idx) {
594            new_null_array(chunk.dtype().clone(), length)
595        } else {
596            let values = chunk
597                .values()
598                .iter()
599                .map(|arr| {
600                    let s = Series::try_from((PlSmallStr::EMPTY, arr.clone())).unwrap();
601                    let s = s.new_from_index(idx, length);
602                    s.chunks()[0].clone()
603                })
604                .collect::<Vec<_>>();
605
606            StructArray::new(chunk.dtype().clone(), length, values, None).boxed()
607        };
608
609        // SAFETY: chunks are from self.
610        unsafe { self.copy_with_chunks(vec![chunk]) }
611    }
612}
613
614#[cfg(feature = "dtype-array")]
615impl ChunkExpandAtIndex<FixedSizeListType> for ArrayChunked {
616    fn new_from_index(&self, index: usize, length: usize) -> ArrayChunked {
617        let opt_val = self.get_as_series(index);
618        match opt_val {
619            Some(val) => {
620                let mut ca = ArrayChunked::full(self.name().clone(), &val, length);
621                unsafe { ca.to_logical(self.inner_dtype().clone()) };
622                ca
623            },
624            None => ArrayChunked::full_null_with_dtype(
625                self.name().clone(),
626                length,
627                self.inner_dtype(),
628                self.width(),
629            ),
630        }
631    }
632}
633
634#[cfg(feature = "object")]
635impl<T: PolarsObject> ChunkExpandAtIndex<ObjectType<T>> for ObjectChunked<T> {
636    fn new_from_index(&self, index: usize, length: usize) -> ObjectChunked<T> {
637        let opt_val = self.get(index);
638        match opt_val {
639            Some(val) => ObjectChunked::<T>::full(self.name().clone(), val.clone(), length),
640            None => ObjectChunked::<T>::full_null(self.name().clone(), length),
641        }
642    }
643}
644
645/// Shift the values of a [`ChunkedArray`] by a number of periods.
646pub trait ChunkShiftFill<T: PolarsDataType, V> {
647    /// Shift the values by a given period and fill the parts that will be empty due to this operation
648    /// with `fill_value`.
649    fn shift_and_fill(&self, periods: i64, fill_value: V) -> ChunkedArray<T>;
650}
651
652pub trait ChunkShift<T: PolarsDataType> {
653    fn shift(&self, periods: i64) -> ChunkedArray<T>;
654}
655
656/// Combine two [`ChunkedArray`] based on some predicate.
657pub trait ChunkZip<T: PolarsDataType> {
658    /// Create a new ChunkedArray with values from self where the mask evaluates `true` and values
659    /// from `other` where the mask evaluates `false`
660    fn zip_with(
661        &self,
662        mask: &BooleanChunked,
663        other: &ChunkedArray<T>,
664    ) -> PolarsResult<ChunkedArray<T>>;
665}
666
667/// Apply kernels on the arrow array chunks in a ChunkedArray.
668pub trait ChunkApplyKernel<A: Array> {
669    /// Apply kernel and return result as a new ChunkedArray.
670    #[must_use]
671    fn apply_kernel(&self, f: &dyn Fn(&A) -> ArrayRef) -> Self;
672
673    /// Apply a kernel that outputs an array of different type.
674    fn apply_kernel_cast<S>(&self, f: &dyn Fn(&A) -> ArrayRef) -> ChunkedArray<S>
675    where
676        S: PolarsDataType;
677}
678
679#[cfg(feature = "is_first_distinct")]
680/// Mask the first unique values as `true`
681pub trait IsFirstDistinct<T: PolarsDataType> {
682    fn is_first_distinct(&self) -> PolarsResult<BooleanChunked> {
683        polars_bail!(opq = is_first_distinct, T::get_static_dtype());
684    }
685}
686
687#[cfg(feature = "is_last_distinct")]
688/// Mask the last unique values as `true`
689pub trait IsLastDistinct<T: PolarsDataType> {
690    fn is_last_distinct(&self) -> PolarsResult<BooleanChunked> {
691        polars_bail!(opq = is_last_distinct, T::get_static_dtype());
692    }
693}