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