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}