1use {pest_derive::Parser, thiserror::Error};
2
3#[derive(Parser)]
5#[grammar = "./grammar.pest"]
6pub struct DateParser;
7
8#[derive(Debug, Error)]
10pub enum ParseDateError {
11 #[error("Failed to parse date: {0}")]
13 ParseError(String),
14}
15
16pub 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 pub fn from_string(string: &str) -> Result<DateTime<Local>, ParseDateError> {
39 from_string_with_reference(string, Local::now())
40 }
41
42 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 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 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 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}