1use chrono::DateTime;
18use chrono::FixedOffset;
19use chrono::Local;
20use chrono::TimeZone;
21use interim::DateError;
22use interim::Dialect;
23use interim::parse_date_string;
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 Self::Local(dt) => DatePattern::from_str_kind(s, kind, dt),
47 Self::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 Self::Local(value)
55 }
56}
57
58impl From<DateTime<FixedOffset>> for DatePatternContext {
59 fn from(value: DateTime<FixedOffset>) -> Self {
60 Self::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<Self, 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(Self::AtOrAfter(millis_since_epoch)),
111 "before" => Ok(Self::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 Self::AtOrAfter(earliest) => *earliest <= timestamp.timestamp,
120 Self::Before(latest) => timestamp.timestamp < *latest,
121 }
122 }
123}
124
125pub fn parse_datetime(s: &str) -> chrono::ParseResult<Timestamp> {
130 Ok(Timestamp::from_datetime(
131 DateTime::parse_from_rfc2822(s).or_else(|_| DateTime::parse_from_rfc3339(s))?,
132 ))
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138
139 fn test_equal<Tz: TimeZone>(now: DateTime<Tz>, expression: &str, should_equal_time: &str)
140 where
141 Tz::Offset: Copy,
142 {
143 let expression = DatePattern::from_str_kind(expression, "after", now).unwrap();
144 assert_eq!(
145 expression,
146 DatePattern::AtOrAfter(MillisSinceEpoch(
147 DateTime::parse_from_rfc3339(should_equal_time)
148 .unwrap()
149 .timestamp_millis()
150 ))
151 );
152 }
153
154 #[test]
155 fn test_date_pattern_parses_dates_without_times_as_the_date_at_local_midnight() {
156 let now = DateTime::parse_from_rfc3339("2024-01-01T00:00:00-08:00").unwrap();
157 test_equal(now, "2023-03-25", "2023-03-25T08:00:00Z");
158 test_equal(now, "3/25/2023", "2023-03-25T08:00:00Z");
159 test_equal(now, "3/25/23", "2023-03-25T08:00:00Z");
160 }
161
162 #[test]
163 fn test_date_pattern_parses_dates_with_times_without_specifying_an_offset() {
164 let now = DateTime::parse_from_rfc3339("2024-01-01T00:00:00-08:00").unwrap();
165 test_equal(now, "2023-03-25T00:00:00", "2023-03-25T08:00:00Z");
166 test_equal(now, "2023-03-25 00:00:00", "2023-03-25T08:00:00Z");
167 }
168
169 #[test]
170 fn test_date_pattern_parses_dates_with_a_specified_offset() {
171 let now = DateTime::parse_from_rfc3339("2024-01-01T00:00:00-08:00").unwrap();
172 test_equal(
173 now,
174 "2023-03-25T00:00:00-05:00",
175 "2023-03-25T00:00:00-05:00",
176 );
177 }
178
179 #[test]
180 fn test_date_pattern_parses_dates_with_the_z_offset() {
181 let now = DateTime::parse_from_rfc3339("2024-01-01T00:00:00-08:00").unwrap();
182 test_equal(now, "2023-03-25T00:00:00Z", "2023-03-25T00:00:00Z");
183 }
184
185 #[test]
186 fn test_date_pattern_parses_relative_durations() {
187 let now = DateTime::parse_from_rfc3339("2024-01-01T00:00:00-08:00").unwrap();
188 test_equal(now, "2 hours ago", "2024-01-01T06:00:00Z");
189 test_equal(now, "5 minutes", "2024-01-01T08:05:00Z");
190 test_equal(now, "1 week ago", "2023-12-25T08:00:00Z");
191 test_equal(now, "yesterday", "2023-12-31T08:00:00Z");
192 test_equal(now, "tomorrow", "2024-01-02T08:00:00Z");
193 }
194
195 #[test]
196 fn test_date_pattern_parses_relative_dates_with_times() {
197 let now = DateTime::parse_from_rfc3339("2024-01-01T08:00:00-08:00").unwrap();
198 test_equal(now, "yesterday 5pm", "2024-01-01T01:00:00Z");
199 test_equal(now, "yesterday 10am", "2023-12-31T18:00:00Z");
200 test_equal(now, "yesterday 10:30", "2023-12-31T18:30:00Z");
201 }
202
203 #[test]
204 fn test_parse_datetime_non_sense_yields_error() {
205 let parse_error = parse_datetime("aaaaa").err().unwrap();
206 assert_eq!(parse_error.kind(), chrono::format::ParseErrorKind::Invalid);
207 }
208}