natural_date_parser/
lib.rs

1use pest_derive::Parser;
2use thiserror::Error;
3
4/// A parser for date-related expressions using the `pest` parser library.
5#[derive(Parser)]
6#[grammar = "./grammar.pest"]
7pub struct DateParser;
8
9/// Enum representing errors that can occur while parsing a date.
10#[derive(Debug, Error)]
11pub enum ParseDateError {
12    /// Error variant for failed date parsing. Includes the error message.
13    #[error("Failed to parse date:\n{0}")]
14    ParseError(String),
15}
16
17/// Module for parsing and processing date-related expressions.
18pub mod date_parser {
19    use crate::{DateParser, ParseDateError, Rule};
20    use chrono::{DateTime, Datelike, Duration, Local, TimeZone, Weekday};
21    use chronoutil::delta::shift_months_opt;
22    use pest::iterators::Pair;
23    use pest::Parser;
24
25    /// Parses a string representing a date and returns the corresponding `DateTime<Local>`.
26    ///
27    /// This function takes a date string, parses it using the `pest` parser, and returns the
28    /// resulting `DateTime<Local>` if successful, or an error if the string cannot be parsed.
29    ///
30    /// # Arguments
31    /// * `string` - The string to be parsed as a date.
32    ///
33    /// # Returns
34    /// * `Result<DateTime<Local>, ParseDateError>` - A `DateTime<Local>` if parsing is successful,
35    ///   or a `ParseDateError` if there was an issue.
36    pub fn from_string(string: &str) -> Result<DateTime<Local>, ParseDateError> {
37        let pairs = DateParser::parse(Rule::date_expression, string)
38            .map_err(|e| ParseDateError::ParseError(e.to_string()))?;
39
40        if let Some(pair) = pairs.clone().next() {
41            match pair.as_rule() {
42                Rule::date_expression => {
43                    let datetime = process_date_expression(pair)?;
44                    return Ok(datetime);
45                }
46                _ => {
47                    return Err(ParseDateError::ParseError(
48                        "Unexpected rule encountered".to_string(),
49                    ));
50                }
51            }
52        }
53
54        Err(ParseDateError::ParseError(
55            "No valid date expression found".to_string(),
56        ))
57    }
58
59    pub fn process_date_expression(
60        pair: Pair<'_, Rule>,
61    ) -> Result<DateTime<Local>, ParseDateError> {
62        let datetime = Local::now();
63        for inner_pair in pair.into_inner() {
64            match inner_pair.as_rule() {
65                Rule::relative_date => {
66                    let parsed = process_relative_date(inner_pair)?;
67                    return Ok(parsed);
68                }
69                Rule::relative_term => {
70                    let parsed = process_relative_term(inner_pair)?;
71                    return Ok(parsed);
72                }
73                Rule::specific_time => {
74                    let parsed = process_specific_time(inner_pair, datetime)?;
75                    return Ok(parsed);
76                }
77                Rule::specific_day => {
78                    if let Some(inner) = inner_pair.into_inner().next() {
79                        let parsed = process_specific_day(inner.as_rule(), datetime)?;
80                        return Ok(parsed);
81                    }
82                }
83                Rule::specific_day_and_time => {
84                    let parsed = process_specific_day_and_time(inner_pair)?;
85                    return Ok(parsed);
86                }
87                Rule::relative_day_and_specific_time => {
88                    let parsed = process_relative_day_and_specific_time(inner_pair)?;
89                    return Ok(parsed);
90                }
91                Rule::future_time => {
92                    let parsed = process_future_time(inner_pair)?;
93                    return Ok(parsed);
94                }
95                _ => {
96                    return Err(ParseDateError::ParseError(
97                        "Unexpected rule encountered".to_string(),
98                    ));
99                }
100            }
101        }
102
103        Err(ParseDateError::ParseError(
104            "No date expression found".to_string(),
105        ))
106    }
107
108    pub fn process_future_time(pair: Pair<'_, Rule>) -> Result<DateTime<Local>, ParseDateError> {
109        let mut datetime = Local::now();
110        let mut duration = 0;
111        let mut unit: Option<Rule> = None;
112
113        for inner_pair in pair.into_inner() {
114            match inner_pair.as_rule() {
115                Rule::number => {
116                    duration = inner_pair.as_str().trim().parse::<i32>().map_err(|_| {
117                        ParseDateError::ParseError("Invalid duration value".to_string())
118                    })?;
119                }
120                Rule::time_unit => {
121                    unit = Some(inner_pair.as_rule());
122                }
123                _ => {
124                    return Err(ParseDateError::ParseError("Unexpected rule".to_string()));
125                }
126            }
127        }
128
129        if let Some(unit) = unit {
130            datetime = match unit {
131                Rule::day_s => datetime + Duration::days(duration as i64),
132                Rule::week_s => datetime + Duration::weeks(duration as i64),
133                Rule::month_s => shift_months_opt(datetime, duration).ok_or_else(|| {
134                    ParseDateError::ParseError("Invalid month adjustment".to_string())
135                })?,
136                Rule::year_s => {
137                    datetime
138                        .with_year(datetime.year() + duration)
139                        .ok_or_else(|| {
140                            ParseDateError::ParseError("Invalid year adjustment".to_string())
141                        })?
142                }
143                _ => {
144                    return Err(ParseDateError::ParseError("Invalid time unit".to_string()));
145                }
146            };
147            Ok(datetime)
148        } else {
149            Err(ParseDateError::ParseError(
150                "Time unit not provided".to_string(),
151            ))
152        }
153    }
154
155    pub fn process_specific_day_and_time(
156        pair: Pair<'_, Rule>,
157    ) -> Result<DateTime<Local>, ParseDateError> {
158        let mut datetime = Local::now();
159        for inner_pair in pair.into_inner() {
160            match inner_pair.as_rule() {
161                Rule::specific_day => {
162                    datetime = process_specific_day(inner_pair.as_rule(), datetime)?;
163                }
164                Rule::specific_time => {
165                    datetime = process_specific_time(inner_pair, datetime)?;
166                }
167                _ => {
168                    return Err(ParseDateError::ParseError(format!(
169                        "Unexpected rule in specific date and time: {:?}",
170                        inner_pair.as_rule()
171                    )));
172                }
173            }
174        }
175        Ok(datetime)
176    }
177
178    pub fn process_relative_day_and_specific_time(
179        pair: Pair<'_, Rule>,
180    ) -> Result<DateTime<Local>, ParseDateError> {
181        let mut datetime = Local::now();
182        for inner_pair in pair.into_inner() {
183            match inner_pair.as_rule() {
184                Rule::relative_date => {
185                    datetime = process_relative_date(inner_pair)?;
186                }
187                Rule::relative_term => {
188                    datetime = process_relative_term(inner_pair)?;
189                }
190                Rule::specific_time => {
191                    datetime = process_specific_time(inner_pair, datetime)?;
192                }
193                _ => {}
194            }
195        }
196        Ok(datetime)
197    }
198
199    pub fn process_relative_date(pair: Pair<'_, Rule>) -> Result<DateTime<Local>, ParseDateError> {
200        let datetime = Local::now();
201        let inner_pairs: Vec<_> = pair.clone().into_inner().collect();
202
203        if inner_pairs.len() == 2 {
204            let first_pair = &inner_pairs[0];
205            let second_pair = &inner_pairs[1];
206
207            if first_pair.as_rule() == Rule::next_or_last
208                && second_pair.as_rule() == Rule::specific_day
209            {
210                let direction = first_pair.clone().into_inner().last().unwrap().as_rule();
211
212                if let Some(inner_pair) = second_pair.clone().into_inner().next() {
213                    match process_weekday(inner_pair.as_rule()) {
214                        Ok(target_weekday) => {
215                            return shift_to_weekday(datetime, target_weekday, direction);
216                        }
217                        Err(e) => {
218                            return Err(ParseDateError::ParseError(format!(
219                                "Unrecognized relative date: {:?}",
220                                e.to_string()
221                            )));
222                        }
223                    }
224                }
225
226                Err(ParseDateError::ParseError(format!(
227                    "Unrecognized relative date: {:?}",
228                    second_pair.to_string()
229                )))
230            } else {
231                Err(ParseDateError::ParseError(
232                    "Pair did not match expected structure for relative_date.".to_string(),
233                ))
234            }
235        } else {
236            Err(ParseDateError::ParseError(
237                "Unexpected number of inner pairs in relative_date.".to_string(),
238            ))
239        }
240    }
241
242    pub fn process_relative_term(pair: Pair<'_, Rule>) -> Result<DateTime<Local>, ParseDateError> {
243        let datetime = Local::now();
244
245        if let Some(inner_pair) = pair.clone().into_inner().next() {
246            match inner_pair.as_rule() {
247                Rule::tomorrow => {
248                    return Ok(datetime + Duration::days(1));
249                }
250                Rule::today => {
251                    return Ok(datetime);
252                }
253                Rule::yesterday => {
254                    return Ok(datetime - Duration::days(1));
255                }
256                _ => {
257                    return Err(ParseDateError::ParseError(format!(
258                        "Unexpected relative term: {:?}",
259                        pair
260                    )));
261                }
262            }
263        }
264
265        Err(ParseDateError::ParseError(
266            "Invalid relative term".to_string(),
267        ))
268    }
269
270    pub fn process_specific_time(
271        pair: Pair<'_, Rule>,
272        datetime: DateTime<Local>,
273    ) -> Result<DateTime<Local>, ParseDateError> {
274        println!("some print");
275        let mut hour: u32 = 0;
276        let mut minute: u32 = 0;
277        let mut is_pm = false;
278
279        // Iterate through inner pairs to capture hour, minute, and am_pm
280        for inner_pair in pair.into_inner() {
281            match inner_pair.as_rule() {
282                Rule::hour => {
283                    hour = inner_pair.as_str().parse::<u32>().map_err(|e| {
284                        ParseDateError::ParseError(format!("Failed to parse hour: {}", e))
285                    })?;
286
287                    if hour > 23 {
288                        return Err(ParseDateError::ParseError(format!(
289                            "Invalid hour: {:?}",
290                            hour
291                        )));
292                    }
293                }
294                Rule::minute => {
295                    minute = inner_pair.as_str().parse::<u32>().map_err(|e| {
296                        ParseDateError::ParseError(format!("Failed to parse minute: {}", e))
297                    })?;
298                }
299                Rule::am_pm => {
300                    if let Some(res) = process_is_pm(inner_pair) {
301                        is_pm = res;
302                    }
303                }
304                _ => {
305                    return Err(ParseDateError::ParseError(
306                        "Unexpected rule in specific_time".to_string(),
307                    ));
308                }
309            }
310        }
311
312        if is_pm && hour < 12 {
313            hour += 12;
314        } else if !is_pm && hour == 12 {
315            println!("here");
316            hour = 0;
317        }
318
319        let modified_datetime = change_time(datetime, hour, minute)?;
320
321        Ok(modified_datetime)
322    }
323
324    pub fn process_specific_day(
325        rule: Rule,
326        datetime: DateTime<Local>,
327    ) -> Result<DateTime<Local>, ParseDateError> {
328        let target_weekday = process_weekday(rule)?;
329        let current_weekday = datetime.weekday();
330
331        let target_day_num = target_weekday.num_days_from_sunday();
332        let current_day_num = current_weekday.num_days_from_sunday();
333
334        let days_difference = if target_day_num >= current_day_num {
335            (target_day_num - current_day_num) as i64
336        } else {
337            -((current_day_num - target_day_num) as i64)
338        };
339
340        let target_date = datetime + Duration::days(days_difference);
341        Ok(target_date)
342    }
343
344    pub fn process_weekday(day: Rule) -> Result<Weekday, ParseDateError> {
345        match day {
346            Rule::monday => Ok(Weekday::Mon),
347            Rule::tuesday => Ok(Weekday::Tue),
348            Rule::wednesday => Ok(Weekday::Wed),
349            Rule::thursday => Ok(Weekday::Thu),
350            Rule::friday => Ok(Weekday::Fri),
351            Rule::saturday => Ok(Weekday::Sat),
352            Rule::sunday => Ok(Weekday::Sun),
353            _ => Err(ParseDateError::ParseError(format!(
354                "Invalid weekday: {:?}",
355                day
356            ))),
357        }
358    }
359
360    pub fn change_time(
361        datetime: DateTime<Local>,
362        hour: u32,
363        minute: u32,
364    ) -> Result<DateTime<Local>, ParseDateError> {
365        match Local.with_ymd_and_hms(
366            datetime.year(),
367            datetime.month(),
368            datetime.day(),
369            hour,
370            minute,
371            0,
372        ) {
373            chrono::LocalResult::Single(new_datetime) => Ok(new_datetime),
374            chrono::LocalResult::None => Err(ParseDateError::ParseError(
375                "Invalid date or time components".to_string(),
376            )),
377            chrono::LocalResult::Ambiguous(_, _) => Err(ParseDateError::ParseError(
378                "Ambiguous date and time".to_string(),
379            )),
380        }
381    }
382
383    pub fn shift_to_weekday(
384        now: DateTime<Local>,
385        target_weekday: Weekday,
386        direction: Rule,
387    ) -> Result<DateTime<Local>, ParseDateError> {
388        let current_weekday = now.weekday();
389
390        let num_from_curr = current_weekday.num_days_from_sunday() as i32;
391        let num_from_target = target_weekday.num_days_from_sunday() as i32;
392
393        let days_difference: i32 = match direction {
394            Rule::next => {
395                if num_from_target == 0 {
396                    7 - num_from_curr + 7
397                } else {
398                    7 - num_from_curr + num_from_target
399                }
400            }
401
402            Rule::last => {
403                if num_from_target == 0 {
404                    -num_from_curr
405                } else {
406                    -num_from_curr - 7 + num_from_target
407                }
408            }
409            Rule::this => {
410                let diff = (num_from_target as i64) - (num_from_curr as i64);
411                if diff >= 0 {
412                    diff as i32
413                } else {
414                    (diff + 7) as i32
415                }
416            }
417            _ => -100,
418        };
419
420        if days_difference < -7 {
421            return Err(ParseDateError::ParseError(format!(
422                "Expected last, this or next, got {:?}",
423                direction
424            )));
425        }
426
427        println!("days_difference {:?}", days_difference);
428
429        Ok(now + Duration::days(days_difference as i64))
430    }
431
432    pub fn process_is_pm(pair: Pair<'_, Rule>) -> Option<bool> {
433        if let Some(inner_pair) = pair.into_inner().next() {
434            if inner_pair.as_rule() == Rule::pm {
435                return Some(true);
436            } else if inner_pair.as_rule() == Rule::am {
437                return Some(false);
438            } else {
439                return None;
440            }
441        }
442        None
443    }
444}