Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_datime/src/parser/relative.rs

32.5 KiB, 74 runs

created by r1870400018:8486, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1//! Comprehensive relative date parsing for natural language expressions.
2//!
3//! This module handles complex relative date expressions such as:
4//! - "next Tuesday", "last Friday", "this Monday"
5//! - "in 2 weeks", "in 3 days", "2 months ago"
6//! - "next month", "last year", "this week"
7//! - "3 days from now", "2 weeks from today"
8//! - "end of this month", "beginning of next year"
9//! - "this coming Monday", "the Tuesday after next"
10//!
11//! [Written with AI entirely](https://need2know.ai/entirely-ai/code)\
12//! Anthropic Claude
13
14use crate::{
15 calendar::CalendarDate,
16 constant::{DayOfWeek, MonthOfYear},
17 time::CalClockZone,
18};
19
20use oxedyne_fe2o3_core::prelude::*;
21
22use std::collections::HashMap;
23
24#[derive(Clone, Debug, PartialEq)]
25pub enum RelativeReference {
26 // The word the expression opened with.
27 Next,
28 Last,
29 This,
30 Coming, // "this coming", "upcoming"
31 AfterNext, // "the Tuesday after next"
32 BeforeLast, // "the Tuesday before last"
33}
34
35#[derive(Clone, Debug, PartialEq)]
36pub enum RelativeUnit {
37 Day,
38 Week,
39 Month,
40 Year,
41 DayOfWeek(DayOfWeek),
42 PeriodBoundary(PeriodType, BoundaryType), // "end of month"
43}
44
45#[derive(Clone, Debug, PartialEq)]
46pub enum PeriodType {
47 Week,
48 Month,
49 Quarter,
50 Year,
51}
52
53#[derive(Clone, Debug, PartialEq)]
54pub enum BoundaryType {
55 Beginning,
56 End,
57 Middle,
58}
59
60#[derive(Clone, Debug, PartialEq)]
61pub enum Direction {
62 Forward,
63 Backward,
64}
65
66#[derive(Clone, Debug, PartialEq)]
67pub struct RelativeExpression {
68 pub reference: RelativeReference,
69 pub unit: RelativeUnit,
70 pub quantity: Option<i32>, // the 2 in "2 weeks"
71 pub direction: Direction,
72 pub context: Option<String>, // "from now", "from today"
73}
74
75pub struct RelativeDateParser {
76 // The vocabularies, each mapping a word to the thing it means.
77 day_names: HashMap<String, DayOfWeek>,
78 month_names: HashMap<String, MonthOfYear>,
79 reference_words: HashMap<String, RelativeReference>,
80 unit_words: HashMap<String, RelativeUnit>,
81 direction_words: HashMap<String, Direction>,
82}
83
84impl Default for RelativeDateParser {
85 fn default() -> Self {
86 Self::new()
87 }
88}
89
90impl RelativeDateParser {
91 pub fn new() -> Self {
92 let mut parser = Self {
93 day_names: HashMap::new(),
94 month_names: HashMap::new(),
95 reference_words: HashMap::new(),
96 unit_words: HashMap::new(),
97 direction_words: HashMap::new(),
98 };
99
100 parser.initialise_vocabularies();
101 parser
102 }
103
104 fn initialise_vocabularies(&mut self) {
105 self.initialise_day_names();
106 self.initialise_month_names();
107 self.initialise_reference_words();
108 self.initialise_unit_words();
109 self.initialise_direction_words();
110 }
111
112 fn initialise_day_names(&mut self) {
113 let days = [
114 (DayOfWeek::Sunday, vec!["sunday", "sun"]),
115 (DayOfWeek::Monday, vec!["monday", "mon"]),
116 (DayOfWeek::Tuesday, vec!["tuesday", "tue", "tues"]),
117 (DayOfWeek::Wednesday, vec!["wednesday", "wed"]),
118 (DayOfWeek::Thursday, vec!["thursday", "thu", "thurs"]),
119 (DayOfWeek::Friday, vec!["friday", "fri"]),
120 (DayOfWeek::Saturday, vec!["saturday", "sat"]),
121 ];
122
123 for (day, names) in days.iter() {
124 for name in names {
125 self.day_names.insert(name.to_string(), *day);
126 }
127 }
128 }
129
130 fn initialise_month_names(&mut self) {
131 let months = [
132 (MonthOfYear::January, vec!["january", "jan"]),
133 (MonthOfYear::February, vec!["february", "feb"]),
134 (MonthOfYear::March, vec!["march", "mar"]),
135 (MonthOfYear::April, vec!["april", "apr"]),
136 (MonthOfYear::May, vec!["may"]),
137 (MonthOfYear::June, vec!["june", "jun"]),
138 (MonthOfYear::July, vec!["july", "jul"]),
139 (MonthOfYear::August, vec!["august", "aug"]),
140 (MonthOfYear::September, vec!["september", "sep", "sept"]),
141 (MonthOfYear::October, vec!["october", "oct"]),
142 (MonthOfYear::November, vec!["november", "nov"]),
143 (MonthOfYear::December, vec!["december", "dec"]),
144 ];
145
146 for (month, names) in months.iter() {
147 for name in names {
148 self.month_names.insert(name.to_string(), *month);
149 }
150 }
151 }
152
153 fn initialise_reference_words(&mut self) {
154 let references = [
155 (RelativeReference::Next, vec!["next", "following", "upcoming"]),
156 (RelativeReference::Last, vec!["last", "previous", "past", "prior"]),
157 (RelativeReference::This, vec!["this", "current"]),
158 (RelativeReference::Coming, vec!["coming", "approaching"]),
159 (RelativeReference::AfterNext, vec!["after"]),
160 (RelativeReference::BeforeLast, vec!["before"]),
161 ];
162
163 for (reference, words) in references.iter() {
164 for word in words {
165 self.reference_words.insert(word.to_string(), reference.clone());
166 }
167 }
168 }
169
170 fn initialise_unit_words(&mut self) {
171 let units = [
172 (RelativeUnit::Day, vec!["day", "days"]),
173 (RelativeUnit::Week, vec!["week", "weeks"]),
174 (RelativeUnit::Month, vec!["month", "months"]),
175 (RelativeUnit::Year, vec!["year", "years"]),
176 ];
177
178 for (unit, words) in units.iter() {
179 for word in words {
180 self.unit_words.insert(word.to_string(), unit.clone());
181 }
182 }
183
184 // Note: Period boundaries are handled by multi-token pattern matching,
185 // not individual token parsing, so we don't add them to unit_words.
186 }
187
188 fn initialise_direction_words(&mut self) {
189 let directions = [
190 (Direction::Forward, vec!["from", "after", "hence", "later"]),
191 (Direction::Backward, vec!["ago", "back", "before", "earlier"]),
192 ];
193
194 for (direction, words) in directions.iter() {
195 for word in words {
196 self.direction_words.insert(word.to_string(), direction.clone());
197 }
198 }
199 }
200
201 /// # Examples
202 ///
203 /// ```ignore
204 /// let parser = RelativeDateParser::new();
205 ///
206 /// // Simple relative dates
207 /// let expr = parser.parse("next Tuesday").unwrap();
208 /// let expr = parser.parse("last Friday").unwrap();
209 /// let expr = parser.parse("this Monday").unwrap();
210 ///
211 /// // Quantified relative dates
212 /// let expr = parser.parse("in 2 weeks").unwrap();
213 /// let expr = parser.parse("3 days ago").unwrap();
214 /// let expr = parser.parse("2 months from now").unwrap();
215 ///
216 /// // Complex expressions
217 /// let expr = parser.parse("the Tuesday after next").unwrap();
218 /// let expr = parser.parse("end of this month").unwrap();
219 /// let expr = parser.parse("beginning of next year").unwrap();
220 /// ```
221 pub fn parse(&self, input: &str) -> Outcome<RelativeExpression> {
222 let normalized = self.normalize_input(input);
223 let tokens = self.tokenize(&normalized);
224 self.parse_tokens(&tokens)
225 }
226
227 fn normalize_input(&self, input: &str) -> String {
228 input
229 .to_lowercase()
230 .replace(" ", " ")
231 .replace("from now", "")
232 .replace("from today", "")
233 .trim()
234 .to_string()
235 }
236
237 fn tokenize(&self, input: &str) -> Vec<String> {
238 input
239 .split_whitespace()
240 .filter(|word| !word.is_empty())
241 .filter(|word| !matches!(*word, "the" | "a" | "an" | "in" | "on" | "at"))
242 .map(|word| word.to_string())
243 .collect()
244 }
245
246 fn parse_tokens(&self, tokens: &[String]) -> Outcome<RelativeExpression> {
247 if tokens.is_empty() {
248 return Err(err!("Empty input for relative date parsing"; Invalid, Input));
249 }
250
251 let mut expression = RelativeExpression {
252 reference: RelativeReference::This,
253 unit: RelativeUnit::Day,
254 quantity: None,
255 direction: Direction::Forward,
256 context: None,
257 };
258
259 let mut i = 0;
260 while i < tokens.len() {
261 let token = &tokens[i];
262
263 // Try to parse quantity (numbers).
264 if let Ok(num) = token.parse::<i32>() {
265 expression.quantity = Some(num);
266 i += 1;
267 continue;
268 }
269
270 // Handle period boundaries FIRST before individual token parsing
271 // This prevents "end" and "month" from being parsed separately
272 if i + 2 < tokens.len() && tokens[i + 1] == "of" {
273 // Try direct pattern: "end of month"
274 if let Some(boundary_unit) = self.parse_period_boundary(token, &tokens[i + 2]) {
275 expression.unit = boundary_unit;
276 i += 3;
277 continue;
278 }
279
280 // Try with reference word: "end of this month", "beginning of next year"
281 if i + 3 < tokens.len() {
282 if let Some(reference) = self.reference_words.get(&tokens[i + 2]) {
283 if let Some(boundary_unit) = self.parse_period_boundary(token, &tokens[i + 3]) {
284 expression.reference = reference.clone();
285 expression.unit = boundary_unit;
286 i += 4;
287 continue;
288 }
289 }
290 }
291 }
292
293 // Try to parse relative reference.
294 if let Some(reference) = self.reference_words.get(token) {
295 expression.reference = reference.clone();
296 i += 1;
297 continue;
298 }
299
300 // Try to parse day of week.
301 if let Some(day) = self.day_names.get(token) {
302 expression.unit = RelativeUnit::DayOfWeek(*day);
303 i += 1;
304 continue;
305 }
306
307 // Try to parse unit.
308 if let Some(unit) = self.unit_words.get(token) {
309 expression.unit = unit.clone();
310 i += 1;
311 continue;
312 }
313
314 // Try to parse direction.
315 if let Some(direction) = self.direction_words.get(token) {
316 expression.direction = direction.clone();
317 i += 1;
318 continue;
319 }
320
321 // Handle special multi-word patterns.
322 if token == "after" && i + 1 < tokens.len() && tokens[i + 1] == "next" {
323 expression.reference = RelativeReference::AfterNext;
324 i += 2;
325 continue;
326 }
327
328 if token == "before" && i + 1 < tokens.len() && tokens[i + 1] == "last" {
329 expression.reference = RelativeReference::BeforeLast;
330 i += 2;
331 continue;
332 }
333
334
335 // Skip unknown tokens.
336 i += 1;
337 }
338
339 // Apply some intelligent defaults and validation.
340 ok!(self.validate_and_adjust_expression(&mut expression));
341
342 Ok(expression)
343 }
344
345 pub fn parse_period_boundary(&self, boundary_word: &str, period_word: &str) -> Option<RelativeUnit> {
346 let boundary_type = match boundary_word {
347 "beginning" | "start" => BoundaryType::Beginning,
348 "end" => BoundaryType::End,
349 "middle" => BoundaryType::Middle,
350 _ => return None,
351 };
352
353 let period_type = match period_word {
354 "week" => PeriodType::Week,
355 "month" => PeriodType::Month,
356 "quarter" => PeriodType::Quarter,
357 "year" => PeriodType::Year,
358 _ => return None,
359 };
360
361 Some(RelativeUnit::PeriodBoundary(period_type, boundary_type))
362 }
363
364 fn validate_and_adjust_expression(&self, expr: &mut RelativeExpression) -> Outcome<()> {
365 // If we have a quantity but no clear direction, infer from context.
366 if expr.quantity.is_some() {
367 // If we have "ago" or similar, it's backward.
368 if expr.direction == Direction::Backward {
369 // Already correct.
370 } else if matches!(expr.reference, RelativeReference::Last) {
371 expr.direction = Direction::Backward;
372 } else {
373 // Default to forward for quantified expressions.
374 expr.direction = Direction::Forward;
375 }
376 }
377
378 // Adjust direction based on reference type for all unit types.
379 match expr.reference {
380 RelativeReference::Next | RelativeReference::Coming => {
381 expr.direction = Direction::Forward;
382 },
383 RelativeReference::Last => {
384 expr.direction = Direction::Backward;
385 },
386 RelativeReference::This => {
387 // For "this X" expressions, determine direction based on context.
388 if expr.direction == Direction::Backward {
389 // Keep backward if explicitly specified.
390 } else {
391 // Default to forward for "this" expressions.
392 expr.direction = Direction::Forward;
393 }
394 },
395 RelativeReference::AfterNext => {
396 expr.direction = Direction::Forward;
397 },
398 RelativeReference::BeforeLast => {
399 expr.direction = Direction::Backward;
400 },
401 }
402
403 Ok(())
404 }
405
406 pub fn calculate_date(&self, expr: &RelativeExpression, base_date: &CalendarDate, zone: CalClockZone) -> Outcome<CalendarDate> {
407 match &expr.unit {
408 RelativeUnit::Day => self.calculate_day_offset(expr, base_date),
409 RelativeUnit::Week => self.calculate_week_offset(expr, base_date),
410 RelativeUnit::Month => self.calculate_month_offset(expr, base_date),
411 RelativeUnit::Year => self.calculate_year_offset(expr, base_date),
412 RelativeUnit::DayOfWeek(target_day) => self.calculate_day_of_week(expr, base_date, *target_day, zone),
413 RelativeUnit::PeriodBoundary(period, boundary) => self.calculate_period_boundary(expr, base_date, period, boundary, zone),
414 }
415 }
416
417 fn calculate_day_offset(&self, expr: &RelativeExpression, base_date: &CalendarDate) -> Outcome<CalendarDate> {
418 let quantity = expr.quantity.unwrap_or(1);
419 let offset = match expr.direction {
420 Direction::Forward => quantity,
421 Direction::Backward => -quantity,
422 };
423
424 base_date.add_days(offset)
425 }
426
427 fn calculate_week_offset(&self, expr: &RelativeExpression, base_date: &CalendarDate) -> Outcome<CalendarDate> {
428 let quantity = expr.quantity.unwrap_or(1);
429 let offset = match expr.direction {
430 Direction::Forward => quantity * 7,
431 Direction::Backward => -quantity * 7,
432 };
433
434 base_date.add_days(offset)
435 }
436
437 fn calculate_month_offset(&self, expr: &RelativeExpression, base_date: &CalendarDate) -> Outcome<CalendarDate> {
438 let quantity = expr.quantity.unwrap_or(1);
439 let offset = match expr.direction {
440 Direction::Forward => quantity,
441 Direction::Backward => -quantity,
442 };
443
444 base_date.add_months(offset)
445 }
446
447 fn calculate_year_offset(&self, expr: &RelativeExpression, base_date: &CalendarDate) -> Outcome<CalendarDate> {
448 let quantity = expr.quantity.unwrap_or(1);
449 let offset = match expr.direction {
450 Direction::Forward => quantity,
451 Direction::Backward => -quantity,
452 };
453
454 base_date.add_years(offset)
455 }
456
457 /// "This Tuesday" can land in the past, since it means the Tuesday of the
458 /// current week. "Next" and "last" never return the base date itself,
459 /// even when it falls on the target day.
460 fn calculate_day_of_week(&self, expr: &RelativeExpression, base_date: &CalendarDate, target_day: DayOfWeek, _zone: CalClockZone) -> Outcome<CalendarDate> {
461 let current_day = base_date.day_of_week();
462 let current_day_num = current_day.of() as i32;
463 let target_day_num = target_day.of() as i32;
464
465 let mut days_to_target = target_day_num - current_day_num;
466
467 match expr.reference {
468 RelativeReference::This => {
469 // This Tuesday: if today is Tuesday, return today; otherwise find the Tuesday in this week
470 if days_to_target == 0 {
471 return Ok(base_date.clone());
472 }
473 // For "this", we want the occurrence within this week, whether past or future
474 // No adjustment needed - days_to_target will be negative for past days in the week
475 },
476 RelativeReference::Next | RelativeReference::Coming => {
477 // Next Tuesday: find the next occurrence (not today even if today is Tuesday)
478 if days_to_target <= 0 {
479 days_to_target += 7;
480 }
481 },
482 RelativeReference::Last => {
483 // Last Tuesday: find the previous occurrence (not today even if today is Tuesday)
484 if days_to_target >= 0 {
485 days_to_target -= 7;
486 }
487 },
488 RelativeReference::AfterNext => {
489 // The Tuesday after next: find the occurrence after next Tuesday
490 if days_to_target <= 0 {
491 days_to_target += 7;
492 }
493 days_to_target += 7; // Add another week
494 },
495 RelativeReference::BeforeLast => {
496 // The Tuesday before last: find the occurrence before last Tuesday
497 if days_to_target >= 0 {
498 days_to_target -= 7;
499 }
500 days_to_target -= 7; // Subtract another week
501 },
502 }
503
504 base_date.add_days(days_to_target)
505 }
506
507 fn calculate_period_boundary(&self, expr: &RelativeExpression, base_date: &CalendarDate, period: &PeriodType, boundary: &BoundaryType, zone: CalClockZone) -> Outcome<CalendarDate> {
508 match period {
509 PeriodType::Week => self.calculate_week_boundary(expr, base_date, boundary),
510 PeriodType::Month => self.calculate_month_boundary(expr, base_date, boundary, zone),
511 PeriodType::Quarter => self.calculate_quarter_boundary(expr, base_date, boundary, zone),
512 PeriodType::Year => self.calculate_year_boundary(expr, base_date, boundary, zone),
513 }
514 }
515
516 fn calculate_week_boundary(&self, expr: &RelativeExpression, base_date: &CalendarDate, boundary: &BoundaryType) -> Outcome<CalendarDate> {
517 let current_day_num = base_date.day_of_week().of() as i32;
518
519 let target_date = match expr.reference {
520 RelativeReference::This => base_date.clone(),
521 RelativeReference::Next => res!(base_date.add_days(7)),
522 RelativeReference::Last => res!(base_date.add_days(-7)),
523 _ => base_date.clone(),
524 };
525
526 match boundary {
527 BoundaryType::Beginning => {
528 // Beginning of week (Sunday)
529 let days_to_sunday = if current_day_num == 0 { 0 } else { -current_day_num };
530 target_date.add_days(days_to_sunday)
531 },
532 BoundaryType::End => {
533 // End of week (Saturday)
534 let days_to_saturday = 6 - current_day_num;
535 target_date.add_days(days_to_saturday)
536 },
537 BoundaryType::Middle => {
538 // Middle of week (Wednesday)
539 let days_to_wednesday = 3 - current_day_num;
540 target_date.add_days(days_to_wednesday)
541 },
542 }
543 }
544
545 fn calculate_month_boundary(&self, expr: &RelativeExpression, base_date: &CalendarDate, boundary: &BoundaryType, zone: CalClockZone) -> Outcome<CalendarDate> {
546 let (target_year, target_month) = match expr.reference {
547 RelativeReference::This => (base_date.year(), base_date.month()),
548 RelativeReference::Next => {
549 if base_date.month() == 12 {
550 (base_date.year() + 1, 1)
551 } else {
552 (base_date.year(), base_date.month() + 1)
553 }
554 },
555 RelativeReference::Last => {
556 if base_date.month() == 1 {
557 (base_date.year() - 1, 12)
558 } else {
559 (base_date.year(), base_date.month() - 1)
560 }
561 },
562 _ => (base_date.year(), base_date.month()),
563 };
564
565 let target_month_enum = res!(MonthOfYear::from_number(target_month));
566
567 match boundary {
568 BoundaryType::Beginning => {
569 CalendarDate::from_ymd(target_year, target_month_enum, 1, zone)
570 },
571 BoundaryType::End => {
572 let days_in_month = target_month_enum.days_in_month(target_year);
573 CalendarDate::from_ymd(target_year, target_month_enum, days_in_month, zone)
574 },
575 BoundaryType::Middle => {
576 let days_in_month = target_month_enum.days_in_month(target_year);
577 let middle_day = (days_in_month + 1) / 2;
578 CalendarDate::from_ymd(target_year, target_month_enum, middle_day, zone)
579 },
580 }
581 }
582
583 fn calculate_quarter_boundary(&self, expr: &RelativeExpression, base_date: &CalendarDate, boundary: &BoundaryType, zone: CalClockZone) -> Outcome<CalendarDate> {
584 let current_quarter = ((base_date.month() - 1) / 3) + 1;
585
586 let target_quarter = match expr.reference {
587 RelativeReference::This => current_quarter,
588 RelativeReference::Next => if current_quarter == 4 { 1 } else { current_quarter + 1 },
589 RelativeReference::Last => if current_quarter == 1 { 4 } else { current_quarter - 1 },
590 _ => current_quarter,
591 };
592
593 let (target_year, first_month) = if expr.reference == RelativeReference::Next && current_quarter == 4 {
594 (base_date.year() + 1, 1)
595 } else if expr.reference == RelativeReference::Last && current_quarter == 1 {
596 (base_date.year() - 1, 10)
597 } else {
598 (base_date.year(), (target_quarter - 1) * 3 + 1)
599 };
600
601 match boundary {
602 BoundaryType::Beginning => {
603 let month_enum = res!(MonthOfYear::from_number(first_month));
604 CalendarDate::from_ymd(target_year, month_enum, 1, zone)
605 },
606 BoundaryType::End => {
607 let last_month = first_month + 2;
608 let month_enum = res!(MonthOfYear::from_number(last_month));
609 let days_in_month = month_enum.days_in_month(target_year);
610 CalendarDate::from_ymd(target_year, month_enum, days_in_month, zone)
611 },
612 BoundaryType::Middle => {
613 // Middle of quarter (middle of second month)
614 let middle_month = first_month + 1;
615 let month_enum = res!(MonthOfYear::from_number(middle_month));
616 let days_in_month = month_enum.days_in_month(target_year);
617 let middle_day = (days_in_month + 1) / 2;
618 CalendarDate::from_ymd(target_year, month_enum, middle_day, zone)
619 },
620 }
621 }
622
623 fn calculate_year_boundary(&self, expr: &RelativeExpression, base_date: &CalendarDate, boundary: &BoundaryType, zone: CalClockZone) -> Outcome<CalendarDate> {
624 let target_year = match expr.reference {
625 RelativeReference::This => base_date.year(),
626 RelativeReference::Next => base_date.year() + 1,
627 RelativeReference::Last => base_date.year() - 1,
628 _ => base_date.year(),
629 };
630
631 match boundary {
632 BoundaryType::Beginning => {
633 CalendarDate::from_ymd(target_year, MonthOfYear::January, 1, zone)
634 },
635 BoundaryType::End => {
636 CalendarDate::from_ymd(target_year, MonthOfYear::December, 31, zone)
637 },
638 BoundaryType::Middle => {
639 // Middle of year (July 1st or 2nd depending on leap year)
640 let middle_day = if target_year % 4 == 0 && (target_year % 100 != 0 || target_year % 400 == 0) { 2 } else { 1 };
641 CalendarDate::from_ymd(target_year, MonthOfYear::July, middle_day, zone)
642 },
643 }
644 }
645
646 /// # Examples
647 ///
648 /// ```ignore
649 /// let parser = RelativeDateParser::new();
650 /// let today = CalendarDate::today(CalClockZone::utc()).unwrap();
651 ///
652 /// let next_tuesday = parser.parse_and_calculate("next Tuesday", &today, CalClockZone::utc()).unwrap();
653 /// let in_two_weeks = parser.parse_and_calculate("in 2 weeks", &today, CalClockZone::utc()).unwrap();
654 /// let end_of_month = parser.parse_and_calculate("end of this month", &today, CalClockZone::utc()).unwrap();
655 /// ```
656 pub fn parse_and_calculate(&self, input: &str, base_date: &CalendarDate, zone: CalClockZone) -> Outcome<CalendarDate> {
657 let expression = res!(self.parse(input));
658 self.calculate_date(&expression, base_date, zone)
659 }
660}
661
662#[cfg(test)]
663mod tests {
664 use super::*;
665 use crate::time::CalClockZone;
666
667 fn get_test_base_date() -> CalendarDate {
668 // Wednesday, June 12, 2024
669 CalendarDate::from_ymd(2024, MonthOfYear::June, 12, CalClockZone::utc()).unwrap()
670 }
671
672 #[test]
673 fn test_parse_simple_relative_dates() {
674 let parser = RelativeDateParser::new();
675
676 // Test "next Tuesday"
677 let expr = parser.parse("next Tuesday").unwrap();
678 assert_eq!(expr.reference, RelativeReference::Next);
679 assert_eq!(expr.unit, RelativeUnit::DayOfWeek(DayOfWeek::Tuesday));
680 assert_eq!(expr.direction, Direction::Forward);
681
682 // Test "last Friday"
683 let expr = parser.parse("last Friday").unwrap();
684 assert_eq!(expr.reference, RelativeReference::Last);
685 assert_eq!(expr.unit, RelativeUnit::DayOfWeek(DayOfWeek::Friday));
686 assert_eq!(expr.direction, Direction::Backward);
687
688 // Test "this Monday"
689 let expr = parser.parse("this Monday").unwrap();
690 assert_eq!(expr.reference, RelativeReference::This);
691 assert_eq!(expr.unit, RelativeUnit::DayOfWeek(DayOfWeek::Monday));
692 }
693
694 #[test]
695 fn test_parse_quantified_relative_dates() {
696 let parser = RelativeDateParser::new();
697
698 // Test "in 2 weeks"
699 let expr = parser.parse("in 2 weeks").unwrap();
700 assert_eq!(expr.quantity, Some(2));
701 assert_eq!(expr.unit, RelativeUnit::Week);
702 assert_eq!(expr.direction, Direction::Forward);
703
704 // Test "3 days ago"
705 let expr = parser.parse("3 days ago").unwrap();
706 assert_eq!(expr.quantity, Some(3));
707 assert_eq!(expr.unit, RelativeUnit::Day);
708 assert_eq!(expr.direction, Direction::Backward);
709
710 // Test "2 months from now"
711 let expr = parser.parse("2 months from now").unwrap();
712 assert_eq!(expr.quantity, Some(2));
713 assert_eq!(expr.unit, RelativeUnit::Month);
714 assert_eq!(expr.direction, Direction::Forward);
715 }
716
717 #[test]
718 fn test_parse_period_boundaries() {
719 let parser = RelativeDateParser::new();
720
721 // Test "end of this month"
722 let expr = parser.parse("end of this month").unwrap();
723 assert_eq!(expr.reference, RelativeReference::This);
724 assert!(matches!(expr.unit, RelativeUnit::PeriodBoundary(PeriodType::Month, BoundaryType::End)));
725
726 // Test "beginning of next year"
727 let expr = parser.parse("beginning of next year").unwrap();
728 assert_eq!(expr.reference, RelativeReference::Next);
729 assert!(matches!(expr.unit, RelativeUnit::PeriodBoundary(PeriodType::Year, BoundaryType::Beginning)));
730 }
731
732 #[test]
733 fn test_calculate_day_of_week() {
734 let parser = RelativeDateParser::new();
735 let base_date = get_test_base_date(); // Wednesday, June 12, 2024
736 let zone = CalClockZone::utc();
737
738 // Next Tuesday (June 18, 2024)
739 let next_tuesday = parser.parse_and_calculate("next Tuesday", &base_date, zone.clone()).unwrap();
740 assert_eq!(next_tuesday.day(), 18);
741 assert_eq!(next_tuesday.day_of_week(), DayOfWeek::Tuesday);
742
743 // Last Friday (June 7, 2024)
744 let last_friday = parser.parse_and_calculate("last Friday", &base_date, zone.clone()).unwrap();
745 assert_eq!(last_friday.day(), 7);
746 assert_eq!(last_friday.day_of_week(), DayOfWeek::Friday);
747
748 // This Monday (June 10, 2024)
749 let this_monday = parser.parse_and_calculate("this Monday", &base_date, zone.clone()).unwrap();
750 assert_eq!(this_monday.day(), 10);
751 assert_eq!(this_monday.day_of_week(), DayOfWeek::Monday);
752 }
753
754 #[test]
755 fn test_calculate_quantified_dates() {
756 let parser = RelativeDateParser::new();
757 let base_date = get_test_base_date(); // Wednesday, June 12, 2024
758 let zone = CalClockZone::utc();
759
760 // In 2 weeks (June 26, 2024)
761 let in_two_weeks = parser.parse_and_calculate("in 2 weeks", &base_date, zone.clone()).unwrap();
762 assert_eq!(in_two_weeks.day(), 26);
763 assert_eq!(in_two_weeks.month(), 6);
764
765 // 3 days ago (June 9, 2024)
766 let three_days_ago = parser.parse_and_calculate("3 days ago", &base_date, zone.clone()).unwrap();
767 assert_eq!(three_days_ago.day(), 9);
768 assert_eq!(three_days_ago.month(), 6);
769
770 // 2 months from now (August 12, 2024)
771 let two_months_later = parser.parse_and_calculate("2 months from now", &base_date, zone.clone()).unwrap();
772 assert_eq!(two_months_later.day(), 12);
773 assert_eq!(two_months_later.month(), 8);
774 }
775
776 #[test]
777 fn test_calculate_period_boundaries() {
778 let parser = RelativeDateParser::new();
779 let base_date = get_test_base_date(); // Wednesday, June 12, 2024
780 let zone = CalClockZone::utc();
781
782 // End of this month (June 30, 2024)
783 let end_of_month = parser.parse_and_calculate("end of this month", &base_date, zone.clone()).unwrap();
784 assert_eq!(end_of_month.day(), 30);
785 assert_eq!(end_of_month.month(), 6);
786
787 // Beginning of next month (July 1, 2024)
788 let beginning_next_month = parser.parse_and_calculate("beginning of next month", &base_date, zone.clone()).unwrap();
789 assert_eq!(beginning_next_month.day(), 1);
790 assert_eq!(beginning_next_month.month(), 7);
791
792 // End of this year (December 31, 2024)
793 let end_of_year = parser.parse_and_calculate("end of this year", &base_date, zone.clone()).unwrap();
794 assert_eq!(end_of_year.day(), 31);
795 assert_eq!(end_of_year.month(), 12);
796 assert_eq!(end_of_year.year(), 2024);
797 }
798
799 #[test]
800 fn test_complex_expressions() {
801 let parser = RelativeDateParser::new();
802 let base_date = get_test_base_date(); // Wednesday, June 12, 2024
803 let zone = CalClockZone::utc();
804
805 // The Tuesday after next
806 let expr = parser.parse("after next Tuesday").unwrap();
807 assert_eq!(expr.reference, RelativeReference::AfterNext);
808 assert_eq!(expr.unit, RelativeUnit::DayOfWeek(DayOfWeek::Tuesday));
809
810 let tuesday_after_next = parser.calculate_date(&expr, &base_date, zone.clone()).unwrap();
811 assert_eq!(tuesday_after_next.day(), 25); // June 25, 2024
812 assert_eq!(tuesday_after_next.day_of_week(), DayOfWeek::Tuesday);
813 }
814
815 #[test]
816 fn test_edge_cases() {
817 let parser = RelativeDateParser::new();
818 let zone = CalClockZone::utc();
819
820 // Test from different days of week
821 let sunday = CalendarDate::from_ymd(2024, MonthOfYear::June, 16, zone.clone()).unwrap(); // Sunday
822
823 // Next Monday from Sunday should be tomorrow
824 let next_monday = parser.parse_and_calculate("next Monday", &sunday, zone.clone()).unwrap();
825 assert_eq!(next_monday.day(), 17);
826 assert_eq!(next_monday.day_of_week(), DayOfWeek::Monday);
827
828 // This Monday from Sunday should be tomorrow (since "this week" includes future days)
829 let this_monday = parser.parse_and_calculate("this Monday", &sunday, zone.clone()).unwrap();
830 assert_eq!(this_monday.day(), 17);
831 assert_eq!(this_monday.day_of_week(), DayOfWeek::Monday);
832 }
833}