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
80pub 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}