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