smart_date/
lib.rs

1#![warn(clippy::all, clippy::pedantic, clippy::unwrap_used)]
2use chrono::{Datelike, Days, NaiveDate, Weekday as ChronoWeekday};
3use parser::{parse_flex_date, parse_flex_date_exact};
4use std::ops::Range;
5
6mod parser;
7
8/// Represents some data that has been parsed out of a string.
9/// Contains the data that was extracted as well as the location in
10/// the input string of the substring that was related to the data.
11pub struct Parsed<T> {
12    pub data: T,
13
14    // TODO: consider storing a substring instead, then provide a method to
15    // compute the offset.
16    // see https://stackoverflow.com/questions/67148359/check-if-a-str-is-a-sub-slice-of-another-str
17    pub range: Range<usize>,
18}
19
20/// Represents a relative (or, eventually, absolute) date.
21///
22/// # Examples
23/// Here are of input strings that will eventually be supported.
24/// See [the Todoist docs](https://todoist.com/help/articles/introduction-to-due-dates-and-due-times-q7VobO).
25/// - [x] "today", "tod"
26/// - [x] "tomorrow", "tom", "tmrw"
27/// - [x] "wednesday", "wed" (any weekday)
28/// - [ ] "next week"
29/// - [ ] "this weekend"
30/// - [ ] "next weekend"
31/// - [ ] "in 3 days", "in three days"
32/// - [ ] "in 2 weeks", "in two weeks"
33/// - [ ] "2 weeks from now"
34/// - [ ] "in four months"
35/// - [ ] "in one year"
36/// - [ ] "next month"
37/// - [ ] "january 27", "jan 27", "01/27"
38/// - [ ] "jan 27 2024", "01/27/2024"
39/// - [ ] "27th"
40/// - [ ] "mid january"
41/// - [ ] "mid jan"
42/// - [ ] "later this week"
43/// - [ ] "two weeks from tomorrow"
44#[derive(Clone, Debug, PartialEq, Eq)]
45pub enum FlexibleDate {
46    Today,
47    Tomorrow,
48    Weekday(Weekday),
49}
50
51#[derive(Clone, Debug, PartialEq, Eq)]
52pub enum Weekday {
53    Monday,
54    Tuesday,
55    Wednesday,
56    Thursday,
57    Friday,
58    Saturday,
59    Sunday,
60}
61
62impl From<ChronoWeekday> for Weekday {
63    fn from(day: ChronoWeekday) -> Self {
64        match day {
65            ChronoWeekday::Mon => Weekday::Monday,
66            ChronoWeekday::Tue => Weekday::Tuesday,
67            ChronoWeekday::Wed => Weekday::Wednesday,
68            ChronoWeekday::Thu => Weekday::Thursday,
69            ChronoWeekday::Fri => Weekday::Friday,
70            ChronoWeekday::Sat => Weekday::Saturday,
71            ChronoWeekday::Sun => Weekday::Sunday,
72        }
73    }
74}
75
76impl Weekday {
77    fn week_index(&self) -> u64 {
78        match self {
79            Weekday::Monday => 0,
80            Weekday::Tuesday => 1,
81            Weekday::Wednesday => 2,
82            Weekday::Thursday => 3,
83            Weekday::Friday => 4,
84            Weekday::Saturday => 5,
85            Weekday::Sunday => 6,
86        }
87    }
88
89    #[must_use]
90    pub fn days_until(&self, day: &Self) -> u64 {
91        let day_index = day.week_index();
92        let self_index = self.week_index();
93        (7 + day_index - self_index) % 7
94    }
95}
96
97impl FlexibleDate {
98    /// Parses a `FlexibleDate` from within a string. Fails (returns `None`) if the full string does
99    /// not match a date.
100    ///
101    ///
102    /// ```rust
103    /// # use smart_date::FlexibleDate;
104    /// # fn main() {
105    /// let result1 = FlexibleDate::parse_from_str("today").unwrap();
106    /// assert_eq!(result1, FlexibleDate::Today);
107    ///
108    /// let result2 = FlexibleDate::parse_from_str("tom").unwrap();
109    /// assert_eq!(result2, FlexibleDate::Tomorrow);
110    ///
111    /// let result3 = FlexibleDate::parse_from_str("go to the store today");
112    /// assert_eq!(result3, None);
113    ///  # }
114    /// ```
115    #[must_use]
116    pub fn parse_from_str(text: &str) -> Option<FlexibleDate> {
117        parse_flex_date_exact(text).ok().map(|(_, date)| date)
118    }
119
120    /// Finds and parses a `FlexibleDate` from within a string. The returned `Parsed<>` type contains
121    /// the date that was parsed as well as the location of the matching substring in the input.
122    ///
123    ///
124    /// ```rust
125    /// # use smart_date::FlexibleDate;
126    /// # fn main() {
127    /// let result1 = FlexibleDate::find_and_parse_in_str("go to the store today").unwrap();
128    /// assert_eq!(result1.data, FlexibleDate::Today);
129    /// assert_eq!(result1.range, (16..21));
130    ///
131    /// let result2 = FlexibleDate::find_and_parse_in_str("do a barrel tom okay?").unwrap();
132    /// assert_eq!(result2.data, FlexibleDate::Tomorrow);
133    /// assert_eq!(result2.range, (12..15));
134    ///  # }
135    /// ```
136    #[must_use]
137    pub fn find_and_parse_in_str(text: &str) -> Option<Parsed<FlexibleDate>> {
138        parse_flex_date(text)
139    }
140
141    /// Converts the `FlexibleDate` into a [`NaiveDate`].
142    ///
143    /// ```rust
144    /// # use smart_date::FlexibleDate;
145    /// # use smart_date::Weekday;
146    /// # use chrono::Datelike;
147    /// # fn main() {
148    /// let today = chrono::NaiveDate::parse_from_str("2023-10-08", "%Y-%m-%d").unwrap();
149    ///
150    /// let date = FlexibleDate::Today.into_naive_date(today);
151    /// assert_eq!(date.month(), 10);
152    /// assert_eq!(date.day(), 8);
153    /// assert_eq!(date.year(), 2023);
154    ///
155    /// let date = FlexibleDate::Tomorrow.into_naive_date(today);
156    /// assert_eq!(date.month(), 10);
157    /// assert_eq!(date.day(), 9);
158    /// assert_eq!(date.year(), 2023);
159    ///
160    /// let date = FlexibleDate::Weekday(Weekday::Wednesday).into_naive_date(today);
161    /// // 10/08/23 was a Sunday, 10/11 was the following Wednesday
162    /// assert_eq!(date.month(), 10);
163    /// assert_eq!(date.day(), 11);
164    /// assert_eq!(date.year(), 2023);
165    /// # }
166    /// ```
167    #[must_use]
168    pub fn into_naive_date(self, today: NaiveDate) -> NaiveDate {
169        match self {
170            FlexibleDate::Today => today,
171            FlexibleDate::Tomorrow => today + Days::new(1),
172            FlexibleDate::Weekday(day) => {
173                let weekday: Weekday = today.weekday().into();
174                today + Days::new(weekday.days_until(&day))
175            }
176        }
177    }
178}
179
180#[cfg(test)]
181mod weekday_tests {
182    use super::*;
183
184    #[test]
185    fn test_days_until() {
186        let today = Weekday::Tuesday;
187        assert_eq!(today.days_until(&Weekday::Wednesday), 1);
188        assert_eq!(today.days_until(&Weekday::Tuesday), 0);
189        assert_eq!(today.days_until(&Weekday::Monday), 6);
190    }
191}