1use chrono::DateTime;
18use chrono::FixedOffset;
19use chrono::Local;
20use chrono::TimeZone;
21use interim::parse_date_string;
22use interim::DateError;
23use interim::Dialect;
24use thiserror::Error;
25
26use crate::backend::MillisSinceEpoch;
27use crate::backend::Timestamp;
28
29#[derive(Copy, Clone, Debug)]
31pub enum DatePatternContext {
32 Local(DateTime<Local>),
34 Fixed(DateTime<FixedOffset>),
36}
37
38impl DatePatternContext {
39 pub fn parse_relative(
41 &self,
42 s: &str,
43 kind: &str,
44 ) -> Result<DatePattern, DatePatternParseError> {
45 match *self {
46 DatePatternContext::Local(dt) => DatePattern::from_str_kind(s, kind, dt),
47 DatePatternContext::Fixed(dt) => DatePattern::from_str_kind(s, kind, dt),
48 }
49 }
50}
51
52impl From<DateTime<Local>> for DatePatternContext {
53 fn from(value: DateTime<Local>) -> Self {
54 DatePatternContext::Local(value)
55 }
56}
57
58impl From<DateTime<FixedOffset>> for DatePatternContext {
59 fn from(value: DateTime<FixedOffset>) -> Self {
60 DatePatternContext::Fixed(value)
61 }
62}
63
64#[derive(Debug, Error)]
66pub enum DatePatternParseError {
67 #[error("Invalid date pattern kind `{0}:`")]
69 InvalidKind(String),
70 #[error(transparent)]
72 ParseError(#[from] DateError),
73}
74
75#[derive(Copy, Clone, Debug, Eq, PartialEq)]
77pub enum DatePattern {
78 AtOrAfter(MillisSinceEpoch),
80 Before(MillisSinceEpoch),
82}
83
84impl DatePattern {
85 pub fn from_str_kind<Tz: TimeZone>(
99 s: &str,
100 kind: &str,
101 now: DateTime<Tz>,
102 ) -> Result<DatePattern, DatePatternParseError>
103 where
104 Tz::Offset: Copy,
105 {
106 let d =
107 parse_date_string(s, now, Dialect::Us).map_err(DatePatternParseError::ParseError)?;
108 let millis_since_epoch = MillisSinceEpoch(d.timestamp_millis());
109 match kind {
110 "after" => Ok(DatePattern::AtOrAfter(millis_since_epoch)),
111 "before" => Ok(DatePattern::Before(millis_since_epoch)),
112 kind => Err(DatePatternParseError::InvalidKind(kind.to_owned())),
113 }
114 }
115
116 pub fn matches(&self, timestamp: &Timestamp) -> bool {
118 match self {
119 DatePattern::AtOrAfter(earliest) => *earliest <= timestamp.timestamp,
120 DatePattern::Before(latest) => timestamp.timestamp < *latest,
121 }
122 }
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128
129 fn test_equal<Tz: TimeZone>(now: DateTime<Tz>, expression: &str, should_equal_time: &str)
130 where
131 Tz::Offset: Copy,
132 {
133 let expression = DatePattern::from_str_kind(expression, "after", now).unwrap();
134 assert_eq!(
135 expression,
136 DatePattern::AtOrAfter(MillisSinceEpoch(
137 DateTime::parse_from_rfc3339(should_equal_time)
138 .unwrap()
139 .timestamp_millis()
140 ))
141 );
142 }
143
144 #[test]
145 fn test_date_pattern_parses_dates_without_times_as_the_date_at_local_midnight() {
146 let now = DateTime::parse_from_rfc3339("2024-01-01T00:00:00-08:00").unwrap();
147 test_equal(now, "2023-03-25", "2023-03-25T08:00:00Z");
148 test_equal(now, "3/25/2023", "2023-03-25T08:00:00Z");
149 test_equal(now, "3/25/23", "2023-03-25T08:00:00Z");
150 }
151
152 #[test]
153 fn test_date_pattern_parses_dates_with_times_without_specifying_an_offset() {
154 let now = DateTime::parse_from_rfc3339("2024-01-01T00:00:00-08:00").unwrap();
155 test_equal(now, "2023-03-25T00:00:00", "2023-03-25T08:00:00Z");
156 test_equal(now, "2023-03-25 00:00:00", "2023-03-25T08:00:00Z");
157 }
158
159 #[test]
160 fn test_date_pattern_parses_dates_with_a_specified_offset() {
161 let now = DateTime::parse_from_rfc3339("2024-01-01T00:00:00-08:00").unwrap();
162 test_equal(
163 now,
164 "2023-03-25T00:00:00-05:00",
165 "2023-03-25T00:00:00-05:00",
166 );
167 }
168
169 #[test]
170 fn test_date_pattern_parses_dates_with_the_z_offset() {
171 let now = DateTime::parse_from_rfc3339("2024-01-01T00:00:00-08:00").unwrap();
172 test_equal(now, "2023-03-25T00:00:00Z", "2023-03-25T00:00:00Z");
173 }
174
175 #[test]
176 fn test_date_pattern_parses_relative_durations() {
177 let now = DateTime::parse_from_rfc3339("2024-01-01T00:00:00-08:00").unwrap();
178 test_equal(now, "2 hours ago", "2024-01-01T06:00:00Z");
179 test_equal(now, "5 minutes", "2024-01-01T08:05:00Z");
180 test_equal(now, "1 week ago", "2023-12-25T08:00:00Z");
181 test_equal(now, "yesterday", "2023-12-31T08:00:00Z");
182 test_equal(now, "tomorrow", "2024-01-02T08:00:00Z");
183 }
184
185 #[test]
186 fn test_date_pattern_parses_relative_dates_with_times() {
187 let now = DateTime::parse_from_rfc3339("2024-01-01T08:00:00-08:00").unwrap();
188 test_equal(now, "yesterday 5pm", "2024-01-01T01:00:00Z");
189 test_equal(now, "yesterday 10am", "2023-12-31T18:00:00Z");
190 test_equal(now, "yesterday 10:30", "2023-12-31T18:30:00Z");
191 }
192}