Skip to main content

whichtime_sys/parsers/en/
relative.rs

1//! Relative date parser: "this week", "next month", "last year", etc.
2
3use crate::components::Component;
4use crate::context::ParsingContext;
5use crate::dictionaries::RelativeModifier;
6use crate::dictionaries::en::{get_relative_modifier, get_time_unit, parse_number_pattern};
7use crate::error::Result;
8use crate::parsers::Parser;
9use crate::results::ParsedResult;
10use crate::scanner::TokenType;
11use crate::types::{Duration, TimeUnit, add_duration};
12use chrono::Datelike;
13use regex::Regex;
14use std::sync::LazyLock;
15
16static PATTERN: LazyLock<Regex> = LazyLock::new(|| {
17    Regex::new(
18        r"(?i)\b(this|next|last|past|previous)\s*(week|month|year|quarter|hour|minute|day)\b",
19    )
20    .unwrap()
21});
22
23// Pattern for "next 2 weeks", "last 3 months"
24static NUMBERED_PATTERN: LazyLock<Regex> = LazyLock::new(|| {
25    Regex::new(
26        r"(?i)\b(next|last|past)\s+(\d+|one|two|three|four|five|six|seven|eight|nine|ten|eleven|twelve)\s*(weeks?|months?|years?|quarters?|hours?|minutes?|days?)\b"
27    ).unwrap()
28});
29
30/// Parser for English relative calendar periods such as "next month".
31pub struct RelativeDateParser;
32
33impl Parser for RelativeDateParser {
34    fn name(&self) -> &'static str {
35        "RelativeDateParser"
36    }
37
38    fn should_apply(&self, context: &ParsingContext) -> bool {
39        context.has_token_type(TokenType::RelativeModifier)
40            && context.has_token_type(TokenType::TimeUnit)
41    }
42
43    fn parse(&self, context: &ParsingContext) -> Result<Vec<ParsedResult>> {
44        let mut results = Vec::new();
45        let ref_date = context.reference.instant;
46
47        // Try numbered pattern first ("next 2 weeks")
48        for mat in NUMBERED_PATTERN.find_iter(context.text) {
49            let matched_text = mat.as_str();
50            let index = mat.start();
51
52            let Some(caps) = NUMBERED_PATTERN.captures(matched_text) else {
53                continue;
54            };
55
56            let modifier_str = caps
57                .get(1)
58                .map(|m| m.as_str().to_lowercase())
59                .unwrap_or_default();
60            let num_str = caps.get(2).map(|m| m.as_str()).unwrap_or("1");
61            let unit_str = caps
62                .get(3)
63                .map(|m| m.as_str().to_lowercase())
64                .unwrap_or_default();
65
66            let Some(modifier) = get_relative_modifier(&modifier_str) else {
67                continue;
68            };
69            let num = parse_number_pattern(num_str);
70            let Some(unit) = get_time_unit(&unit_str) else {
71                continue;
72            };
73
74            let multiplier = match modifier {
75                RelativeModifier::Next => 1.0,
76                RelativeModifier::Last => -1.0,
77                RelativeModifier::This => 0.0,
78            };
79
80            let mut duration = Duration::new();
81            let value = num * multiplier;
82            match unit {
83                TimeUnit::Second => duration.second = Some(value),
84                TimeUnit::Minute => duration.minute = Some(value),
85                TimeUnit::Hour => duration.hour = Some(value),
86                TimeUnit::Day => duration.day = Some(value),
87                TimeUnit::Week => duration.week = Some(value),
88                TimeUnit::Month => duration.month = Some(value),
89                TimeUnit::Year => duration.year = Some(value),
90                TimeUnit::Quarter => duration.quarter = Some(value),
91                TimeUnit::Millisecond => duration.millisecond = Some(value),
92            }
93
94            let target_date = add_duration(ref_date, &duration);
95
96            let mut components = context.create_components();
97            components.assign(Component::Year, target_date.year());
98            components.assign(Component::Month, target_date.month() as i32);
99            components.assign(Component::Day, target_date.day() as i32);
100
101            if duration.has_time_component() {
102                use chrono::Timelike;
103                components.assign(Component::Hour, target_date.hour() as i32);
104                components.assign(Component::Minute, target_date.minute() as i32);
105            }
106
107            results.push(context.create_result(
108                index,
109                index + matched_text.len(),
110                components,
111                None,
112            ));
113        }
114
115        // Try simple pattern ("this week", "next month")
116        for mat in PATTERN.find_iter(context.text) {
117            let matched_text = mat.as_str();
118            let index = mat.start();
119
120            // Skip if already matched
121            if results
122                .iter()
123                .any(|r| r.index <= index && r.end_index > index)
124            {
125                continue;
126            }
127
128            let Some(caps) = PATTERN.captures(matched_text) else {
129                continue;
130            };
131
132            let modifier_str = caps
133                .get(1)
134                .map(|m| m.as_str().to_lowercase())
135                .unwrap_or_default();
136            let unit_str = caps
137                .get(2)
138                .map(|m| m.as_str().to_lowercase())
139                .unwrap_or_default();
140
141            let Some(modifier) = get_relative_modifier(&modifier_str) else {
142                continue;
143            };
144            let Some(unit) = get_time_unit(&unit_str) else {
145                continue;
146            };
147
148            let mut duration = Duration::new();
149            let value = match modifier {
150                RelativeModifier::Next => 1.0,
151                RelativeModifier::Last => -1.0,
152                RelativeModifier::This => 0.0,
153            };
154
155            match unit {
156                TimeUnit::Hour => duration.hour = Some(value),
157                TimeUnit::Day => duration.day = Some(value),
158                TimeUnit::Week => duration.week = Some(value),
159                TimeUnit::Month => duration.month = Some(value),
160                TimeUnit::Year => duration.year = Some(value),
161                TimeUnit::Quarter => duration.quarter = Some(value),
162                _ => continue,
163            }
164
165            let target_date = add_duration(ref_date, &duration);
166
167            let mut components = context.create_components();
168
169            // For "this week/month/year", set to start of period
170            match unit {
171                TimeUnit::Week => {
172                    components.assign(Component::Year, target_date.year());
173                    components.assign(Component::Month, target_date.month() as i32);
174                    components.assign(Component::Day, target_date.day() as i32);
175                }
176                TimeUnit::Month => {
177                    components.assign(Component::Year, target_date.year());
178                    components.assign(Component::Month, target_date.month() as i32);
179                    components.assign(Component::Day, 1);
180                }
181                TimeUnit::Year => {
182                    components.assign(Component::Year, target_date.year());
183                    components.assign(Component::Month, 1);
184                    components.assign(Component::Day, 1);
185                }
186                _ => {
187                    components.assign(Component::Year, target_date.year());
188                    components.assign(Component::Month, target_date.month() as i32);
189                    components.assign(Component::Day, target_date.day() as i32);
190                    if duration.has_time_component() {
191                        use chrono::Timelike;
192                        components.assign(Component::Hour, target_date.hour() as i32);
193                        components.assign(Component::Minute, target_date.minute() as i32);
194                    }
195                }
196            }
197
198            results.push(context.create_result(
199                index,
200                index + matched_text.len(),
201                components,
202                None,
203            ));
204        }
205
206        Ok(results)
207    }
208}