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> {
133 let lowercase = str.to_lowercase();
134 let parsed = build_ast_from(&lowercase)?;
135
136 parse_human_time(parsed, now)
137}
138
139fn parse_human_time(parsed: ast::HumanTime, now: NaiveDateTime) -> Result<ParseResult, ParseError> {
140 match parsed {
141 ast::HumanTime::DateTime(date_time) => {
142 parse_date_time(date_time, &now).map(|dt| ParseResult::DateTime(dt))
143 }
144 ast::HumanTime::Date(date) => parse_date(date, &now)
145 .map(|date| ParseResult::Date(date))
146 .map_err(|err| ParseError::ProccessingErrors(vec![err])),
147 ast::HumanTime::Time(time) => parse_time(time)
148 .map(|time| ParseResult::Time(time))
149 .map_err(|err| ParseError::ProccessingErrors(vec![err])),
150 ast::HumanTime::In(in_ast) => parse_in(in_ast, &now)
151 .map(|time| ParseResult::DateTime(time))
152 .map_err(|err| ParseError::ProccessingErrors(vec![err])),
153 ast::HumanTime::Ago(ago) => parse_ago(ago, &now)
154 .map(|time| ParseResult::DateTime(time))
155 .map_err(|err| ParseError::ProccessingErrors(vec![err])),
156 ast::HumanTime::Now => Ok(ParseResult::DateTime(now)),
157 }
158}
159
160fn parse_date_time(date_time: DateTime, now: &NaiveDateTime) -> Result<NaiveDateTime, ParseError> {
161 let date = parse_date(date_time.date, now);
162 let time = parse_time(date_time.time);
163
164 match (date, time) {
165 (Ok(date), Ok(time)) => Ok(NaiveDateTime::new(date, time)),
166 (Ok(_), Err(time_error)) => Err(ParseError::ProccessingErrors(vec![time_error])),
167 (Err(date_error), Ok(_)) => Err(ParseError::ProccessingErrors(vec![date_error])),
168 (Err(date_error), Err(time_error)) => {
169 Err(ParseError::ProccessingErrors(vec![date_error, time_error]))
170 }
171 }
172}
173
174fn parse_date(date: Date, now: &NaiveDateTime) -> Result<NaiveDate, ProcessingError> {
175 match date {
176 Date::Today => Ok(now.date()),
177 Date::Tomorrow => {
178 now.date()
179 .checked_add_days(Days::new(1))
180 .ok_or(ProcessingError::AddToNow {
181 unit: String::from("days"),
182 count: 1,
183 })
184 }
185 Date::Overmorrow => {
186 now.date()
187 .checked_add_days(Days::new(2))
188 .ok_or(ProcessingError::AddToNow {
189 unit: String::from("days"),
190 count: 2,
191 })
192 }
193 Date::Yesterday => {
194 now.date()
195 .checked_sub_days(Days::new(1))
196 .ok_or(ProcessingError::SubtractFromNow {
197 unit: String::from("days"),
198 count: 1,
199 })
200 }
201 Date::IsoDate(iso_date) => parse_iso_date(iso_date),
202 Date::DayMonthYear(day, month, year) => parse_day_month_year(day, month, year as i32),
203 Date::DayMonth(day, month) => parse_day_month_year(day, month, now.year()),
204 Date::RelativeWeekWeekday(relative, weekday) => {
205 find_weekday_relative_week(relative, weekday.into(), now.date())
206 }
207 Date::RelativeWeekday(relative, weekday) => {
208 find_weekday_relative(relative, weekday.into(), now.date())
209 }
210 Date::RelativeTimeUnit(relative, time_unit) => {
211 Ok(relative_date_time_unit(relative, time_unit, now.clone())?.date())
212 }
213 Date::UpcomingWeekday(weekday) => {
214 find_weekday_relative(RelativeSpecifier::Next, weekday.into(), now.date())
215 }
216 }
217}
218
219fn parse_iso_date(iso_date: IsoDate) -> Result<NaiveDate, ProcessingError> {
220 let (year, month, day) = (iso_date.year as i32, iso_date.month, iso_date.day);
221 NaiveDate::from_ymd_opt(year, month, day).ok_or(ProcessingError::InvalidDate {
222 year,
223 month,
224 day,
225 })
226}
227
228fn parse_day_month_year(day: u32, month: Month, year: i32) -> Result<NaiveDate, ProcessingError> {
229 let month = month.number_from_month();
230 NaiveDate::from_ymd_opt(year, month, day).ok_or(ProcessingError::InvalidDate {
231 year,
232 month,
233 day,
234 })
235}
236
237fn parse_time(time: Time) -> Result<NaiveTime, ProcessingError> {
238 match time {
239 Time::HourMinute(hour, minute) => NaiveTime::from_hms_opt(hour, minute, 0)
240 .ok_or(ProcessingError::TimeHourMinute { hour, minute }),
241 Time::HourMinuteSecond(hour, minute, second) => NaiveTime::from_hms_opt(
242 hour, minute, second,
243 )
244 .ok_or(ProcessingError::TimeHourMinuteSecond {
245 hour,
246 minute,
247 second,
248 }),
249 }
250}
251
252fn parse_in(in_ast: In, now: &NaiveDateTime) -> Result<NaiveDateTime, ProcessingError> {
253 let dt = now.clone();
254 apply_duration(in_ast.0, dt, Direction::Forwards)
255}
256
257fn parse_ago(ago: Ago, now: &NaiveDateTime) -> Result<NaiveDateTime, ProcessingError> {
258 match ago {
259 Ago::AgoFromNow(ago) => {
260 let dt = now.clone();
261 apply_duration(ago, dt, Direction::Backwards)
262 }
263 Ago::AgoFromTime(ago, time) => {
264 let human_time = parse_human_time(*time, now.clone())
265 .map_err(|e| ProcessingError::InnerHumanTimeParse(Box::new(e)))?;
266 let dt = match human_time {
267 ParseResult::DateTime(dt) => dt,
268 ParseResult::Date(date) => NaiveDateTime::new(date, now.time()),
269 ParseResult::Time(time) => NaiveDateTime::new(now.date(), time),
270 };
271 apply_duration(ago, dt, Direction::Backwards)
272 }
273 }
274}
275
276#[derive(PartialEq, Eq)]
277enum Direction {
278 Forwards,
279 Backwards,
280}
281
282fn apply_duration(
283 duration: AstDuration,
284 mut dt: NaiveDateTime,
285 direction: Direction,
286) -> Result<NaiveDateTime, ProcessingError> {
287 for quant in duration.0 {
288 match quant {
289 Quantifier::Year(years) => {
290 let years = years as i32;
291 if direction == Direction::Forwards {
292 dt = dt
293 .with_year(dt.year() + years)
294 .ok_or(ProcessingError::InvalidDate {
295 year: dt.year() + years,
296 month: dt.month(),
297 day: dt.day(),
298 })?;
299 } else {
300 dt = dt
301 .with_year(dt.year() - years)
302 .ok_or(ProcessingError::InvalidDate {
303 year: dt.year() - years,
304 month: dt.month(),
305 day: dt.day(),
306 })?;
307 }
308 }
309 Quantifier::Month(months) => {
310 if direction == Direction::Forwards {
311 dt = dt.checked_add_months(Months::new(months)).ok_or(
312 ProcessingError::AddToDate {
313 unit: "months".to_string(),
314 count: months,
315 date: dt,
316 },
317 )?
318 } else {
319 dt = dt.checked_sub_months(Months::new(months)).ok_or(
320 ProcessingError::SubtractFromDate {
321 unit: "months".to_string(),
322 count: months,
323 date: dt,
324 },
325 )?
326 }
327 }
328 Quantifier::Week(weeks) => {
329 if direction == Direction::Forwards {
330 dt = dt.checked_add_days(Days::new(weeks as u64 * 7)).ok_or(
331 ProcessingError::AddToDate {
332 unit: "weeks".to_string(),
333 count: weeks,
334 date: dt,
335 },
336 )?
337 } else {
338 dt = dt.checked_sub_days(Days::new(weeks as u64 * 7)).ok_or(
339 ProcessingError::AddToDate {
340 unit: "weeks".to_string(),
341 count: weeks,
342 date: dt,
343 },
344 )?
345 }
346 }
347 Quantifier::Day(days) => {
348 if direction == Direction::Forwards {
349 dt = dt.checked_add_days(Days::new(days as u64)).ok_or(
350 ProcessingError::AddToDate {
351 unit: "days".to_string(),
352 count: days,
353 date: dt,
354 },
355 )?
356 } else {
357 dt = dt.checked_sub_days(Days::new(days as u64)).ok_or(
358 ProcessingError::AddToDate {
359 unit: "days".to_string(),
360 count: days,
361 date: dt,
362 },
363 )?
364 }
365 }
366 Quantifier::Hour(hours) => {
367 if direction == Direction::Forwards {
368 dt = dt + ChronoDuration::hours(hours as i64)
369 } else {
370 dt = dt - ChronoDuration::hours(hours as i64)
371 }
372 }
373 Quantifier::Minute(minutes) => {
374 if direction == Direction::Forwards {
375 dt = dt + ChronoDuration::minutes(minutes as i64)
376 } else {
377 dt = dt - ChronoDuration::minutes(minutes as i64)
378 }
379 }
380 Quantifier::Second(seconds) => {
381 if direction == Direction::Forwards {
382 dt = dt + ChronoDuration::seconds(seconds as i64)
383 } else {
384 dt = dt - ChronoDuration::seconds(seconds as i64)
385 }
386 }
387 };
388 }
389
390 Ok(dt)
391}
392
393fn relative_date_time_unit(
394 relative: RelativeSpecifier,
395 time_unit: TimeUnit,
396 now: NaiveDateTime,
397) -> Result<NaiveDateTime, ProcessingError> {
398 let quantifier = match time_unit {
399 TimeUnit::Year => Quantifier::Year(1),
400 TimeUnit::Month => Quantifier::Month(1),
401 TimeUnit::Week => Quantifier::Week(1),
402 TimeUnit::Day => Quantifier::Day(1),
403 TimeUnit::Hour | TimeUnit::Minute | TimeUnit::Second => {
404 unreachable!("Non-date time units should never be used in this function.")
405 }
406 };
407
408
409 match relative {
410 RelativeSpecifier::This => Ok(now),
411 RelativeSpecifier::Next => apply_duration(AstDuration(vec![quantifier]), now, Direction::Forwards),
412 RelativeSpecifier::Last => apply_duration(AstDuration(vec![quantifier]), now, Direction::Backwards),
413 }
414}
415
416fn find_weekday_relative_week(
417 relative: RelativeSpecifier,
418 weekday: Weekday,
419 now: NaiveDate,
420) -> Result<NaiveDate, ProcessingError> {
421 let day_offset = -(now.weekday().num_days_from_monday() as i64);
422 let week_offset = match relative {
423 RelativeSpecifier::This => 0,
424 RelativeSpecifier::Next => 1,
425 RelativeSpecifier::Last => -1,
426 } * 7;
427 let offset = day_offset + week_offset;
428
429 let now = if offset.is_positive() {
430 now.checked_add_days(Days::new(offset.unsigned_abs()))
431 .ok_or(ProcessingError::AddToNow {
432 unit: "days".to_string(),
433 count: offset.unsigned_abs() as u32,
434 })?
435 } else {
436 now.checked_sub_days(Days::new(offset.unsigned_abs()))
437 .ok_or(ProcessingError::SubtractFromNow {
438 unit: "days".to_string(),
439 count: offset.unsigned_abs() as u32,
440 })?
441 };
442
443 find_weekday_relative(RelativeSpecifier::This, weekday, now)
444}
445
446fn find_weekday_relative(
447 relative: RelativeSpecifier,
448 weekday: Weekday,
449 now: NaiveDate,
450) -> Result<NaiveDate, ProcessingError> {
451 match relative {
452 RelativeSpecifier::This | RelativeSpecifier::Next => {
453 if matches!(relative, RelativeSpecifier::This) && now.weekday() == weekday {
454 return Ok(now.clone());
455 }
456
457 let current_weekday = now.weekday().num_days_from_monday();
458 let target_weekday = weekday.num_days_from_monday();
459
460 let offset = if target_weekday > current_weekday {
461 target_weekday - current_weekday
462 } else {
463 7 - current_weekday + target_weekday
464 };
465
466 now.checked_add_days(Days::new(offset as u64))
467 .ok_or(ProcessingError::AddToNow {
468 unit: "days".to_string(),
469 count: offset,
470 })
471 }
472 RelativeSpecifier::Last => {
473 let current_weekday = now.weekday().num_days_from_monday();
474 let target_weekday = weekday.num_days_from_monday();
475
476 let offset = if target_weekday >= current_weekday {
477 7 + current_weekday - target_weekday
478 } else {
479 current_weekday - target_weekday
480 };
481
482 now.checked_sub_days(Days::new(offset as u64))
483 .ok_or(ProcessingError::SubtractFromNow {
484 unit: "days".to_string(),
485 count: offset,
486 })
487 }
488 }
489}