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> {
45 match (self, other) {
46 (Bound::None, Bound::None) => Some(Ordering::Equal),
48 (Bound::None, _) => Some(Ordering::Less),
50 (_, Bound::None) => Some(Ordering::Greater),
52 (Bound::Date(z1), Bound::Date(z2)) | (Bound::Timestamp(z1), Bound::Timestamp(z2)) => {
54 z1.partial_cmp(z2)
55 }
56 (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 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 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 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 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 #[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 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 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 let t = Bound::try_new_timestamp(T);
168 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 assert_eq!(z1, z2);
178 assert!(z1 == z2);
179 }
180}