Skip to main content

ogc_cql2/
queryable.rs

1// SPDX-License-Identifier: Apache-2.0
2
3#![warn(missing_docs)]
4
5//! OGC CQL2 [Queryable][Q] is a token that represents a property of a [Resource][1]
6//! that can be used in a [filter expression][2].
7//!
8//! [1][crate::Resource]
9//! [2][crate::Expression]
10//!
11
12use crate::{
13    MyError,
14    bound::Bound,
15    geom::{G, GTrait},
16    qstring::QString,
17    wkb::PostGisBinary,
18};
19use core::fmt;
20use jiff::{Timestamp, Zoned, civil::Date, tz::TimeZone};
21use std::{cmp::Ordering, mem};
22use tracing::error;
23
24/// [Queryable][Q] type variants.
25#[derive(Debug)]
26pub enum DataType {
27    /// A Unicode UTF-8 string.
28    Str,
29    /// A numeric value including integers and floating points.
30    Num,
31    /// A boolean value.
32    Bool,
33    /// An _Instant_ with a granularity of a second or smaller. Timestamps are
34    /// always in the time zone UTC ("Z").
35    Timestamp,
36    /// An _Instant_ with a granularity of a day. Dates are local without an
37    /// associated time zone.
38    Date,
39    /// A temporal range of 2 _Instants_ each either _fixed_ or _unbounded_.
40    #[allow(dead_code)]
41    Interval,
42    /// A spatial (geometry) value.
43    Geom,
44    /// A collection of homogeneous values.
45    #[allow(dead_code)]
46    List,
47}
48
49/// A [`Resource`][crate::Resource] queryable property possible concrete value
50/// variants.
51#[derive(Clone)]
52pub enum Q {
53    /// Unknown or undefined w/in the current context.
54    Null,
55    /// A known boolean value.
56    Bool(bool),
57    /// A known numeric literal.
58    Num(f64),
59    /// Either a known UTF8 character string literal, or one that when used in
60    /// comparisons, should be used ignoring its case and/or accent(s).
61    Str(QString),
62    /// A known geometry (spatial) instance.
63    Geom(G),
64    /// Either a known temporal instant or an unbounded value.
65    Instant(Bound),
66    /// A temporal interval.
67    Interval(Bound, Bound),
68    /// A list of other [Queryables][Q].
69    List(Vec<Q>),
70}
71
72impl fmt::Debug for Q {
73    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74        match self {
75            Self::Null => write!(f, "Null"),
76            Self::Bool(arg0) => f.debug_tuple("Bool").field(arg0).finish(),
77            Self::Num(arg0) => f.debug_tuple("Num").field(arg0).finish(),
78            Self::Str(arg0) => f.debug_tuple("Str").field(arg0).finish(),
79            Self::Geom(x) => write!(f, "Geom({})", x.to_wkt()),
80            Self::Instant(arg0) => f.debug_tuple("Instant").field(arg0).finish(),
81            Self::Interval(arg0, arg1) => {
82                f.debug_tuple("Interval").field(arg0).field(arg1).finish()
83            }
84            Self::List(arg0) => f.debug_tuple("List").field(arg0).finish(),
85        }
86    }
87}
88
89impl PartialEq for Q {
90    fn eq(&self, other: &Self) -> bool {
91        match (self, other) {
92            (Self::Bool(l0), Self::Bool(r0)) => l0 == r0,
93            (Self::Num(l0), Self::Num(r0)) => l0 == r0,
94            (Self::Str(l0), Self::Str(r0)) => l0 == r0,
95            (Self::Geom(l0), Self::Geom(r0)) => l0 == r0,
96            (Self::Instant(l0), Self::Instant(r0)) => l0 == r0,
97            (Self::Interval(l0, l1), Self::Interval(r0, r1)) => l0 == r0 && l1 == r1,
98            (Self::List(l0), Self::List(r0)) => l0 == r0,
99            _ => mem::discriminant(self) == mem::discriminant(other),
100        }
101    }
102}
103
104impl Eq for Q {}
105
106impl PartialOrd for Q {
107    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
108        match (self, other) {
109            (Q::Null, Q::Null) => Some(Ordering::Equal),
110            (Q::Bool(a), Q::Bool(b)) => a.partial_cmp(b),
111            (Q::Num(a), Q::Num(b)) => a.partial_cmp(b),
112            (Q::Str(a), Q::Str(b)) => a.partial_cmp(b),
113            (Q::Instant(a), Q::Instant(b)) => a.partial_cmp(b),
114            (Q::Interval(a0, a1), Q::Interval(b0, b1)) => match a0.partial_cmp(b0) {
115                Some(Ordering::Equal) => match (a1, b1) {
116                    (Bound::None, Bound::None) => Some(Ordering::Equal),
117                    (Bound::None, _) => Some(Ordering::Greater),
118                    (_, Bound::None) => Some(Ordering::Less),
119                    _ => a1.partial_cmp(b1),
120                },
121                x => x,
122            },
123            (Q::List(a), Q::List(b)) => a.partial_cmp(b),
124            // anything else, incl. geometries are incomparable...
125            _ => None,
126        }
127    }
128}
129
130impl fmt::Display for Q {
131    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
132        match self {
133            Q::Null => write!(f, "Null"),
134            Q::Bool(x) => write!(f, "{x}"),
135            Q::Num(x) => write!(f, "{x}"),
136            Q::Str(x) => write!(f, "{x}"),
137            Q::Geom(x) => write!(f, "{}", x.to_wkt()),
138            Q::Instant(x) => write!(f, "{x}"),
139            Q::Interval(Bound::None, Bound::None) => write!(f, "[...]"),
140            Q::Interval(Bound::None, y) => write!(f, "[..{y}]"),
141            Q::Interval(x, Bound::None) => write!(f, "[{x}..]"),
142            Q::Interval(x, y) => write!(f, "[{x}..{y}]"),
143            Q::List(x) => write!(f, "{x:?}"),
144        }
145    }
146}
147
148impl Q {
149    /// Create a new instance as a plain literal string from given argument
150    /// **after trimming it**.
151    pub fn new_plain_str(value: &str) -> Self {
152        Self::Str(QString::plain(value.trim()))
153    }
154
155    /// Try creating a new temporal timestamp variant instance from a string of
156    /// the form _fullDate_ followed by "T", followed by _utcTime_.
157    pub fn try_from_timestamp_str(value: &str) -> Result<Self, MyError> {
158        let x = value.parse::<Timestamp>()?;
159        let z = x.to_zoned(TimeZone::UTC);
160        Ok(Q::Instant(Bound::Timestamp(z)))
161    }
162
163    /// Try creating a new temporal timestamp variant instance from a number of
164    /// nanoseconds since the Unix epoch.
165    pub fn try_from_timestamp_ns(value: i128) -> Result<Self, MyError> {
166        let x = Timestamp::from_nanosecond(value)?;
167        let z = x.to_zoned(TimeZone::UTC);
168        Ok(Q::Instant(Bound::Timestamp(z)))
169    }
170
171    /// Try creating a new temporal timestamp variant instance from a [Zoned].
172    pub fn try_from_timestamp(value: &Zoned) -> Result<Self, MyError> {
173        Ok(Q::Instant(Bound::Timestamp(value.to_owned())))
174    }
175
176    /// Try creating a new temporal date variant instance from a _fullDate_
177    /// string.
178    ///
179    /// **IMPORTANT** - CQL2 specs state that dates are to be considered as
180    /// local wrt. time zones. This implementation however always assigns a
181    /// UTC time zone.
182    pub fn try_from_date_str(value: &str) -> Result<Self, MyError> {
183        let x = value.parse::<Date>()?;
184        let z = x.to_zoned(TimeZone::UTC)?;
185        Ok(Q::Instant(Bound::Date(z)))
186    }
187
188    /// Try creating a new temporal date variant instance from a number of
189    /// nanoseconds since the Unix epoch.
190    pub fn try_from_date_ns(value: i128) -> Result<Self, MyError> {
191        let x = Timestamp::from_nanosecond(value)?;
192        let z = x.to_zoned(TimeZone::UTC);
193        Ok(Q::Instant(Bound::Date(z)))
194    }
195
196    /// Try creating a new temporal date variant instance from a civil [Date].
197    pub fn try_from_date(value: &Date) -> Result<Self, MyError> {
198        let z = value.to_zoned(TimeZone::UTC)?;
199        Ok(Q::Instant(Bound::Date(z)))
200    }
201
202    /// Try creating a new instance from a Well Known Text encoded geometry.
203    pub fn try_from_wkt(value: &str) -> Result<Self, MyError> {
204        let g = G::try_from(value)?;
205        Ok(Q::Geom(g))
206    }
207
208    /// Try creating a new instance from a _GeoPackage Well Known Binary_
209    /// encoded geometry.
210    pub fn try_from_wkb(value: &[u8]) -> Result<Self, MyError> {
211        let g = G::try_from(value)?;
212        Ok(Q::Geom(g))
213    }
214
215    /// Try creating a new instance from a _PostGIS Extended Well Known Binary_
216    /// encoded geometry.
217    pub fn try_from_ewkb(value: &[u8]) -> Result<Self, MyError> {
218        let ewkb = PostGisBinary::try_from(value)?;
219        let g = ewkb.geom();
220        Ok(Q::Geom(g))
221    }
222
223    /// Return TRUE if this is `Null`; FALSE otherwise.
224    pub(crate) fn is_null(&self) -> bool {
225        matches!(self, Q::Null)
226    }
227
228    /// Return TRUE if this is a temporal value; FALSE otherwise.
229    pub(crate) fn is_instant(&self) -> bool {
230        matches!(self, Q::Instant(_))
231    }
232
233    /// Return the current value of this if it's a boolean value.
234    pub fn to_bool(&self) -> Result<bool, MyError> {
235        match self {
236            Q::Bool(x) => Ok(*x),
237            _ => Err(MyError::Runtime(format!("{self} is not a boolean").into())),
238        }
239    }
240
241    /// Return the current value of this if it's a [string][QString] value.
242    pub fn to_str(&self) -> Result<QString, MyError> {
243        match self {
244            Q::Str(x) => Ok(x.to_owned()),
245            _ => Err(MyError::Runtime(format!("{self} is not a string").into())),
246        }
247    }
248
249    /// Return the current value of this if it's a number value.
250    pub fn to_num(&self) -> Result<f64, MyError> {
251        match self {
252            Q::Num(x) => Ok(*x),
253            _ => Err(MyError::Runtime(format!("{self} is not a number").into())),
254        }
255    }
256
257    /// Return the current value of this if it's a [Geometry][G] value.
258    pub fn to_geom(&self) -> Result<G, MyError> {
259        match self {
260            Q::Geom(x) => Ok(x.to_owned()),
261            _ => Err(MyError::Runtime(format!("{self} is not a geometry").into())),
262        }
263    }
264
265    /// Return the current value of this if it's a [Bound] value.
266    pub fn to_bound(&self) -> Result<Bound, MyError> {
267        match self {
268            Q::Instant(x) => Ok(x.to_owned()),
269            _ => Err(MyError::Runtime(
270                format!("{self} is not a bounded instant").into(),
271            )),
272        }
273    }
274
275    /// Return the current value of this if it's a _Interval_ value as a pair
276    /// of [Bound]s.
277    pub fn to_interval(&self) -> Result<(Bound, Bound), MyError> {
278        match self {
279            Q::Interval(x, y) => Ok((x.to_owned(), y.to_owned())),
280            _ => Err(MyError::Runtime(
281                format!("{self} is not an interval").into(),
282            )),
283        }
284    }
285
286    /// Return the current value of this if it's a collection.
287    pub fn to_list(&self) -> Result<Vec<Q>, MyError> {
288        match self {
289            Q::List(x) => Ok(x.to_owned()),
290            _ => Err(MyError::Runtime(format!("{self} is not a list").into())),
291        }
292    }
293
294    /// Return TRUE if both arguments are of the same type; FALSE otherwise.
295    pub(crate) fn same_type(this: &Self, that: &Self) -> bool {
296        mem::discriminant(this) == mem::discriminant(that)
297    }
298
299    // Return the optional literal data type of this.
300    pub(crate) fn literal_type(&self) -> Option<DataType> {
301        match self {
302            Q::Bool(_) => Some(DataType::Bool),
303            Q::Num(_) => Some(DataType::Num),
304            Q::Str(_) => Some(DataType::Str),
305            Q::Geom(_) => Some(DataType::Geom),
306            Q::Instant(x) => match x {
307                Bound::None => None,
308                Bound::Date(_) => Some(DataType::Date),
309                Bound::Timestamp(_) => Some(DataType::Timestamp),
310            },
311            // Q::Null | Q::Interval(_, _) | Q::List(qs)
312            _ => None,
313        }
314    }
315
316    pub(crate) fn contained_by(&self, list: Vec<Self>) -> Result<bool, MyError> {
317        if list.is_empty() {
318            return Ok(false);
319        }
320
321        if let Some(z_type) = self.literal_type() {
322            if matches!(z_type, DataType::Bool) {
323                let lhs = self.to_bool()?;
324                let rhs: Result<Vec<bool>, MyError> = list.iter().map(|e| e.to_bool()).collect();
325                let rhs = rhs?;
326                Ok(rhs.contains(&lhs))
327            } else if matches!(z_type, DataType::Num) {
328                let lhs = self.to_num()?;
329                let rhs: Result<Vec<f64>, MyError> = list.iter().map(|e| e.to_num()).collect();
330                let rhs = rhs?;
331                Ok(rhs.contains(&lhs))
332            } else if matches!(z_type, DataType::Str) {
333                let lhs = &self.to_str()?;
334                let rhs: Result<Vec<QString>, MyError> = list.iter().map(|e| e.to_str()).collect();
335                let rhs = rhs?;
336                Ok(rhs.contains(lhs))
337            } else if matches!(z_type, DataType::Date) || matches!(z_type, DataType::Timestamp) {
338                let lhs = self.to_bound()?.as_zoned().unwrap();
339                let rhs: Result<Vec<Zoned>, MyError> = list
340                    .iter()
341                    .map(|e| e.to_bound().and_then(|b| b.to_zoned()))
342                    .collect();
343                let rhs = rhs?;
344                Ok(rhs.contains(&lhs))
345            } else if matches!(z_type, DataType::Geom) {
346                let lhs = self.to_geom()?;
347                let rhs: Result<Vec<G>, MyError> = list.iter().map(|e| e.to_geom()).collect();
348                let rhs = rhs?;
349                Ok(rhs.contains(&lhs))
350            } else {
351                error!("Failed. self = {self:?}; list = {list:?}");
352                Ok(false)
353            }
354        } else {
355            Ok(false)
356        }
357    }
358}
359
360impl From<bool> for Q {
361    fn from(value: bool) -> Self {
362        Q::Bool(value)
363    }
364}
365
366// logic to convert integers, both signed and unsigned to E::Num...
367
368trait TryToF64<T> {
369    fn try_to_f64(self) -> Result<f64, MyError>;
370}
371
372// Implement trait for small integer types that involve safe conversion.
373macro_rules! impl_safe_try_to_f64 {
374    ($($t:ty),*) => {
375        $(
376            impl TryToF64<$t> for $t {
377                fn try_to_f64(self) -> Result<f64, $crate::MyError> {
378                    Ok(self as f64)
379                }
380            }
381        )*
382    };
383}
384
385// rinse + repeat...
386impl_safe_try_to_f64!(u8, u16, u32, i8, i16, i32);
387
388// Implement trait for unsigned integer types that may result in precision
389// loss.
390//
391// Constant values used for ensuring safe conversion of integers to `f64` are
392// based on the fact that in Rust `f64` numbers have a `52` bit long mantissa,
393// which implies that integers with abstract values bit-length greater than
394// `52` will not be accurately cast.
395macro_rules! impl_try_unsigned_to_f64 {
396    ($($t:ty),*) => {
397        $(
398            impl TryToF64<$t> for $t {
399                fn try_to_f64(self) -> Result<f64, $crate::MyError> {
400                    const MAX_LIMIT: $t = (1 << 53) - 1;
401
402                    if self <= MAX_LIMIT {
403                        Ok(self as f64)
404                    } else {
405                        Err(MyError::PrecisionLoss(self.to_string().into()))
406                    }
407                }
408            }
409        )*
410    };
411}
412
413impl_try_unsigned_to_f64!(u64, u128);
414
415// Implement trait for signed integer types that may result in precision loss.
416macro_rules! impl_try_signed_to_f64 {
417    ($($t:ty),*) => {
418        $(
419            impl TryToF64<$t> for $t {
420                fn try_to_f64(self) -> Result<f64, $crate::MyError> {
421                    const MAX_LIMIT: $t = (1 << 53) - 1;
422                    // const MIN_LIMIT: $t = -((1 << 53) - 1);
423                    const MIN_LIMIT: $t = - MAX_LIMIT;
424
425                    if (MIN_LIMIT..=MAX_LIMIT).contains(&self) {
426                        Ok(self as f64)
427                    } else {
428                        Err(MyError::PrecisionLoss(self.to_string().into()))
429                    }
430                }
431            }
432        )*
433    };
434}
435
436impl_try_signed_to_f64!(i64, i128);
437
438// special cases for both usize and isize to cater for platform-specific
439// bit-length of those types.
440
441impl TryToF64<usize> for usize {
442    fn try_to_f64(self) -> Result<f64, MyError> {
443        match usize::BITS {
444            32 => (self as u32).try_to_f64(),
445            _ => (self as u64).try_to_f64(),
446        }
447    }
448}
449
450impl TryToF64<isize> for isize {
451    fn try_to_f64(self) -> Result<f64, MyError> {
452        match isize::BITS {
453            32 => (self as i32).try_to_f64(),
454            _ => (self as i64).try_to_f64(),
455        }
456    }
457}
458
459// generate TryFrom<x> implementetation to Q...
460macro_rules! impl_try_from_int {
461    ($($t:ty),*) => {
462        $(
463            impl TryFrom<$t> for $crate::Q {
464                type Error = MyError;
465                fn try_from(value: $t) -> Result<Self, $crate::MyError> {
466                    let x = value.try_to_f64()?;
467                    Ok(Q::Num(x))
468                }
469            }
470        )*
471    };
472}
473
474impl_try_from_int!(
475    u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize
476);
477
478impl From<f64> for Q {
479    fn from(value: f64) -> Self {
480        Q::Num(value)
481    }
482}
483
484impl TryFrom<Date> for Q {
485    type Error = MyError;
486
487    fn try_from(value: Date) -> Result<Self, Self::Error> {
488        let z = value.to_zoned(TimeZone::UTC)?;
489        Ok(Q::Instant(Bound::Date(z)))
490    }
491}
492
493impl From<Timestamp> for Q {
494    fn from(value: Timestamp) -> Self {
495        let z = value.to_zoned(TimeZone::UTC);
496        Q::Instant(Bound::Timestamp(z))
497    }
498}
499
500impl From<Bound> for Q {
501    fn from(value: Bound) -> Self {
502        Q::Instant(value)
503    }
504}
505
506#[cfg(test)]
507mod tests {
508    use super::*;
509    use rand::Rng;
510
511    #[test]
512    fn test_usize_max() {
513        let x = usize::MAX.try_to_f64();
514        assert!(matches!(x.err(), Some(MyError::PrecisionLoss(_))))
515    }
516
517    #[test]
518    fn test_usize() {
519        let x: usize = (1 << 53) - 1;
520        let y1 = x as f64;
521        let y2 = x.try_to_f64().expect("Failed");
522        assert_eq!(y1, y2)
523    }
524
525    #[test]
526    fn test_u128_max() {
527        let x = u128::MAX.try_to_f64();
528        assert!(matches!(x.err(), Some(MyError::PrecisionLoss(_))))
529    }
530
531    #[test]
532    fn test_u128() {
533        let x: u128 = (1 << 53) - 1;
534        let y1 = x as f64;
535        let y2 = x.try_to_f64().expect("Failed");
536        assert_eq!(y1, y2)
537    }
538
539    #[test]
540    fn test_u64_max() {
541        let x = u64::MAX.try_to_f64();
542        assert!(matches!(x.err(), Some(MyError::PrecisionLoss(_))))
543    }
544
545    #[test]
546    fn test_u64() {
547        let x: u64 = (1 << 53) - 1;
548        let y1 = x as f64;
549        let y2 = x.try_to_f64().expect("Failed");
550        assert_eq!(y1, y2)
551    }
552
553    #[test]
554    fn test_isize_max() {
555        let x = isize::MAX.try_to_f64();
556        assert!(matches!(x.err(), Some(MyError::PrecisionLoss(_))))
557    }
558
559    #[test]
560    fn test_isize() {
561        let x1: isize = (1 << 53) - 1;
562        let y1 = x1 as f64;
563        let y2 = x1.try_to_f64().expect("Failed");
564        assert_eq!(y1, y2);
565
566        let x2 = -x1;
567        let y1 = x2 as f64;
568        let y2 = x2.try_to_f64().expect("Failed");
569        assert_eq!(y1, y2)
570    }
571
572    #[test]
573    fn test_isize_min() {
574        let x = isize::MIN.try_to_f64();
575        assert!(matches!(x.err(), Some(MyError::PrecisionLoss(_))))
576    }
577
578    #[test]
579    fn test_i128_max() {
580        let x = i128::MAX.try_to_f64();
581        assert!(matches!(x.err(), Some(MyError::PrecisionLoss(_))))
582    }
583
584    #[test]
585    fn test_i128() {
586        let x1: i128 = (1 << 53) - 1;
587        let y1 = x1 as f64;
588        let y2 = x1.try_to_f64().expect("Failed");
589        assert_eq!(y1, y2);
590
591        let x2 = -x1;
592        let y1 = x2 as f64;
593        let y2 = x2.try_to_f64().expect("Failed");
594        assert_eq!(y1, y2)
595    }
596
597    #[test]
598    fn test_i128_min() {
599        let x = i128::MIN.try_to_f64();
600        assert!(matches!(x.err(), Some(MyError::PrecisionLoss(_))))
601    }
602
603    #[test]
604    fn test_i64_max() {
605        let x = i64::MAX.try_to_f64();
606        assert!(matches!(x.err(), Some(MyError::PrecisionLoss(_))))
607    }
608
609    #[test]
610    fn test_i64_min() {
611        let x = i64::MIN.try_to_f64();
612        assert!(matches!(x.err(), Some(MyError::PrecisionLoss(_))))
613    }
614
615    #[test]
616    fn test_i64() {
617        let x1: i64 = (1 << 53) - 1;
618        let y1 = x1 as f64;
619        let y2 = x1.try_to_f64().expect("Failed");
620        assert_eq!(y1, y2);
621
622        let x2 = -x1;
623        let y1 = x2 as f64;
624        let y2 = x2.try_to_f64().expect("Failed");
625        assert_eq!(y1, y2)
626    }
627
628    #[test]
629    fn fuzz_test_i64() {
630        const LIMIT: i64 = (1 << 53) - 1;
631
632        fn random_i64() -> i64 {
633            let mut rng = rand::rng();
634            match rng.random_bool(0.5) {
635                true => LIMIT - rng.random_range(1..=LIMIT.abs()),
636                false => LIMIT + rng.random_range(1..=LIMIT.abs()),
637            }
638        }
639
640        let mut expected = 0;
641        let mut actual = 0;
642        for _ in 0..1000 {
643            let x = random_i64();
644            if !(-LIMIT..=LIMIT).contains(&x) {
645                expected += 1;
646            }
647            // else as f64 is ok...
648            match Q::try_from(x) {
649                Ok(_) => (), // cool
650                Err(MyError::PrecisionLoss(_)) => actual += 1,
651                Err(x) => panic!("Unexpected {x}"),
652            }
653        }
654
655        assert_eq!(expected, actual)
656    }
657
658    #[test]
659    fn fuzz_test_u64() {
660        const LIMIT: u64 = (1 << 53) - 1;
661
662        fn random_u64() -> u64 {
663            let mut rng = rand::rng();
664            match rng.random_bool(0.5) {
665                true => LIMIT.saturating_sub(rng.random_range(1..=LIMIT)),
666                false => LIMIT + rng.random_range(1..=LIMIT),
667            }
668        }
669
670        let mut expected = 0;
671        let mut actual = 0;
672        for _ in 0..1000 {
673            let x = random_u64();
674            if x > LIMIT {
675                expected += 1;
676            }
677            // else as f64 is ok...
678            match Q::try_from(x) {
679                Ok(_) => (), // cool
680                Err(MyError::PrecisionLoss(_)) => actual += 1,
681                Err(x) => panic!("Unexpected {x}"),
682            }
683        }
684
685        assert_eq!(expected, actual)
686    }
687
688    #[test]
689    fn test_like() {
690        // plain input and pattern.  no wildcards...
691        let input = QString::plain("hello");
692        let pattern = QString::plain("h%o");
693        let r1 = QString::like(&input, &pattern);
694        assert!(r1);
695
696        // case-insensitive input, plain pattern.  multi wildcard...
697        let input = QString::icase("HELLO");
698        let pattern = QString::plain("h%o");
699        let r2 = QString::like(&input, &pattern);
700        assert!(r2);
701        let input = QString::icase("HELLODOLLY");
702        let pattern = QString::plain("h%odo%y");
703        let r2p = QString::like(&input, &pattern);
704        assert!(r2p);
705
706        // plain input, case-insensitive pattern.  single wildcard...
707        let input = QString::plain("hello");
708        let pattern = QString::icase("h__lo");
709        let r3 = QString::like(&input, &pattern);
710        assert!(r3);
711        // multi wildcard...
712        let pattern = QString::icase("h%lo");
713        let r3p = QString::like(&input, &pattern);
714        assert!(r3p);
715
716        // plain input and pattern.  escaped multi wildcard...
717        let input = QString::plain("hello");
718        let pattern = QString::plain("h\\%o");
719        let r4 = QString::like(&input, &pattern);
720        assert!(!r4);
721
722        let input = QString::plain("h%llo");
723        let pattern = QString::plain("h\\%llo");
724        let r5 = QString::like(&input, &pattern);
725        assert!(r5);
726
727        // empty input and multi wildcard pattern should match
728        let input = QString::plain("");
729        let pattern = QString::plain("%");
730        let r6 = QString::like(&input, &pattern);
731        assert!(r6);
732
733        // non-empty input and empty pattern should fail
734        let input = QString::plain("abc");
735        let pattern = QString::plain("");
736        let r7 = QString::like(&input, &pattern);
737        assert!(!r7);
738
739        // w/ unicode... case-insensitive input and no wildcards...
740
741        let input = QString::icase("ß"); // small sharp s
742        let pattern = QString::icase("ẞ"); // capital sharp s
743        let u1 = QString::like(&input, &pattern);
744        assert!(u1);
745
746        let input = QString::icase("Σ");
747        let pattern = QString::plain("σ");
748        let u2 = QString::like(&input, &pattern);
749        assert!(u2);
750
751        // unicase bug?  Turkish dotted i
752        // let input = QString::plain("İ"); // capital dotted I
753        // let pattern = QString::icase("i"); // small dotted i
754        // let u3 = QString::like(&input, &pattern);
755        // assert!(u3);
756
757        // w/ unicode + wildcard...
758
759        let input = QString::plain("こんにちは");
760        let pattern = QString::plain("こ%は");
761        let u4 = QString::like(&input, &pattern);
762        assert!(u4);
763
764        let pattern = QString::icase("こ_にちは");
765        let u5 = QString::like(&input, &pattern);
766        assert!(u5);
767    }
768}