ogc_cql2/
bound.rs

1// SPDX-License-Identifier: Apache-2.0
2
3#![warn(missing_docs)]
4
5//! In OGC CQL2 a temporal _Bound_ is a value that is either bounded or not used
6//! either on its own as an _Instant_ or a _limit_ in an _Interval_.
7//!
8
9use crate::{MyError, Q};
10use core::fmt;
11use jiff::{Timestamp, Zoned, civil::Date, tz::TimeZone};
12use std::{cmp::Ordering, mem};
13
14/// Possible variants of a CQL2 _Instant_ and _Interval_ limit.
15#[derive(Debug, Clone)]
16pub enum Bound {
17    /// Unbounded temporal value used as lower, upper, or both limit(s);
18    /// represented by the string `'..'` .
19    None,
20    /// Instant with a 1-day granularity, in UTC time-zone.
21    Date(Zoned),
22    /// Instant with a 1-second or less granularity in UTC time-zone.
23    Timestamp(Zoned),
24}
25
26impl PartialEq for Bound {
27    fn eq(&self, other: &Self) -> bool {
28        match (self, other) {
29            (Bound::Date(x), Bound::Date(y))
30            | (Bound::Date(x), Bound::Timestamp(y))
31            | (Bound::Timestamp(x), Bound::Date(y))
32            | (Bound::Timestamp(x), Bound::Timestamp(y)) => x == y,
33            _ => mem::discriminant(self) == mem::discriminant(other),
34        }
35    }
36}
37
38impl Eq for Bound {}
39
40impl PartialOrd for Bound {
41    // Comparing values of this type are not straightforward since they can be
42    // one of 3 variants. If one or both of the arguments are unbounded the
43    // solution is trivial. It gets more elaborate when they are bounded...
44    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
45        match (self, other) {
46            // if both are unbounded, the result is alwas equal.
47            (Bound::None, Bound::None) => Some(Ordering::Equal),
48            // if the LHS is unbounded and the RHS is not then the result is...
49            (Bound::None, _) => Some(Ordering::Less),
50            // and the opposite is true if it's the other way around...
51            (_, Bound::None) => Some(Ordering::Greater),
52            // if they're both bounded instants of the same type...
53            (Bound::Date(z1), Bound::Date(z2)) | (Bound::Timestamp(z1), Bound::Timestamp(z2)) => {
54                z1.partial_cmp(z2)
55            }
56            // now the heavy metal...
57            (Bound::Date(z1), Bound::Timestamp(z2)) | (Bound::Timestamp(z1), Bound::Date(z2)) => {
58                z1.partial_cmp(z2)
59            }
60        }
61    }
62}
63
64impl fmt::Display for Bound {
65    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66        match self {
67            Bound::None => write!(f, ".."),
68            Bound::Date(x) => write!(f, "{x}/d"),
69            Bound::Timestamp(x) => write!(f, "{x}/t"),
70        }
71    }
72}
73
74impl TryFrom<Q> for Bound {
75    type Error = MyError;
76
77    fn try_from(value: Q) -> Result<Self, Self::Error> {
78        Self::try_from(&value)
79    }
80}
81
82impl TryFrom<&Q> for Bound {
83    type Error = MyError;
84
85    fn try_from(value: &Q) -> Result<Self, Self::Error> {
86        match value {
87            Q::Str(x) => {
88                let s = x.as_str();
89                match s {
90                    "'..'" => Ok(Bound::None),
91                    _ => Err(MyError::Runtime(
92                        "Only '..' string is allowed for interval bounds".into(),
93                    )),
94                }
95            }
96            Q::Instant(x) => Ok(x.to_owned()),
97            _ => Err(MyError::Runtime("Expected a zoned timestamp | '..'".into())),
98        }
99    }
100}
101
102impl Bound {
103    /// Try creating a new Bound::Date variant from a well-formed RFC-3339
104    /// date string. Return [MyError] if an error occurs.
105    pub fn try_new_date(s: &str) -> Result<Self, MyError> {
106        let d = s.parse::<Date>()?;
107        let z = d.to_zoned(TimeZone::UTC)?;
108        Ok(Bound::Date(z))
109    }
110
111    /// Try creating a new Bound::Timestamp variant from a well-formed RFC-3339
112    /// timestamp string. Return [MyError] if an error occurs.
113    pub fn try_new_timestamp(s: &str) -> Result<Self, MyError> {
114        let d = s.parse::<Timestamp>()?;
115        let z = d.to_zoned(TimeZone::UTC);
116        Ok(Bound::Timestamp(z))
117    }
118
119    /// Return inner value if it was a bounded instant.
120    pub(crate) fn to_zoned(&self) -> Result<Zoned, MyError> {
121        match self {
122            Bound::Date(z) => Ok(z.to_owned()),
123            Bound::Timestamp(z) => Ok(z.to_owned()),
124            _ => Err(MyError::Runtime(
125                format!("{self} is not a bounded instant").into(),
126            )),
127        }
128    }
129
130    // Return the inner value in `Some` if this is not the unbound variant.
131    // Return `None` otherwise.
132    pub(crate) fn as_zoned(&self) -> Option<Zoned> {
133        match self {
134            Bound::Date(x) => Some(x.to_owned()),
135            Bound::Timestamp(x) => Some(x.to_owned()),
136            Bound::None => None,
137        }
138    }
139
140    // Return TRUE if this is an unbound variant, FALSE otherwise.
141    #[cfg(test)]
142    pub(crate) fn is_unbound(&self) -> bool {
143        matches!(self, Bound::None)
144    }
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150
151    #[test]
152    // #[tracing_test::traced_test]
153    fn test_bound() {
154        const D: &str = "2015-01-01";
155        const T: &str = "2015-01-01T00:00:00Z";
156
157        let d = Bound::try_new_date(D);
158        // tracing::debug!("d = {d:?}");
159        assert!(d.is_ok());
160        let b1 = d.unwrap();
161        assert!(!b1.is_unbound());
162        let b1_ = b1.as_zoned();
163        assert!(b1_.is_some());
164        let z1 = b1_.unwrap();
165        // tracing::debug!("z1 = {z1:?}");
166
167        let t = Bound::try_new_timestamp(T);
168        // tracing::debug!("t = {t:?}");
169        assert!(t.is_ok());
170        let b2 = t.unwrap();
171        assert!(!b2.is_unbound());
172        let b2_ = b2.as_zoned();
173        assert!(b2_.is_some());
174        let z2 = b2_.unwrap();
175        // tracing::debug!("z2 = {z2:?}");
176
177        assert_eq!(z1, z2);
178        assert!(z1 == z2);
179    }
180}