intervalle/
lib.rs

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    /// Figuring out the system's local timezone
128    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}