1use std::error::Error;
2use time::{
3 ext::NumericalDuration, OffsetDateTime, PrimitiveDateTime as DateTime, Time, UtcOffset,
4};
5use winnow::{
6 ascii::digit1,
7 combinator::{alt, cut_err, opt, preceded, separated_pair},
8 error::{ParseError, StrContext, StrContextValue},
9 prelude::*,
10 token::literal,
11 ModalResult,
12};
13
14#[derive(Debug)]
15pub enum IntervalleError {
16 ParseError(String, String, usize),
17}
18
19impl<C> From<ParseError<&str, C>> for IntervalleError
20where
21 C: std::fmt::Display,
22{
23 fn from(ce: ParseError<&str, C>) -> Self {
24 Self::ParseError(
25 format!("{}", ce.inner()).replace("\n", ", "),
26 String::from(*ce.input()),
27 ce.offset(),
28 )
29 }
30}
31
32impl std::fmt::Display for IntervalleError {
33 fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
34 match self {
35 IntervalleError::ParseError(info, input, offset) => {
36 write!(f, "\n |\n{offset:3} | {input}\n | ")?;
37 for _ in 0..*offset {
38 write!(f, " ")?;
39 }
40 write!(f, "^ {info}")
41 }
42 }
43 }
44}
45
46impl Error for IntervalleError {}
47
48#[derive(PartialEq, Debug, Clone)]
49pub enum TimeSpec {
50 After(DateTime),
51 Before(DateTime),
52 Point(DateTime),
53}
54
55fn yesterday(anchor: DateTime) -> DateTime {
56 anchor
57 .date()
58 .midnight()
59 .checked_sub(1.days())
60 .expect("Unreacheable, we allow 4 digit years and the library supports i32")
61}
62
63fn tomorrow(anchor: DateTime) -> DateTime {
64 anchor
65 .date()
66 .midnight()
67 .checked_add(1.days())
68 .expect("Unreacheable, we allow 4 digit years and the library supports i32")
69}
70
71macro_rules! digits {
72 ($len:expr, $dest:ty) => {
73 digit1
74 .verify(|s: &str| s.len() == $len)
75 .try_map(str::parse::<$dest>)
76 .context(StrContext::Label("digit count"))
77 };
78}
79
80macro_rules! date {
81 () => {
82 (
83 digits!(4, u16),
84 preceded(
85 cut_err("-")
86 .context(StrContext::Label("date delimiter"))
87 .context(StrContext::Expected(StrContextValue::CharLiteral('-'))),
88 digits!(2, u8),
89 ),
90 preceded(
91 cut_err("-")
92 .context(StrContext::Label("date delimiter"))
93 .context(StrContext::Expected(StrContextValue::CharLiteral('-'))),
94 digits!(2, u8),
95 ),
96 )
97 .try_map(|(year, month, day)| {
98 time::Date::from_calendar_date(year as i32, time::Month::try_from(month)?, day)
99 })
100 .map(|d| d.midnight())
101 .context(StrContext::Label("date format"))
102 };
103}
104
105macro_rules! time {
106 () => {
107 (
108 digits!(2, u8),
109 preceded(
110 cut_err(":")
111 .context(StrContext::Label("time delimiter"))
112 .context(StrContext::Expected(StrContextValue::CharLiteral(':'))),
113 cut_err(digits!(2, u8)),
114 ),
115 opt(preceded(
116 literal(":")
117 .context(StrContext::Label("time delimiter"))
118 .context(StrContext::Expected(StrContextValue::CharLiteral(':'))),
119 cut_err(digits!(2, u8)),
120 )),
121 )
122 .try_map(|(hour, min, sec)| time::Time::from_hms(hour, min, sec.unwrap_or(0)))
123 };
124}
125
126impl TimeSpec {
127 fn local_offset() -> Result<UtcOffset, Box<dyn Error>> {
129 let time_zone_local = tz::TimeZone::local()?
130 .find_current_local_time_type()?
131 .ut_offset();
132
133 UtcOffset::from_whole_seconds(time_zone_local).map_err(|e| e.into())
134 }
135
136 pub fn parse(timespec: &str) -> Result<TimeSpec, IntervalleError> {
137 let now =
138 OffsetDateTime::now_utc().to_offset(Self::local_offset().unwrap_or(UtcOffset::UTC));
139 let now = DateTime::new(now.date(), now.time());
140 let time_range = TimeRange::parser
141 .parse(timespec)
142 .map_err(IntervalleError::from)?;
143
144 Ok(time_range.evaluate(now))
145 }
146}
147
148#[derive(Debug, Clone)]
149enum TimeRange {
150 Before(TimeRef),
151 After(TimeRef),
152 Point(TimeRef),
153}
154
155#[derive(Debug, Clone)]
156enum TimeRef {
157 Today,
158 Yesterday,
159 Tomorrow,
160 DateTime(DateTime),
161 Date(DateTime),
162 Time(Time),
163}
164
165impl TimeRef {
166 fn evaluate(&self, now: DateTime) -> DateTime {
167 match self {
168 TimeRef::Today => now.date().midnight(),
169 TimeRef::Yesterday => yesterday(now),
170 TimeRef::Tomorrow => tomorrow(now),
171 TimeRef::DateTime(dt) => *dt,
172 TimeRef::Date(d) => *d,
173 TimeRef::Time(t) => now.date().midnight().replace_time(*t),
174 }
175 }
176}
177
178impl TimeRange {
179 fn parser(timespec: &mut &str) -> ModalResult<TimeRange> {
180 (
181 opt(alt(("+", "-"))),
182 alt((
183 literal("today").value(TimeRef::Today),
184 literal("yesterday").value(TimeRef::Yesterday),
185 literal("tomorrow").value(TimeRef::Tomorrow),
186 separated_pair(
187 date!(),
188 literal(" ").context(StrContext::Expected(StrContextValue::CharLiteral(' '))),
189 cut_err(time!()).context(StrContext::Label("time")),
190 )
191 .map(|(pdate, ptime)| TimeRef::DateTime(pdate.replace_time(ptime)))
192 .context(StrContext::Label("time_and_date")),
193 date!().map(TimeRef::Date),
194 time!().map(TimeRef::Time),
195 )),
196 )
197 .context(StrContext::Label("timespec"))
198 .map(|(modifier, dtime)| match modifier {
199 Some("+") => TimeRange::After(dtime),
200 Some("-") => TimeRange::Before(dtime),
201 None => TimeRange::Point(dtime),
202 _ => unreachable!(),
203 })
204 .parse_next(timespec)
205 }
206
207 fn evaluate(&self, now: DateTime) -> TimeSpec {
208 match self {
209 TimeRange::Before(dtime) => TimeSpec::Before(dtime.evaluate(now)),
210 TimeRange::After(dtime) => TimeSpec::After(dtime.evaluate(now)),
211 TimeRange::Point(dtime) => TimeSpec::Point(dtime.evaluate(now)),
212 }
213 }
214}
215
216#[test]
217fn test_parse_today() {
218 insta::assert_debug_snapshot!(TimeRange::parser.parse("today").unwrap());
219}
220
221#[test]
222fn test_evaluate_today() {
223 let target = time::Date::from_calendar_date(2023, time::Month::November, 11)
224 .unwrap()
225 .midnight();
226
227 let anchor = target.replace_time(time::Time::from_hms(12, 20, 45).unwrap());
228 let range = TimeRange::Point(TimeRef::Today);
229
230 assert_eq!(range.evaluate(anchor), TimeSpec::Point(target))
231}
232
233#[test]
234fn test_parse_yesterday() {
235 insta::assert_debug_snapshot!(TimeRange::parser.parse("yesterday").unwrap())
236}
237
238#[test]
239fn test_yesterday() {
240 let target = time::Date::from_calendar_date(2023, time::Month::November, 10)
241 .unwrap()
242 .midnight();
243
244 let anchor = time::Date::from_calendar_date(2023, time::Month::November, 11)
245 .unwrap()
246 .midnight()
247 .replace_time(time::Time::from_hms(12, 20, 45).unwrap());
248 let range = TimeRange::Point(TimeRef::Yesterday);
249
250 assert_eq!(range.evaluate(anchor), TimeSpec::Point(target))
251}
252
253#[test]
254fn test_parse_tomorrow() {
255 insta::assert_debug_snapshot!(TimeRange::parser.parse("tomorrow").unwrap())
256}
257
258#[test]
259fn test_tomorrow() {
260 let target = time::Date::from_calendar_date(2023, time::Month::November, 12)
261 .unwrap()
262 .midnight();
263
264 let anchor = time::Date::from_calendar_date(2023, time::Month::November, 11)
265 .unwrap()
266 .midnight()
267 .replace_time(time::Time::from_hms(12, 20, 45).unwrap());
268 let range = TimeRange::Point(TimeRef::Tomorrow);
269
270 assert_eq!(range.evaluate(anchor), TimeSpec::Point(target))
271}
272
273#[test]
274fn test_parse_date_time() {
275 insta::assert_debug_snapshot!(TimeRange::parser.parse("2024-08-08 14:10:11").unwrap())
276}
277
278#[test]
279fn test_date_time() {
280 let target = time::Date::from_calendar_date(2024, time::Month::August, 08)
281 .unwrap()
282 .midnight()
283 .replace_time(time::Time::from_hms(14, 10, 11).unwrap());
284
285 let anchor = time::Date::from_calendar_date(2023, time::Month::November, 11)
286 .unwrap()
287 .midnight()
288 .replace_time(time::Time::from_hms(12, 20, 45).unwrap());
289
290 let range = TimeRange::Point(TimeRef::DateTime(
291 time::Date::from_calendar_date(2024, time::Month::August, 08)
292 .unwrap()
293 .midnight()
294 .replace_time(time::Time::from_hms(14, 10, 11).unwrap()),
295 ));
296
297 assert_eq!(range.evaluate(anchor), TimeSpec::Point(target))
298}
299
300#[test]
301fn test_parse_date_time_no_sec() {
302 insta::assert_debug_snapshot!(TimeRange::parser.parse("2024-08-08 14:10").unwrap())
303}
304
305#[test]
306fn test_date_time_no_sec() {
307 let target = time::Date::from_calendar_date(2024, time::Month::August, 08)
308 .unwrap()
309 .midnight()
310 .replace_time(time::Time::from_hms(14, 10, 00).unwrap());
311
312 let anchor = time::Date::from_calendar_date(2023, time::Month::November, 11)
313 .unwrap()
314 .midnight()
315 .replace_time(time::Time::from_hms(12, 20, 45).unwrap());
316
317 let range = TimeRange::Point(TimeRef::DateTime(
318 time::Date::from_calendar_date(2024, time::Month::August, 08)
319 .unwrap()
320 .midnight()
321 .replace_time(time::Time::from_hms(14, 10, 00).unwrap()),
322 ));
323
324 assert_eq!(range.evaluate(anchor), TimeSpec::Point(target))
325}
326
327#[test]
328fn test_parse_date() {
329 insta::assert_debug_snapshot!(TimeRange::parser.parse("2024-08-08").unwrap())
330}
331
332#[test]
333fn test_date() {
334 let target = time::Date::from_calendar_date(2024, time::Month::August, 08)
335 .unwrap()
336 .midnight();
337
338 let anchor = time::Date::from_calendar_date(2023, time::Month::November, 11)
339 .unwrap()
340 .midnight()
341 .replace_time(time::Time::from_hms(12, 20, 45).unwrap());
342
343 let range = TimeRange::Point(TimeRef::DateTime(
344 time::Date::from_calendar_date(2024, time::Month::August, 08)
345 .unwrap()
346 .midnight(),
347 ));
348
349 assert_eq!(range.evaluate(anchor), TimeSpec::Point(target))
350}
351
352#[test]
353fn test_parse_time() {
354 insta::assert_debug_snapshot!(TimeRange::parser.parse("15:28:59").unwrap())
355}
356
357#[test]
358fn test_time() {
359 let target = time::Date::from_calendar_date(2024, time::Month::August, 08)
360 .unwrap()
361 .midnight()
362 .replace_time(time::Time::from_hms(15, 28, 59).unwrap());
363
364 let anchor = target.replace_time(time::Time::from_hms(12, 20, 45).unwrap());
365
366 let range = TimeRange::Point(TimeRef::Time(time::Time::from_hms(15, 28, 59).unwrap()));
367
368 assert_eq!(range.evaluate(anchor), TimeSpec::Point(target))
369}
370
371#[test]
372fn test_parse_time_no_sec() {
373 insta::assert_debug_snapshot!(TimeRange::parser.parse("15:28").unwrap())
374}
375
376#[test]
377fn test_time_no_sec() {
378 let target = time::Date::from_calendar_date(2024, time::Month::August, 08)
379 .unwrap()
380 .midnight()
381 .replace_time(time::Time::from_hms(15, 28, 00).unwrap());
382
383 let anchor = target.replace_time(time::Time::from_hms(12, 20, 45).unwrap());
384
385 let range = TimeRange::Point(TimeRef::Time(time::Time::from_hms(15, 28, 00).unwrap()));
386
387 assert_eq!(range.evaluate(anchor), TimeSpec::Point(target))
388}
389
390#[test]
391fn test_parse_before_time_no_sec() {
392 insta::assert_debug_snapshot!(TimeRange::parser.parse("-15:28").unwrap())
393}
394
395#[test]
396fn test_parse_after_time_no_sec() {
397 insta::assert_debug_snapshot!(TimeRange::parser.parse("+15:28").unwrap())
398}