1use polars_core::prelude::arity::{binary_elementwise, ternary_elementwise, unary_elementwise};
2use polars_core::prelude::*;
3use polars_core::with_match_physical_numeric_polars_type;
4
5#[inline]
6fn clamp<T: PartialOrd>(input: T, min: T, max: T) -> T {
7 if input < min {
8 min
9 } else if input > max {
10 max
11 } else {
12 input
13 }
14}
15
16#[inline]
17fn clamp_min<T: PartialOrd>(input: T, min: T) -> T {
18 if input < min { min } else { input }
19}
20
21#[inline]
22fn clamp_max<T: PartialOrd>(input: T, max: T) -> T {
23 if input > max { max } else { input }
24}
25
26pub fn clip(s: &Series, min: &Series, max: &Series) -> PolarsResult<Series> {
28 polars_ensure!(
29 s.dtype().to_physical().is_primitive_numeric(),
30 InvalidOperation: "`clip` only supports physical numeric types"
31 );
32 let n = [s.len(), min.len(), max.len()]
33 .into_iter()
34 .find(|l| *l != 1)
35 .unwrap_or(1);
36
37 for (i, (name, length)) in [("self", s.len()), ("min", min.len()), ("max", max.len())]
38 .into_iter()
39 .enumerate()
40 {
41 polars_ensure!(
42 length == n || length == 1,
43 length_mismatch = "clip",
44 length,
45 n,
46 argument = name,
47 argument_idx = i
48 );
49 }
50
51 let original_type = s.dtype();
52 let (min, max) = (min.strict_cast(s.dtype())?, max.strict_cast(s.dtype())?);
53
54 let (s, min, max) = (
55 s.to_physical_repr(),
56 min.to_physical_repr(),
57 max.to_physical_repr(),
58 );
59
60 with_match_physical_numeric_polars_type!(s.dtype(), |$T| {
61 let ca: &ChunkedArray<$T> = s.as_ref().as_ref().as_ref();
62 let min: &ChunkedArray<$T> = min.as_ref().as_ref().as_ref();
63 let max: &ChunkedArray<$T> = max.as_ref().as_ref().as_ref();
64 let out = clip_helper_both_bounds(ca, min, max).into_series();
65 match original_type {
66 #[cfg(feature = "dtype-decimal")]
67 DataType::Decimal(precision, scale) => {
68 let phys = out.i128()?.as_ref().clone();
69 Ok(phys.into_decimal_unchecked(*precision, *scale).into_series())
70 },
71 dt if dt.is_logical() => out.cast(original_type),
72 _ => Ok(out)
73 }
74 })
75}
76
77pub fn clip_max(s: &Series, max: &Series) -> PolarsResult<Series> {
79 polars_ensure!(
80 s.dtype().to_physical().is_primitive_numeric(),
81 InvalidOperation: "`clip` only supports physical numeric types"
82 );
83 polars_ensure!(
84 s.len() == max.len() || s.len() == 1 || max.len() == 1,
85 length_mismatch = "clip(max)",
86 s.len(),
87 max.len()
88 );
89
90 let original_type = s.dtype();
91 let max = max.strict_cast(s.dtype())?;
92
93 let (s, max) = (s.to_physical_repr(), max.to_physical_repr());
94
95 with_match_physical_numeric_polars_type!(s.dtype(), |$T| {
96 let ca: &ChunkedArray<$T> = s.as_ref().as_ref().as_ref();
97 let max: &ChunkedArray<$T> = max.as_ref().as_ref().as_ref();
98 let out = clip_helper_single_bound(ca, max, clamp_max).into_series();
99 match original_type {
100 #[cfg(feature = "dtype-decimal")]
101 DataType::Decimal(precision, scale) => {
102 let phys = out.i128()?.as_ref().clone();
103 Ok(phys.into_decimal_unchecked(*precision, *scale).into_series())
104 },
105 dt if dt.is_logical() => out.cast(original_type),
106 _ => Ok(out)
107 }
108 })
109}
110
111pub fn clip_min(s: &Series, min: &Series) -> PolarsResult<Series> {
113 polars_ensure!(
114 s.dtype().to_physical().is_primitive_numeric(),
115 InvalidOperation: "`clip` only supports physical numeric types"
116 );
117 polars_ensure!(
118 s.len() == min.len() || s.len() == 1 || min.len() == 1,
119 length_mismatch = "clip(min)",
120 s.len(),
121 min.len()
122 );
123
124 let original_type = s.dtype();
125 let min = min.strict_cast(s.dtype())?;
126
127 let (s, min) = (s.to_physical_repr(), min.to_physical_repr());
128
129 with_match_physical_numeric_polars_type!(s.dtype(), |$T| {
130 let ca: &ChunkedArray<$T> = s.as_ref().as_ref().as_ref();
131 let min: &ChunkedArray<$T> = min.as_ref().as_ref().as_ref();
132 let out = clip_helper_single_bound(ca, min, clamp_min).into_series();
133 match original_type {
134 #[cfg(feature = "dtype-decimal")]
135 DataType::Decimal(precision, scale) => {
136 let phys = out.i128()?.as_ref().clone();
137 Ok(phys.into_decimal_unchecked(*precision, *scale).into_series())
138 },
139 dt if dt.is_logical() => out.cast(original_type),
140 _ => Ok(out)
141 }
142 })
143}
144
145fn clip_helper_both_bounds<T>(
146 ca: &ChunkedArray<T>,
147 min: &ChunkedArray<T>,
148 max: &ChunkedArray<T>,
149) -> ChunkedArray<T>
150where
151 T: PolarsNumericType,
152 T::Native: PartialOrd,
153{
154 match (min.len(), max.len()) {
155 (1, 1) => match (min.get(0), max.get(0)) {
156 (Some(min), Some(max)) => clip_unary(ca, |v| clamp(v, min, max)),
157 (Some(min), None) => clip_unary(ca, |v| clamp_min(v, min)),
158 (None, Some(max)) => clip_unary(ca, |v| clamp_max(v, max)),
159 (None, None) => ca.clone(),
160 },
161 (1, _) => match min.get(0) {
162 Some(min) => clip_binary(ca, max, |v, b| clamp(v, min, b)),
163 None => clip_binary(ca, max, clamp_max),
164 },
165 (_, 1) => match max.get(0) {
166 Some(max) => clip_binary(ca, min, |v, b| clamp(v, b, max)),
167 None => clip_binary(ca, min, clamp_min),
168 },
169 _ => clip_ternary(ca, min, max),
170 }
171}
172
173fn clip_helper_single_bound<T, F>(
174 ca: &ChunkedArray<T>,
175 bound: &ChunkedArray<T>,
176 op: F,
177) -> ChunkedArray<T>
178where
179 T: PolarsNumericType,
180 T::Native: PartialOrd,
181 F: Fn(T::Native, T::Native) -> T::Native,
182{
183 match bound.len() {
184 1 => match bound.get(0) {
185 Some(bound) => clip_unary(ca, |v| op(v, bound)),
186 None => ca.clone(),
187 },
188 _ => clip_binary(ca, bound, op),
189 }
190}
191
192fn clip_unary<T, F>(ca: &ChunkedArray<T>, op: F) -> ChunkedArray<T>
193where
194 T: PolarsNumericType,
195 F: Fn(T::Native) -> T::Native + Copy,
196{
197 unary_elementwise(ca, |v| v.map(op))
198}
199
200fn clip_binary<T, F>(ca: &ChunkedArray<T>, bound: &ChunkedArray<T>, op: F) -> ChunkedArray<T>
201where
202 T: PolarsNumericType,
203 T::Native: PartialOrd,
204 F: Fn(T::Native, T::Native) -> T::Native,
205{
206 binary_elementwise(ca, bound, |opt_s, opt_bound| match (opt_s, opt_bound) {
207 (Some(s), Some(bound)) => Some(op(s, bound)),
208 (Some(s), None) => Some(s),
209 (None, _) => None,
210 })
211}
212
213fn clip_ternary<T>(
214 ca: &ChunkedArray<T>,
215 min: &ChunkedArray<T>,
216 max: &ChunkedArray<T>,
217) -> ChunkedArray<T>
218where
219 T: PolarsNumericType,
220 T::Native: PartialOrd,
221{
222 ternary_elementwise(ca, min, max, |opt_v, opt_min, opt_max| {
223 match (opt_v, opt_min, opt_max) {
224 (Some(v), Some(min), Some(max)) => Some(clamp(v, min, max)),
225 (Some(v), Some(min), None) => Some(clamp_min(v, min)),
226 (Some(v), None, Some(max)) => Some(clamp_max(v, max)),
227 (Some(v), None, None) => Some(v),
228 (None, _, _) => None,
229 }
230 })
231}