human_date_parser/
lib.rs

1use std::fmt::Display;
2
3use ast::{
4    build_ast_from, Ago, Date, DateTime, Duration as AstDuration, In, IsoDate, Quantifier,
5    RelativeSpecifier, Time, TimeUnit,
6};
7use chrono::{
8    Datelike, Days, Duration as ChronoDuration, Month, Months, NaiveDate, NaiveDateTime,
9    NaiveTime, Weekday,
10};
11use thiserror::Error;
12
13mod ast;
14#[cfg(test)]
15mod tests;
16
17#[derive(Debug, Error)]
18pub enum ParseError {
19    #[error("Could not match input to any known format")]
20    InvalidFormat,
21    #[error("One or more errors occured when processing input")]
22    ProccessingErrors(Vec<ProcessingError>),
23    #[error(
24        "An internal library error occured. This should not happen. Please report it. Error: {0}"
25    )]
26    InternalError(#[from] InternalError),
27}
28
29#[derive(Debug, Error)]
30pub enum ProcessingError {
31    #[error("Could not build time from {hour}:{minute}")]
32    TimeHourMinute { hour: u32, minute: u32 },
33    #[error("Could not build time from {hour}:{minute}:{second}")]
34    TimeHourMinuteSecond { hour: u32, minute: u32, second: u32 },
35    #[error("Failed to add {count} {unit} to the current time")]
36    AddToNow { unit: String, count: u32 },
37    #[error("Failed to subtract {count} {unit} from the current time")]
38    SubtractFromNow { unit: String, count: u32 },
39    #[error("Failed to subtract {count} {unit} from {date}")]
40    SubtractFromDate {
41        unit: String,
42        count: u32,
43        date: NaiveDateTime,
44    },
45    #[error("Failed to add {count} {unit} to {date}")]
46    AddToDate {
47        unit: String,
48        count: u32,
49        date: NaiveDateTime,
50    },
51    #[error("{year}-{month}-{day} is not a valid date")]
52    InvalidDate { year: i32, month: u32, day: u32 },
53    #[error("Failed to parse inner human time: {0}")]
54    InnerHumanTimeParse(Box<ParseError>),
55}
56
57#[derive(Debug, Error)]
58pub enum InternalError {
59    #[error("Failed to build AST. This is a bug.")]
60    FailedToBuildAst,
61}
62
63#[derive(Debug)]
64pub enum ParseResult {
65    DateTime(NaiveDateTime),
66    Date(NaiveDate),
67    Time(NaiveTime),
68}
69
70impl Display for ParseResult {
71    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72        match self {
73            ParseResult::DateTime(datetime) => write!(f, "{}", datetime),
74            ParseResult::Date(date) => write!(f, "{}", date),
75            ParseResult::Time(time) => write!(f, "{}", time),
76        }
77    }
78}
79
80/// Converts a human expression of a date into a more usable one.
81///
82/// # Errors
83///
84/// This function will return an error if the string contains values than can not be parsed into a date.
85///
86/// # Examples
87/// ```
88/// use chrono::Local;
89/// use human_date_parser::{from_human_time, ParseResult};
90/// let now = Local::now().naive_local();
91/// let date = from_human_time("Last Friday at 19:45", now).unwrap();
92/// match date {
93///     ParseResult::DateTime(date) => println!("{date}"),
94///     _ => unreachable!()
95/// }
96/// ```
97pub fn from_human_time(str: &str, now: NaiveDateTime) -> Result<ParseResult, ParseError> {
98    let lowercase = str.to_lowercase();
99    let parsed = build_ast_from(&lowercase)?;
100
101    parse_human_time(parsed, now)
102}
103
104fn parse_human_time(parsed: ast::HumanTime, now: NaiveDateTime) -> Result<ParseResult, ParseError> {
105    match parsed {
106        ast::HumanTime::DateTime(date_time) => {
107            parse_date_time(date_time, &now).map(|dt| ParseResult::DateTime(dt))
108        }
109        ast::HumanTime::Date(date) => parse_date(date, &now)
110            .map(|date| ParseResult::Date(date))
111            .map_err(|err| ParseError::ProccessingErrors(vec![err])),
112        ast::HumanTime::Time(time) => parse_time(time)
113            .map(|time| ParseResult::Time(time))
114            .map_err(|err| ParseError::ProccessingErrors(vec![err])),
115        ast::HumanTime::In(in_ast) => parse_in(in_ast, &now)
116            .map(|time| ParseResult::DateTime(time))
117            .map_err(|err| ParseError::ProccessingErrors(vec![err])),
118        ast::HumanTime::Ago(ago) => parse_ago(ago, &now)
119            .map(|time| ParseResult::DateTime(time))
120            .map_err(|err| ParseError::ProccessingErrors(vec![err])),
121        ast::HumanTime::Now => Ok(ParseResult::DateTime(now)),
122    }
123}
124
125fn parse_date_time(date_time: DateTime, now: &NaiveDateTime) -> Result<NaiveDateTime, ParseError> {
126    let date = parse_date(date_time.date, now);
127    let time = parse_time(date_time.time);
128
129    match (date, time) {
130        (Ok(date), Ok(time)) => Ok(NaiveDateTime::new(date, time)),
131        (Ok(_), Err(time_error)) => Err(ParseError::ProccessingErrors(vec![time_error])),
132        (Err(date_error), Ok(_)) => Err(ParseError::ProccessingErrors(vec![date_error])),
133        (Err(date_error), Err(time_error)) => {
134            Err(ParseError::ProccessingErrors(vec![date_error, time_error]))
135        }
136    }
137}
138
139fn parse_date(date: Date, now: &NaiveDateTime) -> Result<NaiveDate, ProcessingError> {
140    match date {
141        Date::Today => Ok(now.date()),
142        Date::Tomorrow => {
143            now.date()
144                .checked_add_days(Days::new(1))
145                .ok_or(ProcessingError::AddToNow {
146                    unit: String::from("days"),
147                    count: 1,
148                })
149        }
150        Date::Overmorrow => {
151            now.date()
152                .checked_add_days(Days::new(2))
153                .ok_or(ProcessingError::AddToNow {
154                    unit: String::from("days"),
155                    count: 2,
156                })
157        }
158        Date::Yesterday => {
159            now.date()
160                .checked_sub_days(Days::new(1))
161                .ok_or(ProcessingError::SubtractFromNow {
162                    unit: String::from("days"),
163                    count: 1,
164                })
165        }
166        Date::IsoDate(iso_date) => parse_iso_date(iso_date),
167        Date::DayMonthYear(day, month, year) => parse_day_month_year(day, month, year as i32),
168        Date::DayMonth(day, month) => parse_day_month_year(day, month, now.year()),
169        Date::RelativeWeekWeekday(relative, weekday) => {
170            find_weekday_relative_week(relative, weekday.into(), now.date())
171        }
172        Date::RelativeWeekday(relative, weekday) => {
173            find_weekday_relative(relative, weekday.into(), now.date())
174        }
175        Date::RelativeTimeUnit(relative, time_unit) => {
176            Ok(relative_date_time_unit(relative, time_unit, now.clone())?.date())
177        }
178        Date::UpcomingWeekday(weekday) => {
179            find_weekday_relative(RelativeSpecifier::Next, weekday.into(), now.date())
180        }
181    }
182}
183
184fn parse_iso_date(iso_date: IsoDate) -> Result<NaiveDate, ProcessingError> {
185    let (year, month, day) = (iso_date.year as i32, iso_date.month, iso_date.day);
186    NaiveDate::from_ymd_opt(year, month, day).ok_or(ProcessingError::InvalidDate {
187        year,
188        month,
189        day,
190    })
191}
192
193fn parse_day_month_year(day: u32, month: Month, year: i32) -> Result<NaiveDate, ProcessingError> {
194    let month = month.number_from_month();
195    NaiveDate::from_ymd_opt(year, month, day).ok_or(ProcessingError::InvalidDate {
196        year,
197        month,
198        day,
199    })
200}
201
202fn parse_time(time: Time) -> Result<NaiveTime, ProcessingError> {
203    match time {
204        Time::HourMinute(hour, minute) => NaiveTime::from_hms_opt(hour, minute, 0)
205            .ok_or(ProcessingError::TimeHourMinute { hour, minute }),
206        Time::HourMinuteSecond(hour, minute, second) => NaiveTime::from_hms_opt(
207            hour, minute, second,
208        )
209        .ok_or(ProcessingError::TimeHourMinuteSecond {
210            hour,
211            minute,
212            second,
213        }),
214    }
215}
216
217fn parse_in(in_ast: In, now: &NaiveDateTime) -> Result<NaiveDateTime, ProcessingError> {
218    let dt = now.clone();
219    apply_duration(in_ast.0, dt, Direction::Forwards)
220}
221
222fn parse_ago(ago: Ago, now: &NaiveDateTime) -> Result<NaiveDateTime, ProcessingError> {
223    match ago {
224        Ago::AgoFromNow(ago) => {
225            let dt = now.clone();
226            apply_duration(ago, dt, Direction::Backwards)
227        }
228        Ago::AgoFromTime(ago, time) => {
229            let human_time = parse_human_time(*time, now.clone())
230                .map_err(|e| ProcessingError::InnerHumanTimeParse(Box::new(e)))?;
231            let dt = match human_time {
232                ParseResult::DateTime(dt) => dt,
233                ParseResult::Date(date) => NaiveDateTime::new(date, now.time()),
234                ParseResult::Time(time) => NaiveDateTime::new(now.date(), time),
235            };
236            apply_duration(ago, dt, Direction::Backwards)
237        }
238    }
239}
240
241#[derive(PartialEq, Eq)]
242enum Direction {
243    Forwards,
244    Backwards,
245}
246
247fn apply_duration(
248    duration: AstDuration,
249    mut dt: NaiveDateTime,
250    direction: Direction,
251) -> Result<NaiveDateTime, ProcessingError> {
252    for quant in duration.0 {
253        match quant {
254            Quantifier::Year(years) => {
255                let years = years as i32;
256                if direction == Direction::Forwards {
257                    dt = dt
258                        .with_year(dt.year() + years)
259                        .ok_or(ProcessingError::InvalidDate {
260                            year: dt.year() + years,
261                            month: dt.month(),
262                            day: dt.day(),
263                        })?;
264                } else {
265                    dt = dt
266                        .with_year(dt.year() - years)
267                        .ok_or(ProcessingError::InvalidDate {
268                            year: dt.year() - years,
269                            month: dt.month(),
270                            day: dt.day(),
271                        })?;
272                }
273            }
274            Quantifier::Month(months) => {
275                if direction == Direction::Forwards {
276                    dt = dt.checked_add_months(Months::new(months)).ok_or(
277                        ProcessingError::AddToDate {
278                            unit: "months".to_string(),
279                            count: months,
280                            date: dt,
281                        },
282                    )?
283                } else {
284                    dt = dt.checked_sub_months(Months::new(months)).ok_or(
285                        ProcessingError::SubtractFromDate {
286                            unit: "months".to_string(),
287                            count: months,
288                            date: dt,
289                        },
290                    )?
291                }
292            }
293            Quantifier::Week(weeks) => {
294                if direction == Direction::Forwards {
295                    dt = dt.checked_add_days(Days::new(weeks as u64 * 7)).ok_or(
296                        ProcessingError::AddToDate {
297                            unit: "weeks".to_string(),
298                            count: weeks,
299                            date: dt,
300                        },
301                    )?
302                } else {
303                    dt = dt.checked_sub_days(Days::new(weeks as u64 * 7)).ok_or(
304                        ProcessingError::AddToDate {
305                            unit: "weeks".to_string(),
306                            count: weeks,
307                            date: dt,
308                        },
309                    )?
310                }
311            }
312            Quantifier::Day(days) => {
313                if direction == Direction::Forwards {
314                    dt = dt.checked_add_days(Days::new(days as u64)).ok_or(
315                        ProcessingError::AddToDate {
316                            unit: "days".to_string(),
317                            count: days,
318                            date: dt,
319                        },
320                    )?
321                } else {
322                    dt = dt.checked_sub_days(Days::new(days as u64)).ok_or(
323                        ProcessingError::AddToDate {
324                            unit: "days".to_string(),
325                            count: days,
326                            date: dt,
327                        },
328                    )?
329                }
330            }
331            Quantifier::Hour(hours) => {
332                if direction == Direction::Forwards {
333                    dt = dt + ChronoDuration::hours(hours as i64)
334                } else {
335                    dt = dt - ChronoDuration::hours(hours as i64)
336                }
337            }
338            Quantifier::Minute(minutes) => {
339                if direction == Direction::Forwards {
340                    dt = dt + ChronoDuration::minutes(minutes as i64)
341                } else {
342                    dt = dt - ChronoDuration::minutes(minutes as i64)
343                }
344            }
345            Quantifier::Second(seconds) => {
346                if direction == Direction::Forwards {
347                    dt = dt + ChronoDuration::seconds(seconds as i64)
348                } else {
349                    dt = dt - ChronoDuration::seconds(seconds as i64)
350                }
351            }
352        };
353    }
354
355    Ok(dt)
356}
357
358fn relative_date_time_unit(
359    relative: RelativeSpecifier,
360    time_unit: TimeUnit,
361    now: NaiveDateTime,
362) -> Result<NaiveDateTime, ProcessingError> {
363    let quantifier = match time_unit {
364        TimeUnit::Year => Quantifier::Year(1),
365        TimeUnit::Month => Quantifier::Month(1),
366        TimeUnit::Week => Quantifier::Week(1),
367        TimeUnit::Day => Quantifier::Day(1),
368        TimeUnit::Hour | TimeUnit::Minute | TimeUnit::Second => {
369            unreachable!("Non-date time units should never be used in this function.")
370        }
371    };
372
373
374    match relative {
375        RelativeSpecifier::This => Ok(now),
376        RelativeSpecifier::Next => apply_duration(AstDuration(vec![quantifier]), now, Direction::Forwards),
377        RelativeSpecifier::Last => apply_duration(AstDuration(vec![quantifier]), now, Direction::Backwards),
378    }
379}
380
381fn find_weekday_relative_week(
382    relative: RelativeSpecifier,
383    weekday: Weekday,
384    now: NaiveDate,
385) -> Result<NaiveDate, ProcessingError> {
386    let day_offset = -(now.weekday().num_days_from_monday() as i64);
387    let week_offset = match relative {
388        RelativeSpecifier::This => 0,
389        RelativeSpecifier::Next => 1,
390        RelativeSpecifier::Last => -1,
391    } * 7;
392    let offset = day_offset + week_offset;
393
394    let now = if offset.is_positive() {
395        now.checked_add_days(Days::new(offset.unsigned_abs()))
396            .ok_or(ProcessingError::AddToNow {
397                unit: "days".to_string(),
398                count: offset.unsigned_abs() as u32,
399            })?
400    } else {
401        now.checked_sub_days(Days::new(offset.unsigned_abs()))
402            .ok_or(ProcessingError::SubtractFromNow {
403                unit: "days".to_string(),
404                count: offset.unsigned_abs() as u32,
405            })?
406    };
407
408    find_weekday_relative(RelativeSpecifier::This, weekday, now)
409}
410
411fn find_weekday_relative(
412    relative: RelativeSpecifier,
413    weekday: Weekday,
414    now: NaiveDate,
415) -> Result<NaiveDate, ProcessingError> {
416    match relative {
417        RelativeSpecifier::This | RelativeSpecifier::Next => {
418            if matches!(relative, RelativeSpecifier::This) && now.weekday() == weekday {
419                return Ok(now.clone());
420            }
421
422            let current_weekday = now.weekday().num_days_from_monday();
423            let target_weekday = weekday.num_days_from_monday();
424
425            let offset = if target_weekday > current_weekday {
426                target_weekday - current_weekday
427            } else {
428                7 - current_weekday + target_weekday
429            };
430
431            now.checked_add_days(Days::new(offset as u64))
432                .ok_or(ProcessingError::AddToNow {
433                    unit: "days".to_string(),
434                    count: offset,
435                })
436        }
437        RelativeSpecifier::Last => {
438            let current_weekday = now.weekday().num_days_from_monday();
439            let target_weekday = weekday.num_days_from_monday();
440
441            let offset = if target_weekday >= current_weekday {
442                7 + current_weekday - target_weekday
443            } else {
444                current_weekday - target_weekday
445            };
446
447            now.checked_sub_days(Days::new(offset as u64))
448                .ok_or(ProcessingError::SubtractFromNow {
449                    unit: "days".to_string(),
450                    count: offset,
451                })
452        }
453    }
454}