1#![warn(missing_docs)]
4
5use crate::{MyError, Q};
10use core::fmt;
11use jiff::{Timestamp, Zoned, civil::Date, tz::TimeZone};
12use std::{cmp::Ordering, mem};
13
14#[derive(Debug, Clone)]
16pub enum Bound {
17 None,
20 Date(Zoned),
22 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 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
42 match (self, other) {
43 (Bound::None, Bound::None) => Some(Ordering::Equal),
45 (Bound::None, _) => Some(Ordering::Less),
47 (_, Bound::None) => Some(Ordering::Greater),
49 (Bound::Date(z1), Bound::Date(z2)) | (Bound::Timestamp(z1), Bound::Timestamp(z2)) => {
51 z1.partial_cmp(z2)
52 }
53 (Bound::Date(z1), Bound::Timestamp(z2)) | (Bound::Timestamp(z1), Bound::Date(z2)) => {
56 z1.partial_cmp(z2)
57 }
58 }
59 }
60}
61
62impl fmt::Display for Bound {
63 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64 match self {
65 Bound::None => write!(f, ".."),
66 Bound::Date(x) => write!(f, "{x}/d"),
67 Bound::Timestamp(x) => write!(f, "{x}/t"),
68 }
69 }
70}
71
72impl TryFrom<Q> for Bound {
73 type Error = MyError;
74
75 fn try_from(value: Q) -> Result<Self, Self::Error> {
76 Self::try_from(&value)
77 }
78}
79
80impl TryFrom<&Q> for Bound {
81 type Error = MyError;
82
83 fn try_from(value: &Q) -> Result<Self, Self::Error> {
84 match value {
85 Q::Str(x) => {
86 let s = x.as_str();
87 match s {
88 "'..'" => Ok(Bound::None),
89 _ => Err(MyError::Runtime(
90 "Only '..' string is allowed for interval bounds".into(),
91 )),
92 }
93 }
94 Q::Instant(x) => Ok(x.to_owned()),
95 _ => Err(MyError::Runtime("Expected a zoned timestamp | '..'".into())),
96 }
97 }
98}
99
100impl Bound {
101 pub fn try_new_date(s: &str) -> Result<Self, MyError> {
104 let d = s.parse::<Date>()?;
105 let z = d.to_zoned(TimeZone::UTC)?;
106 Ok(Bound::Date(z))
107 }
108
109 pub fn try_new_timestamp(s: &str) -> Result<Self, MyError> {
112 let d = s.parse::<Timestamp>()?;
113 let z = d.to_zoned(TimeZone::UTC);
114 Ok(Bound::Timestamp(z))
115 }
116
117 pub fn as_zoned(&self) -> Option<Zoned> {
120 match self {
121 Bound::Date(x) => Some(x.to_owned()),
122 Bound::Timestamp(x) => Some(x.to_owned()),
123 Bound::None => None,
124 }
125 }
126
127 pub(crate) fn to_zoned(&self) -> Result<Zoned, MyError> {
129 match self {
130 Bound::Date(z) => Ok(z.to_owned()),
131 Bound::Timestamp(z) => Ok(z.to_owned()),
132 _ => Err(MyError::Runtime(
133 format!("{self} is not a bounded instant").into(),
134 )),
135 }
136 }
137
138 #[cfg(test)]
140 pub(crate) fn is_unbound(&self) -> bool {
141 matches!(self, Bound::None)
142 }
143}
144
145#[cfg(test)]
146mod tests {
147 use super::*;
148
149 #[test]
150 fn test_bound() {
151 const D: &str = "2015-01-01";
152 const T: &str = "2015-01-01T00:00:00Z";
153
154 let d = Bound::try_new_date(D);
155 assert!(d.is_ok());
156 let b1 = d.unwrap();
157 assert!(!b1.is_unbound());
158 let b1_ = b1.as_zoned();
159 assert!(b1_.is_some());
160 let z1 = b1_.unwrap();
161
162 let t = Bound::try_new_timestamp(T);
163 assert!(t.is_ok());
164 let b2 = t.unwrap();
165 assert!(!b2.is_unbound());
166 let b2_ = b2.as_zoned();
167 assert!(b2_.is_some());
168 let z2 = b2_.unwrap();
169
170 assert_eq!(z1, z2);
171 assert!(z1 == z2);
172 }
173}