natural_date_rs/
lib.rs

1use {pest_derive::Parser, thiserror::Error};
2
3/// A parser for date-related expressions using the `pest` parser library.
4#[derive(Parser)]
5#[grammar = "./grammar.pest"]
6pub struct DateParser;
7
8/// Enum representing errors that can occur while parsing a date.
9#[derive(Debug, Error)]
10pub enum ParseDateError {
11    /// Error variant for failed date parsing. Includes the error message.
12    #[error("Failed to parse date: {0}")]
13    ParseError(String),
14}
15
16/// Module for parsing and processing date-related expressions.
17pub mod date_parser {
18    use {
19        crate::{DateParser, ParseDateError, Rule},
20        chrono::{DateTime, Datelike, Duration, Local, TimeZone, Weekday},
21        chronoutil::delta::shift_months_opt,
22        pest::{Parser, iterators::Pair},
23    };
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    /// The reference date is automatically assumed to be Local::now().
31    ///
32    /// # Arguments
33    /// * `string` - The string to be parsed as a date.
34    ///
35    /// # Returns
36    /// * `Result<DateTime<Local>, ParseDateError>` - A `DateTime<Local>` if parsing is successful,
37    ///   or a `ParseDateError` if there was an issue.
38    pub fn from_string(string: &str) -> Result<DateTime<Local>, ParseDateError> {
39        from_string_with_reference(string, Local::now())
40    }
41
42    /// Parses a string representing a date and returns the corresponding `DateTime<Local>`.
43    ///
44    /// This function takes a date string, parses it using the `pest` parser, and returns the
45    /// resulting `DateTime<Local>` if successful, or an error if the string cannot be parsed.
46    ///
47    /// You specify a reference date from which to calculate relative dates. This allows for
48    /// easier testing and better use in multi-timezone setups.
49    ///
50    /// # Arguments
51    /// * `string` - The string to be parsed as a date.
52    /// * `reference_date` - The DateTime representing "now", a moment from which relative dates and times will be calculated.
53    ///
54    /// # Returns
55    /// * `Result<DateTime<Local>, ParseDateError>` - A `DateTime<Local>` if parsing is successful,
56    ///   or a `ParseDateError` if there was an issue.
57    pub fn from_string_with_reference(
58        string: &str,
59        reference_date: DateTime<Local>,
60    ) -> Result<DateTime<Local>, ParseDateError> {
61        let pairs = DateParser::parse(Rule::date_expression, string)
62            .map_err(|e| ParseDateError::ParseError(e.to_string()))?;
63
64        if let Some(pair) = pairs.clone().next() {
65            match pair.as_rule() {
66                Rule::date_expression => {
67                    let datetime = process_date_expression(pair, reference_date)?;
68                    return Ok(datetime);
69                }
70                _ => {
71                    return Err(ParseDateError::ParseError(
72                        "Unexpected rule encountered".to_string(),
73                    ));
74                }
75            }
76        }
77
78        Err(ParseDateError::ParseError(
79            "No valid date expression found".to_string(),
80        ))
81    }
82
83    pub fn process_date_expression(
84        pair: Pair<'_, Rule>,
85        datetime: DateTime<Local>,
86    ) -> Result<DateTime<Local>, ParseDateError> {
87        for inner_pair in pair.into_inner() {
88            match inner_pair.as_rule() {
89                Rule::relative_date => {
90                    let parsed = process_relative_date(inner_pair, datetime)?;
91                    return Ok(parsed);
92                }
93                Rule::relative_term => {
94                    let parsed = process_relative_term(inner_pair, datetime)?;
95                    return Ok(parsed);
96                }
97                Rule::specific_date_and_time => {
98                    let parsed = process_specific_date_and_time(inner_pair, datetime)?;
99                    return Ok(parsed);
100                }
101                Rule::specific_date => {
102                    let parsed = process_specific_date(inner_pair, datetime)?;
103                    return Ok(parsed);
104                }
105                Rule::specific_time => {
106                    let parsed = process_specific_time(inner_pair, datetime)?;
107                    return Ok(parsed);
108                }
109                Rule::specific_day => {
110                    if let Some(inner) = inner_pair.into_inner().next() {
111                        let parsed = process_specific_day(inner.as_rule(), datetime)?;
112                        return Ok(parsed);
113                    }
114                }
115                Rule::specific_day_and_time => {
116                    let parsed = process_specific_day_and_time(inner_pair, datetime)?;
117                    return Ok(parsed);
118                }
119                Rule::relative_day_and_specific_time => {
120                    let parsed = process_relative_day_and_specific_time(inner_pair, datetime)?;
121                    return Ok(parsed);
122                }
123                Rule::future_time => {
124                    let parsed = process_future_time(inner_pair, datetime)?;
125                    return Ok(parsed);
126                }
127                _ => {
128                    return Err(ParseDateError::ParseError(
129                        "Unexpected rule encountered in date expression".to_string(),
130                    ));
131                }
132            }
133        }
134
135        Err(ParseDateError::ParseError(
136            "No date expression found".to_string(),
137        ))
138    }
139
140    pub fn process_future_time(
141        pair: Pair<'_, Rule>,
142        mut datetime: DateTime<Local>,
143    ) -> Result<DateTime<Local>, ParseDateError> {
144        let mut duration = 0;
145        let mut unit: Option<Rule> = None;
146
147        for inner_pair in pair.into_inner() {
148            match inner_pair.as_rule() {
149                Rule::number => {
150                    duration = inner_pair.as_str().trim().parse::<i32>().map_err(|_| {
151                        ParseDateError::ParseError("Invalid duration value".to_string())
152                    })?;
153                }
154                Rule::minute_s
155                | Rule::hour_s
156                | Rule::day_s
157                | Rule::week_s
158                | Rule::month_s
159                | Rule::year_s => {
160                    unit = Some(inner_pair.as_rule());
161                }
162                _ => {
163                    return Err(ParseDateError::ParseError("Unexpected rule".to_string()));
164                }
165            }
166        }
167
168        if let Some(unit) = unit {
169            datetime = match unit {
170                Rule::minute_s => datetime + Duration::minutes(duration as i64),
171                Rule::hour_s => datetime + Duration::hours(duration as i64),
172                Rule::day_s => datetime + Duration::days(duration as i64),
173                Rule::week_s => datetime + Duration::weeks(duration as i64),
174                Rule::month_s => shift_months_opt(datetime, duration).ok_or_else(|| {
175                    ParseDateError::ParseError("Invalid month adjustment".to_string())
176                })?,
177                Rule::year_s => {
178                    datetime
179                        .with_year(datetime.year() + duration)
180                        .ok_or_else(|| {
181                            ParseDateError::ParseError("Invalid year adjustment".to_string())
182                        })?
183                }
184                _ => {
185                    return Err(ParseDateError::ParseError("Invalid time unit".to_string()));
186                }
187            };
188            Ok(datetime)
189        } else {
190            Err(ParseDateError::ParseError(
191                "Time unit not provided".to_string(),
192            ))
193        }
194    }
195
196    pub fn process_specific_date_and_time(
197        pair: Pair<'_, Rule>,
198        mut datetime: DateTime<Local>,
199    ) -> Result<DateTime<Local>, ParseDateError> {
200        for inner_pair in pair.into_inner() {
201            match inner_pair.as_rule() {
202                Rule::specific_date => {
203                    datetime = process_specific_date(inner_pair, datetime)?;
204                }
205                Rule::specific_time => {
206                    datetime = process_specific_time(inner_pair, datetime)?;
207                }
208                _ => {
209                    return Err(ParseDateError::ParseError(format!(
210                        "Unexpected rule in specific date and time: {:?}",
211                        inner_pair.as_rule()
212                    )));
213                }
214            }
215        }
216        Ok(datetime)
217    }
218
219    pub fn process_specific_day_and_time(
220        pair: Pair<'_, Rule>,
221        mut datetime: DateTime<Local>,
222    ) -> Result<DateTime<Local>, ParseDateError> {
223        for inner_pair in pair.into_inner() {
224            match inner_pair.as_rule() {
225                Rule::specific_day => {
226                    datetime = process_specific_day(inner_pair.as_rule(), datetime)?;
227                }
228                Rule::specific_time => {
229                    datetime = process_specific_time(inner_pair, datetime)?;
230                }
231                _ => {
232                    return Err(ParseDateError::ParseError(format!(
233                        "Unexpected rule in specific date and time: {:?}",
234                        inner_pair.as_rule()
235                    )));
236                }
237            }
238        }
239        Ok(datetime)
240    }
241
242    pub fn process_relative_day_and_specific_time(
243        pair: Pair<'_, Rule>,
244        mut datetime: DateTime<Local>,
245    ) -> Result<DateTime<Local>, ParseDateError> {
246        for inner_pair in pair.into_inner() {
247            match inner_pair.as_rule() {
248                Rule::relative_date => {
249                    datetime = process_relative_date(inner_pair, datetime)?;
250                }
251                Rule::relative_term => {
252                    datetime = process_relative_term(inner_pair, datetime)?;
253                }
254                Rule::specific_time => {
255                    datetime = process_specific_time(inner_pair, datetime)?;
256                }
257                _ => {}
258            }
259        }
260        Ok(datetime)
261    }
262
263    pub fn process_relative_date(
264        pair: Pair<'_, Rule>,
265        datetime: DateTime<Local>,
266    ) -> Result<DateTime<Local>, ParseDateError> {
267        let inner_pairs: Vec<_> = pair.clone().into_inner().collect();
268
269        if inner_pairs.len() == 2 {
270            let first_pair = &inner_pairs[0];
271            let second_pair = &inner_pairs[1];
272
273            if first_pair.as_rule() == Rule::next_or_last
274                && second_pair.as_rule() == Rule::specific_day
275            {
276                let direction = first_pair.clone().into_inner().last().unwrap().as_rule();
277
278                if let Some(inner_pair) = second_pair.clone().into_inner().next() {
279                    match process_weekday(inner_pair.as_rule()) {
280                        Ok(target_weekday) => {
281                            return shift_to_weekday(datetime, target_weekday, direction);
282                        }
283                        Err(e) => {
284                            return Err(ParseDateError::ParseError(format!(
285                                "Unrecognized relative date: {:?}",
286                                e.to_string()
287                            )));
288                        }
289                    }
290                }
291
292                Err(ParseDateError::ParseError(format!(
293                    "Unrecognized relative date: {:?}",
294                    second_pair.to_string()
295                )))
296            } else {
297                Err(ParseDateError::ParseError(
298                    "Pair did not match expected structure for relative_date.".to_string(),
299                ))
300            }
301        } else {
302            Err(ParseDateError::ParseError(
303                "Unexpected number of inner pairs in relative_date.".to_string(),
304            ))
305        }
306    }
307
308    pub fn process_relative_term(
309        pair: Pair<'_, Rule>,
310        datetime: DateTime<Local>,
311    ) -> Result<DateTime<Local>, ParseDateError> {
312        if let Some(inner_pair) = pair.clone().into_inner().next() {
313            match inner_pair.as_rule() {
314                Rule::tomorrow => {
315                    return Ok(datetime + Duration::days(1));
316                }
317                Rule::today => {
318                    return Ok(datetime);
319                }
320                Rule::yesterday => {
321                    return Ok(datetime - Duration::days(1));
322                }
323                _ => {
324                    return Err(ParseDateError::ParseError(format!(
325                        "Unexpected relative term: {:?}",
326                        pair
327                    )));
328                }
329            }
330        }
331
332        Err(ParseDateError::ParseError(
333            "Invalid relative term".to_string(),
334        ))
335    }
336
337    pub fn process_specific_time(
338        pair: Pair<'_, Rule>,
339        datetime: DateTime<Local>,
340    ) -> Result<DateTime<Local>, ParseDateError> {
341        let mut hour: u32 = 0;
342        let mut minute: u32 = 0;
343        let mut is_pm = false;
344
345        // Iterate through inner pairs to capture hour, minute, and am_pm
346        for inner_pair in pair.into_inner() {
347            match inner_pair.as_rule() {
348                Rule::hour => {
349                    hour = inner_pair.as_str().parse::<u32>().map_err(|e| {
350                        ParseDateError::ParseError(format!(
351                            "Failed to parse hour '{}': {e}",
352                            inner_pair.as_str()
353                        ))
354                    })?;
355
356                    if hour > 23 {
357                        return Err(ParseDateError::ParseError(format!(
358                            "Invalid hour: {hour:?}"
359                        )));
360                    }
361                }
362                Rule::minute => {
363                    minute = inner_pair.as_str().parse::<u32>().map_err(|e| {
364                        ParseDateError::ParseError(format!(
365                            "Failed to parse minute '{}': {e}",
366                            inner_pair.as_str()
367                        ))
368                    })?;
369                }
370                Rule::am_pm => {
371                    if let Some(res) = process_is_pm(inner_pair) {
372                        is_pm = res;
373                    }
374                }
375                _ => {
376                    return Err(ParseDateError::ParseError(
377                        "Unexpected rule in specific_time".to_string(),
378                    ));
379                }
380            }
381        }
382
383        if is_pm && hour < 12 {
384            hour += 12;
385        } else if !is_pm && hour == 12 {
386            hour = 0;
387        }
388
389        let modified_datetime = change_time(datetime, hour, minute)?;
390
391        Ok(modified_datetime)
392    }
393
394    pub fn process_specific_date(
395        pair: Pair<'_, Rule>,
396        datetime: DateTime<Local>,
397    ) -> Result<DateTime<Local>, ParseDateError> {
398        let mut year: i32 = 0;
399        let mut month: u32 = 0;
400        let mut day: u32 = 0;
401
402        // Iterate through inner pairs to capture year, month, day
403        for inner_pair in pair.clone().into_inner() {
404            match inner_pair.as_rule() {
405                Rule::date_sep => {}
406                Rule::year => {
407                    year = inner_pair.as_str().parse::<i32>().map_err(|e| {
408                        ParseDateError::ParseError(format!("Failed to parse year: {e}"))
409                    })?;
410                }
411                Rule::month => {
412                    month = inner_pair.as_str().parse::<u32>().map_err(|e| {
413                        ParseDateError::ParseError(format!("Failed to parse month: {e}"))
414                    })?;
415                }
416                Rule::month_name => {
417                    month = process_month_name(inner_pair.as_str()).map_err(|e| {
418                        ParseDateError::ParseError(format!("Failed to parse month name: {e}"))
419                    })?;
420                }
421                Rule::month_short_name => {
422                    month = process_month_name(inner_pair.as_str()).map_err(|e| {
423                        ParseDateError::ParseError(format!("Failed to parse short month: {e}"))
424                    })?;
425                }
426                Rule::day => {
427                    day = inner_pair.as_str().parse::<u32>().map_err(|e| {
428                        ParseDateError::ParseError(format!(
429                            "Failed to parse day '{}': {e}",
430                            inner_pair.as_str()
431                        ))
432                    })?;
433                }
434                _ => {
435                    return Err(ParseDateError::ParseError(format!(
436                        "Unexpected rule {inner_pair:?} in specific_date"
437                    )));
438                }
439            }
440        }
441
442        datetime
443            .with_year(year)
444            .and_then(|dt| dt.with_month(month).and_then(|dt| dt.with_day(day)))
445            .ok_or(ParseDateError::ParseError(format!(
446                "Invalid date: {pair:?}"
447            )))
448    }
449
450    pub fn process_specific_day(
451        rule: Rule,
452        datetime: DateTime<Local>,
453    ) -> Result<DateTime<Local>, ParseDateError> {
454        let target_weekday = process_weekday(rule)?;
455        let current_weekday = datetime.weekday();
456
457        let target_day_num = target_weekday.num_days_from_sunday();
458        let current_day_num = current_weekday.num_days_from_sunday();
459
460        let days_difference = if target_day_num >= current_day_num {
461            (target_day_num - current_day_num) as i64
462        } else {
463            -((current_day_num - target_day_num) as i64)
464        };
465
466        let target_date = datetime + Duration::days(days_difference);
467        Ok(target_date)
468    }
469
470    pub fn process_month_name(month: &str) -> Result<u32, ParseDateError> {
471        let n = month.trim().to_ascii_lowercase();
472
473        Ok(match n.as_str() {
474            "jan" | "january" => 1,
475            "feb" | "february" => 2,
476            "mar" | "march" => 3,
477            "apr" | "april" => 4,
478            "may" => 5,
479            "jun" | "june" => 6,
480            "jul" | "july" => 7,
481            "aug" | "august" => 8,
482            "sep" | "sept" | "september" => 9,
483            "oct" | "october" => 10,
484            "nov" | "november" => 11,
485            "dec" | "december" => 12,
486            _ => {
487                return Err(ParseDateError::ParseError(format!(
488                    "Invalid month name: {month:?}"
489                )));
490            }
491        })
492    }
493
494    pub fn process_weekday(day_of_week: Rule) -> Result<Weekday, ParseDateError> {
495        match day_of_week {
496            Rule::monday => Ok(Weekday::Mon),
497            Rule::tuesday => Ok(Weekday::Tue),
498            Rule::wednesday => Ok(Weekday::Wed),
499            Rule::thursday => Ok(Weekday::Thu),
500            Rule::friday => Ok(Weekday::Fri),
501            Rule::saturday => Ok(Weekday::Sat),
502            Rule::sunday => Ok(Weekday::Sun),
503            _ => Err(ParseDateError::ParseError(format!(
504                "Invalid weekday: {:?}",
505                day_of_week
506            ))),
507        }
508    }
509
510    pub fn change_time(
511        datetime: DateTime<Local>,
512        hour: u32,
513        minute: u32,
514    ) -> Result<DateTime<Local>, ParseDateError> {
515        match Local.with_ymd_and_hms(
516            datetime.year(),
517            datetime.month(),
518            datetime.day(),
519            hour,
520            minute,
521            0,
522        ) {
523            chrono::LocalResult::Single(new_datetime) => Ok(new_datetime),
524            chrono::LocalResult::None => Err(ParseDateError::ParseError(
525                "Invalid date or time components".to_string(),
526            )),
527            chrono::LocalResult::Ambiguous(_, _) => Err(ParseDateError::ParseError(
528                "Ambiguous date and time".to_string(),
529            )),
530        }
531    }
532
533    pub fn shift_to_weekday(
534        now: DateTime<Local>,
535        target_weekday: Weekday,
536        direction: Rule,
537    ) -> Result<DateTime<Local>, ParseDateError> {
538        let current_weekday = now.weekday();
539
540        let num_from_curr = current_weekday.num_days_from_sunday() as i32;
541        let num_from_target = target_weekday.num_days_from_sunday() as i32;
542
543        let days_difference: i32 = match direction {
544            Rule::next => {
545                if num_from_target == 0 {
546                    7 - num_from_curr + 7
547                } else {
548                    7 - num_from_curr + num_from_target
549                }
550            }
551
552            Rule::last => {
553                if num_from_target == 0 {
554                    -num_from_curr
555                } else {
556                    -num_from_curr - 7 + num_from_target
557                }
558            }
559            Rule::this => {
560                let diff = (num_from_target as i64) - (num_from_curr as i64);
561                if diff >= 0 {
562                    diff as i32
563                } else {
564                    (diff + 7) as i32
565                }
566            }
567            _ => -100,
568        };
569
570        if days_difference < -7 {
571            return Err(ParseDateError::ParseError(format!(
572                "Expected last, this or next, got {:?}",
573                direction
574            )));
575        }
576
577        // println!("days_difference {:?}", days_difference);
578
579        Ok(now + Duration::days(days_difference as i64))
580    }
581
582    pub fn process_is_pm(pair: Pair<'_, Rule>) -> Option<bool> {
583        if let Some(inner_pair) = pair.into_inner().next() {
584            if inner_pair.as_rule() == Rule::pm {
585                return Some(true);
586            } else if inner_pair.as_rule() == Rule::am {
587                return Some(false);
588            } else {
589                return None;
590            }
591        }
592        None
593    }
594}