Skip to main content

polars_core/series/
comparison.rs

1//! Comparison operations on Series.
2
3use polars_error::feature_gated;
4
5use crate::prelude::*;
6use crate::series::arithmetic::coerce_lhs_rhs;
7#[cfg(feature = "dtype-decimal")]
8use crate::series::arithmetic::decimal_op_operands;
9use crate::series::nulls::replace_non_null;
10
11macro_rules! impl_eq_compare {
12    ($self:expr, $rhs:expr, $method:ident) => {{
13        use DataType::*;
14        let (lhs, rhs) = ($self, $rhs);
15        validate_types(lhs.dtype(), rhs.dtype())?;
16
17        polars_ensure!(
18            lhs.len() == rhs.len() ||
19
20            // Broadcast
21            lhs.len() == 1 ||
22            rhs.len() == 1,
23            ShapeMismatch: "could not compare between two series of different length ({} != {})",
24            lhs.len(),
25            rhs.len()
26        );
27
28        match (lhs.dtype(), rhs.dtype()) {
29            #[cfg(feature = "dtype-categorical")]
30            (Categorical(lcats, _), Categorical(rcats, _)) => {
31                ensure_same_categories(lcats, rcats)?;
32                return with_match_categorical_physical_type!(lcats.physical(), |$C| {
33                    lhs.cat::<$C>().unwrap().$method(rhs.cat::<$C>().unwrap())
34                })
35            },
36            #[cfg(feature = "dtype-categorical")]
37            (Enum(lfcats, _), Enum(rfcats, _)) => {
38                ensure_same_frozen_categories(lfcats, rfcats)?;
39                return with_match_categorical_physical_type!(lfcats.physical(), |$C| {
40                    lhs.cat::<$C>().unwrap().$method(rhs.cat::<$C>().unwrap())
41                })
42            },
43            #[cfg(feature = "dtype-categorical")]
44            (Categorical(_, _) | Enum(_, _), String) => {
45                return with_match_categorical_physical_type!(lhs.dtype().cat_physical().unwrap(), |$C| {
46                    Ok(lhs.cat::<$C>().unwrap().$method(rhs.str().unwrap()))
47                })
48            },
49            #[cfg(feature = "dtype-categorical")]
50            (String, Categorical(_, _) | Enum(_, _)) => {
51                return with_match_categorical_physical_type!(rhs.dtype().cat_physical().unwrap(), |$C| {
52                    Ok(rhs.cat::<$C>().unwrap().$method(lhs.str().unwrap()))
53                })
54            },
55            #[cfg(feature = "dtype-map")]
56            (ldt @ Map(_, _), rdt @ Map(_, _)) if ldt == rdt => {
57                let lhs = lhs.map().unwrap();
58                let rhs = rhs.map().unwrap();
59                return lhs.storage().$method(rhs.storage());
60            },
61
62            #[cfg(feature = "dtype-extension")]
63            (le @ Extension(_, _), re @ Extension(_, _)) if le == re => {
64                let lhs = lhs.ext().unwrap();
65                let rhs = rhs.ext().unwrap();
66                return lhs.storage().$method(rhs.storage());
67            },
68
69            #[cfg(feature = "dtype-extension")]
70            (Extension(_, storage), rdt) if **storage == *rdt => {
71                let lhs = lhs.ext().unwrap();
72                return lhs.storage().$method(rhs);
73            },
74
75            #[cfg(feature = "dtype-extension")]
76            (ldt, Extension(_, storage)) if *ldt == **storage => {
77                let rhs = rhs.ext().unwrap();
78                return lhs.$method(rhs.storage());
79            },
80            _ => (),
81        };
82
83        #[cfg(feature = "dtype-decimal")]
84        if let Some((l, r)) = decimal_cmp_operands(lhs, rhs)? {
85            let mut out = l.$method(&r);
86            out.rename(lhs.name().clone());
87            return Ok(out);
88        }
89
90        let (lhs, rhs) = coerce_lhs_rhs(lhs, rhs)
91            .map_err(|_| polars_err!(
92                    SchemaMismatch: "could not evaluate comparison between series '{}' of dtype: {:?} and series '{}' of dtype: {:?}",
93                    lhs.name(), lhs.dtype(), rhs.name(), rhs.dtype()
94            ))?;
95        let lhs = lhs.to_physical_repr();
96        let rhs = rhs.to_physical_repr();
97        let mut out = match lhs.dtype() {
98            Null => lhs.null().unwrap().$method(rhs.null().unwrap()),
99            Boolean => lhs.bool().unwrap().$method(rhs.bool().unwrap()),
100            String => lhs.str().unwrap().$method(rhs.str().unwrap()),
101            Binary => lhs.binary().unwrap().$method(rhs.binary().unwrap()),
102            BinaryOffset => lhs.binary_offset().unwrap().$method(rhs.binary_offset().unwrap()),
103            UInt8 => feature_gated!("dtype-u8", lhs.u8().unwrap().$method(rhs.u8().unwrap())),
104            UInt16 => feature_gated!("dtype-u16", lhs.u16().unwrap().$method(rhs.u16().unwrap())),
105            UInt32 => lhs.u32().unwrap().$method(rhs.u32().unwrap()),
106            UInt64 => lhs.u64().unwrap().$method(rhs.u64().unwrap()),
107            UInt128 => feature_gated!("dtype-u128", lhs.u128().unwrap().$method(rhs.u128().unwrap())),
108            Int8 => feature_gated!("dtype-i8", lhs.i8().unwrap().$method(rhs.i8().unwrap())),
109            Int16 => feature_gated!("dtype-i16", lhs.i16().unwrap().$method(rhs.i16().unwrap())),
110            Int32 => lhs.i32().unwrap().$method(rhs.i32().unwrap()),
111            Int64 => lhs.i64().unwrap().$method(rhs.i64().unwrap()),
112            Int128 => feature_gated!("dtype-i128", lhs.i128().unwrap().$method(rhs.i128().unwrap())),
113            Float16 => feature_gated!("dtype-f16", lhs.f16().unwrap().$method(rhs.f16().unwrap())),
114            Float32 => lhs.f32().unwrap().$method(rhs.f32().unwrap()),
115            Float64 => lhs.f64().unwrap().$method(rhs.f64().unwrap()),
116            List(_) => lhs.list().unwrap().$method(rhs.list().unwrap()),
117            #[cfg(feature = "dtype-array")]
118            Array(_, _) => lhs.array().unwrap().$method(rhs.array().unwrap()),
119            #[cfg(feature = "dtype-struct")]
120            Struct(_) => lhs.struct_().unwrap().$method(rhs.struct_().unwrap()),
121
122            dt => polars_bail!(InvalidOperation: "could not apply comparison on series of dtype '{}; operand names: '{}', '{}'", dt, lhs.name(), rhs.name()),
123        };
124        out.rename(lhs.name().clone());
125        PolarsResult::Ok(out)
126    }};
127}
128
129macro_rules! bail_invalid_ineq {
130    ($lhs:expr, $rhs:expr, $op:literal) => {
131        polars_bail!(
132            InvalidOperation: "cannot perform '{}' comparison between series '{}' of dtype: {} and series '{}' of dtype: {}",
133            $op,
134            $lhs.name(), $lhs.dtype(),
135            $rhs.name(), $rhs.dtype(),
136        )
137    };
138}
139
140macro_rules! impl_ineq_compare {
141    ($self:expr, $rhs:expr, $method:ident, $op:literal, $rev_method:ident) => {{
142        use DataType::*;
143        let (lhs, rhs) = ($self, $rhs);
144        validate_types(lhs.dtype(), rhs.dtype())?;
145
146        polars_ensure!(
147            lhs.len() == rhs.len() ||
148
149            // Broadcast
150            lhs.len() == 1 ||
151            rhs.len() == 1,
152            ShapeMismatch:
153                "could not perform '{}' comparison between series '{}' of length: {} and series '{}' of length: {}, because they have different lengths",
154            $op,
155            lhs.name(), lhs.len(),
156            rhs.name(), rhs.len()
157        );
158
159        match (lhs.dtype(), rhs.dtype()) {
160            #[cfg(feature = "dtype-categorical")]
161            (Categorical(lcats, _), Categorical(rcats, _)) => {
162                ensure_same_categories(lcats, rcats)?;
163                return with_match_categorical_physical_type!(lcats.physical(), |$C| {
164                    lhs.cat::<$C>().unwrap().$method(rhs.cat::<$C>().unwrap())
165                })
166            },
167            #[cfg(feature = "dtype-categorical")]
168            (Enum(lfcats, _), Enum(rfcats, _)) => {
169                ensure_same_frozen_categories(lfcats, rfcats)?;
170                return with_match_categorical_physical_type!(lfcats.physical(), |$C| {
171                    lhs.cat::<$C>().unwrap().$method(rhs.cat::<$C>().unwrap())
172                })
173            },
174            #[cfg(feature = "dtype-categorical")]
175            (Categorical(_, _) | Enum(_, _), String) => {
176                return with_match_categorical_physical_type!(lhs.dtype().cat_physical().unwrap(), |$C| {
177                    lhs.cat::<$C>().unwrap().$method(rhs.str().unwrap())
178                })
179            },
180            #[cfg(feature = "dtype-categorical")]
181            (String, Categorical(_, _) | Enum(_, _)) => {
182                return with_match_categorical_physical_type!(rhs.dtype().cat_physical().unwrap(), |$C| {
183                    // We use the reverse method as string <-> enum comparisons are only implemented one-way.
184                    rhs.cat::<$C>().unwrap().$rev_method(lhs.str().unwrap())
185                })
186            },
187            // Delegating to the storage would report the `List(Struct)` dtypes.
188            #[cfg(feature = "dtype-map")]
189            (Map(_, _), _) | (_, Map(_, _)) => bail_invalid_ineq!(lhs, rhs, $op),
190
191            #[cfg(feature = "dtype-extension")]
192            (le @ Extension(_, _), re @ Extension(_, _)) if le == re => {
193                let lhs = lhs.ext().unwrap();
194                let rhs = rhs.ext().unwrap();
195                return lhs.storage().$method(rhs.storage());
196            },
197
198            #[cfg(feature = "dtype-extension")]
199            (Extension(_, storage), rdt) if **storage == *rdt => {
200                let lhs = lhs.ext().unwrap();
201                return lhs.storage().$method(rhs);
202            },
203
204            #[cfg(feature = "dtype-extension")]
205            (ldt, Extension(_, storage)) if *ldt == **storage => {
206                let rhs = rhs.ext().unwrap();
207                return lhs.$method(rhs.storage());
208            },
209            _ => (),
210        };
211
212        #[cfg(feature = "dtype-decimal")]
213        if let Some((l, r)) = decimal_cmp_operands(lhs, rhs)? {
214            let mut out = l.$method(&r);
215            out.rename(lhs.name().clone());
216            return Ok(out);
217        }
218
219        let (lhs, rhs) = coerce_lhs_rhs(lhs, rhs).map_err(|_|
220            polars_err!(
221                SchemaMismatch: "could not evaluate '{}' comparison between series '{}' of dtype: {:?} and series '{}' of dtype: {:?}",
222                $op,
223                lhs.name(), lhs.dtype(),
224                rhs.name(), rhs.dtype()
225            )
226        )?;
227        let lhs = lhs.to_physical_repr();
228        let rhs = rhs.to_physical_repr();
229        let mut out = match lhs.dtype() {
230            Null => lhs.null().unwrap().$method(rhs.null().unwrap()),
231            Boolean => lhs.bool().unwrap().$method(rhs.bool().unwrap()),
232            String => lhs.str().unwrap().$method(rhs.str().unwrap()),
233            Binary => lhs.binary().unwrap().$method(rhs.binary().unwrap()),
234            BinaryOffset => lhs.binary_offset().unwrap().$method(rhs.binary_offset().unwrap()),
235            UInt8 => feature_gated!("dtype-u8", lhs.u8().unwrap().$method(rhs.u8().unwrap())),
236            UInt16 => feature_gated!("dtype-u16", lhs.u16().unwrap().$method(rhs.u16().unwrap())),
237            UInt32 => lhs.u32().unwrap().$method(rhs.u32().unwrap()),
238            UInt64 => lhs.u64().unwrap().$method(rhs.u64().unwrap()),
239            UInt128 => feature_gated!("dtype-u128", lhs.u128().unwrap().$method(rhs.u128().unwrap())),
240            Int8 => feature_gated!("dtype-i8", lhs.i8().unwrap().$method(rhs.i8().unwrap())),
241            Int16 => feature_gated!("dtype-i16", lhs.i16().unwrap().$method(rhs.i16().unwrap())),
242            Int32 => lhs.i32().unwrap().$method(rhs.i32().unwrap()),
243            Int64 => lhs.i64().unwrap().$method(rhs.i64().unwrap()),
244            Int128 => feature_gated!("dtype-i128", lhs.i128().unwrap().$method(rhs.i128().unwrap())),
245            Float16 => feature_gated!("dtype-f16", lhs.f16().unwrap().$method(rhs.f16().unwrap())),
246            Float32 => lhs.f32().unwrap().$method(rhs.f32().unwrap()),
247            Float64 => lhs.f64().unwrap().$method(rhs.f64().unwrap()),
248            List(_) => bail_invalid_ineq!(lhs, rhs, $op),
249            #[cfg(feature = "dtype-array")]
250            Array(_, _) => bail_invalid_ineq!(lhs, rhs, $op),
251            #[cfg(feature = "dtype-struct")]
252            Struct(_) => bail_invalid_ineq!(lhs, rhs, $op),
253
254            dt => polars_bail!(InvalidOperation: "could not apply comparison on series of dtype '{}'; operand names: '{}', '{}'", dt, lhs.name(), rhs.name()),
255        };
256        out.rename(lhs.name().clone());
257        PolarsResult::Ok(out)
258    }};
259}
260
261/// Returns the physical values of decimal operands aligned to the larger scale.
262#[cfg(feature = "dtype-decimal")]
263fn decimal_cmp_operands(
264    lhs: &Series,
265    rhs: &Series,
266) -> PolarsResult<Option<(Int128Chunked, Int128Chunked)>> {
267    use polars_compute::decimal::dec128_upscale_saturating;
268
269    let Some(operands) = decimal_op_operands(lhs, rhs) else {
270        return Ok(None);
271    };
272    let (lhs, rhs) = operands?;
273    let (lhs, rhs) = (lhs.decimal()?, rhs.decimal()?);
274    let scale = lhs.scale().max(rhs.scale());
275    let upscale = |ca: &DecimalChunked| {
276        let e = scale - ca.scale();
277        if e == 0 {
278            ca.physical().clone()
279        } else {
280            ca.physical()
281                .apply_values(|v| dec128_upscale_saturating(v, e))
282        }
283    };
284    Ok(Some((upscale(lhs), upscale(rhs))))
285}
286
287fn validate_types(left: &DataType, right: &DataType) -> PolarsResult<()> {
288    use DataType::*;
289
290    match (left, right) {
291        (String, dt) | (dt, String) if dt.is_primitive_numeric() => {
292            polars_bail!(ComputeError: "cannot compare string with numeric type ({})", dt)
293        },
294        #[cfg(feature = "dtype-categorical")]
295        (Categorical(_, _) | Enum(_, _), dt) | (dt, Categorical(_, _) | Enum(_, _))
296            if !(dt.is_categorical() | dt.is_string() | dt.is_enum()) =>
297        {
298            polars_bail!(ComputeError: "cannot compare categorical with {}", dt)
299        },
300        #[cfg(feature = "dtype-duration")]
301        (Date, Duration(_)) | (Duration(_), Date) => {
302            polars_bail!(ComputeError: "cannot compare date with duration")
303        },
304        _ => (),
305    };
306    Ok(())
307}
308
309impl ChunkCompareEq<&Series> for Series {
310    type Item = PolarsResult<BooleanChunked>;
311
312    /// Create a boolean mask by checking for equality.
313    fn equal(&self, rhs: &Series) -> Self::Item {
314        impl_eq_compare!(self, rhs, equal)
315    }
316
317    /// Create a boolean mask by checking for equality.
318    fn equal_missing(&self, rhs: &Series) -> Self::Item {
319        impl_eq_compare!(self, rhs, equal_missing)
320    }
321
322    /// Create a boolean mask by checking for inequality.
323    fn not_equal(&self, rhs: &Series) -> Self::Item {
324        impl_eq_compare!(self, rhs, not_equal)
325    }
326
327    /// Create a boolean mask by checking for inequality.
328    fn not_equal_missing(&self, rhs: &Series) -> Self::Item {
329        impl_eq_compare!(self, rhs, not_equal_missing)
330    }
331}
332
333impl ChunkCompareIneq<&Series> for Series {
334    type Item = PolarsResult<BooleanChunked>;
335
336    /// Create a boolean mask by checking if self > rhs.
337    fn gt(&self, rhs: &Series) -> Self::Item {
338        impl_ineq_compare!(self, rhs, gt, ">", lt)
339    }
340
341    /// Create a boolean mask by checking if self >= rhs.
342    fn gt_eq(&self, rhs: &Series) -> Self::Item {
343        impl_ineq_compare!(self, rhs, gt_eq, ">=", lt_eq)
344    }
345
346    /// Create a boolean mask by checking if self < rhs.
347    fn lt(&self, rhs: &Series) -> Self::Item {
348        impl_ineq_compare!(self, rhs, lt, "<", gt)
349    }
350
351    /// Create a boolean mask by checking if self <= rhs.
352    fn lt_eq(&self, rhs: &Series) -> Self::Item {
353        impl_ineq_compare!(self, rhs, lt_eq, "<=", gt_eq)
354    }
355}
356
357impl<Rhs> ChunkCompareEq<Rhs> for Series
358where
359    Rhs: NumericNative,
360{
361    type Item = PolarsResult<BooleanChunked>;
362
363    fn equal(&self, rhs: Rhs) -> Self::Item {
364        validate_types(self.dtype(), &DataType::Int8)?;
365        let s = self.to_physical_repr();
366        Ok(apply_method_physical_numeric!(&s, equal, rhs))
367    }
368
369    fn equal_missing(&self, rhs: Rhs) -> Self::Item {
370        validate_types(self.dtype(), &DataType::Int8)?;
371        let s = self.to_physical_repr();
372        Ok(apply_method_physical_numeric!(&s, equal_missing, rhs))
373    }
374
375    fn not_equal(&self, rhs: Rhs) -> Self::Item {
376        validate_types(self.dtype(), &DataType::Int8)?;
377        let s = self.to_physical_repr();
378        Ok(apply_method_physical_numeric!(&s, not_equal, rhs))
379    }
380
381    fn not_equal_missing(&self, rhs: Rhs) -> Self::Item {
382        validate_types(self.dtype(), &DataType::Int8)?;
383        let s = self.to_physical_repr();
384        Ok(apply_method_physical_numeric!(&s, not_equal_missing, rhs))
385    }
386}
387
388impl<Rhs> ChunkCompareIneq<Rhs> for Series
389where
390    Rhs: NumericNative,
391{
392    type Item = PolarsResult<BooleanChunked>;
393
394    fn gt(&self, rhs: Rhs) -> Self::Item {
395        validate_types(self.dtype(), &DataType::Int8)?;
396        let s = self.to_physical_repr();
397        Ok(apply_method_physical_numeric!(&s, gt, rhs))
398    }
399
400    fn gt_eq(&self, rhs: Rhs) -> Self::Item {
401        validate_types(self.dtype(), &DataType::Int8)?;
402        let s = self.to_physical_repr();
403        Ok(apply_method_physical_numeric!(&s, gt_eq, rhs))
404    }
405
406    fn lt(&self, rhs: Rhs) -> Self::Item {
407        validate_types(self.dtype(), &DataType::Int8)?;
408        let s = self.to_physical_repr();
409        Ok(apply_method_physical_numeric!(&s, lt, rhs))
410    }
411
412    fn lt_eq(&self, rhs: Rhs) -> Self::Item {
413        validate_types(self.dtype(), &DataType::Int8)?;
414        let s = self.to_physical_repr();
415        Ok(apply_method_physical_numeric!(&s, lt_eq, rhs))
416    }
417}
418
419impl ChunkCompareEq<&str> for Series {
420    type Item = PolarsResult<BooleanChunked>;
421
422    fn equal(&self, rhs: &str) -> PolarsResult<BooleanChunked> {
423        validate_types(self.dtype(), &DataType::String)?;
424        match self.dtype() {
425            DataType::String => Ok(self.str().unwrap().equal(rhs)),
426            #[cfg(feature = "dtype-categorical")]
427            DataType::Categorical(_, _) | DataType::Enum(_, _) => Ok(
428                with_match_categorical_physical_type!(self.dtype().cat_physical().unwrap(), |$C| {
429                    self.cat::<$C>().unwrap().equal(rhs)
430                }),
431            ),
432            #[cfg(feature = "dtype-extension")]
433            DataType::Extension(_, _) => self.ext().unwrap().storage().equal(rhs),
434            _ => Ok(BooleanChunked::full(self.name().clone(), false, self.len())),
435        }
436    }
437
438    fn equal_missing(&self, rhs: &str) -> Self::Item {
439        validate_types(self.dtype(), &DataType::String)?;
440        match self.dtype() {
441            DataType::String => Ok(self.str().unwrap().equal_missing(rhs)),
442            #[cfg(feature = "dtype-categorical")]
443            DataType::Categorical(_, _) | DataType::Enum(_, _) => Ok(
444                with_match_categorical_physical_type!(self.dtype().cat_physical().unwrap(), |$C| {
445                    self.cat::<$C>().unwrap().equal_missing(rhs)
446                }),
447            ),
448            #[cfg(feature = "dtype-extension")]
449            DataType::Extension(_, _) => self.ext().unwrap().storage().equal_missing(rhs),
450            _ => Ok(replace_non_null(
451                self.name().clone(),
452                self.0.chunks(),
453                false,
454            )),
455        }
456    }
457
458    fn not_equal(&self, rhs: &str) -> PolarsResult<BooleanChunked> {
459        validate_types(self.dtype(), &DataType::String)?;
460        match self.dtype() {
461            DataType::String => Ok(self.str().unwrap().not_equal(rhs)),
462            #[cfg(feature = "dtype-categorical")]
463            DataType::Categorical(_, _) | DataType::Enum(_, _) => Ok(
464                with_match_categorical_physical_type!(self.dtype().cat_physical().unwrap(), |$C| {
465                    self.cat::<$C>().unwrap().not_equal(rhs)
466                }),
467            ),
468            #[cfg(feature = "dtype-extension")]
469            DataType::Extension(_, _) => self.ext().unwrap().storage().not_equal(rhs),
470            _ => Ok(BooleanChunked::full(self.name().clone(), true, self.len())),
471        }
472    }
473
474    fn not_equal_missing(&self, rhs: &str) -> Self::Item {
475        validate_types(self.dtype(), &DataType::String)?;
476        match self.dtype() {
477            DataType::String => Ok(self.str().unwrap().not_equal_missing(rhs)),
478            #[cfg(feature = "dtype-categorical")]
479            DataType::Categorical(_, _) | DataType::Enum(_, _) => Ok(
480                with_match_categorical_physical_type!(self.dtype().cat_physical().unwrap(), |$C| {
481                    self.cat::<$C>().unwrap().not_equal_missing(rhs)
482                }),
483            ),
484            #[cfg(feature = "dtype-extension")]
485            DataType::Extension(_, _) => self.ext().unwrap().storage().not_equal_missing(rhs),
486            _ => Ok(replace_non_null(self.name().clone(), self.0.chunks(), true)),
487        }
488    }
489}
490
491impl ChunkCompareIneq<&str> for Series {
492    type Item = PolarsResult<BooleanChunked>;
493
494    fn gt(&self, rhs: &str) -> Self::Item {
495        validate_types(self.dtype(), &DataType::String)?;
496        match self.dtype() {
497            DataType::String => Ok(self.str().unwrap().gt(rhs)),
498            #[cfg(feature = "dtype-categorical")]
499            DataType::Categorical(_, _) | DataType::Enum(_, _) => Ok(
500                with_match_categorical_physical_type!(self.dtype().cat_physical().unwrap(), |$C| {
501                    self.cat::<$C>().unwrap().gt(rhs)
502                }),
503            ),
504            #[cfg(feature = "dtype-extension")]
505            DataType::Extension(_, _) => self.ext().unwrap().storage().gt(rhs),
506            _ => polars_bail!(
507                ComputeError: "cannot compare str value to series of type {}", self.dtype(),
508            ),
509        }
510    }
511
512    fn gt_eq(&self, rhs: &str) -> Self::Item {
513        validate_types(self.dtype(), &DataType::String)?;
514        match self.dtype() {
515            DataType::String => Ok(self.str().unwrap().gt_eq(rhs)),
516            #[cfg(feature = "dtype-categorical")]
517            DataType::Categorical(_, _) | DataType::Enum(_, _) => Ok(
518                with_match_categorical_physical_type!(self.dtype().cat_physical().unwrap(), |$C| {
519                    self.cat::<$C>().unwrap().gt_eq(rhs)
520                }),
521            ),
522            #[cfg(feature = "dtype-extension")]
523            DataType::Extension(_, _) => self.ext().unwrap().storage().gt_eq(rhs),
524            _ => polars_bail!(
525                ComputeError: "cannot compare str value to series of type {}", self.dtype(),
526            ),
527        }
528    }
529
530    fn lt(&self, rhs: &str) -> Self::Item {
531        validate_types(self.dtype(), &DataType::String)?;
532        match self.dtype() {
533            DataType::String => Ok(self.str().unwrap().lt(rhs)),
534            #[cfg(feature = "dtype-categorical")]
535            DataType::Categorical(_, _) | DataType::Enum(_, _) => Ok(
536                with_match_categorical_physical_type!(self.dtype().cat_physical().unwrap(), |$C| {
537                    self.cat::<$C>().unwrap().lt(rhs)
538                }),
539            ),
540            #[cfg(feature = "dtype-extension")]
541            DataType::Extension(_, _) => self.ext().unwrap().storage().lt(rhs),
542            _ => polars_bail!(
543                ComputeError: "cannot compare str value to series of type {}", self.dtype(),
544            ),
545        }
546    }
547
548    fn lt_eq(&self, rhs: &str) -> Self::Item {
549        validate_types(self.dtype(), &DataType::String)?;
550        match self.dtype() {
551            DataType::String => Ok(self.str().unwrap().lt_eq(rhs)),
552            #[cfg(feature = "dtype-categorical")]
553            DataType::Categorical(_, _) | DataType::Enum(_, _) => Ok(
554                with_match_categorical_physical_type!(self.dtype().cat_physical().unwrap(), |$C| {
555                    self.cat::<$C>().unwrap().lt_eq(rhs)
556                }),
557            ),
558            #[cfg(feature = "dtype-extension")]
559            DataType::Extension(_, _) => self.ext().unwrap().storage().lt_eq(rhs),
560            _ => polars_bail!(
561                ComputeError: "cannot compare str value to series of type {}", self.dtype(),
562            ),
563        }
564    }
565}