Skip to main content

whichtime_sys/parsers/ru/
time_unit_relative.rs

1//! Russian time unit relative parser
2//!
3//! Handles Russian relative time expressions like:
4//! - "следующие 2 недели" (next 2 weeks)
5//! - "на этой неделе" (this week)
6//! - "прошлые 3 дня" (past 3 days)
7//! - "5 дней назад" (5 days ago)
8
9use crate::components::Component;
10use crate::context::ParsingContext;
11use crate::dictionaries::ru::{get_time_unit, parse_number_pattern};
12use crate::error::Result;
13use crate::parsers::Parser;
14use crate::results::ParsedResult;
15use crate::types::{Duration, TimeUnit, add_duration};
16use chrono::{Datelike, Timelike};
17use fancy_regex::Regex;
18use std::sync::LazyLock;
19
20// Pattern for "следующие N единиц" (next N units), "прошлые N единиц" (past N units)
21static RELATIVE_PATTERN: LazyLock<Regex> = LazyLock::new(|| {
22    Regex::new(
23        r"(?i)(?<![а-яА-Я])(?:(?P<modifier>следующи[еих]|ближайши[еих]|прошлы[еих]|последни[еих]|предыдущи[еих])\s+)?(?P<num>\d+|один|одна|одну|два|две|три|четыре|пять|шесть|семь|восемь|девять|десять)\s+(?P<unit>секунд[уы]?|минут[уы]?|час(?:ов|а)?|дн(?:ей|я|и|ь)?|день|недел[юиьей]|месяц(?:ев|а)?|год(?:а|ов)?|лет)(?![а-яА-Я])"
24    ).unwrap()
25});
26
27// Pattern for "на этой/следующей/прошлой неделе" (this/next/last week)
28static THIS_WEEK_PATTERN: LazyLock<Regex> = LazyLock::new(|| {
29    Regex::new(
30        r"(?i)(?<![а-яА-Я])(?:на|в)\s+(?P<modifier>это[йм]|следующ(?:ей|ем|ую)|прошло[йм]|будущ(?:ей|ем|ую))\s+(?P<unit>недел[еюи]|месяц[еа]?|году?)(?![а-яА-Я])"
31    ).unwrap()
32});
33
34// Pattern for "N дней/недель/... назад" (N days/weeks/... ago)
35static AGO_PATTERN: LazyLock<Regex> = LazyLock::new(|| {
36    Regex::new(
37        r"(?i)(?<![а-яА-Я])(?P<num>\d+|один|одна|одну|два|две|три|четыре|пять|шесть|семь|восемь|девять|десять)\s+(?P<unit>секунд[уы]?|минут[уы]?|час(?:ов|а)?|дн(?:ей|я|и|ь)?|день|недел[юиьей]|месяц(?:ев|а)?|год(?:а|ов)?|лет)\s+назад(?![а-яА-Я])"
38    ).unwrap()
39});
40
41/// Russian time unit relative parser
42pub struct RUTimeUnitRelativeParser;
43
44impl RUTimeUnitRelativeParser {
45    pub fn new() -> Self {
46        Self
47    }
48
49    fn parse_unit(unit_str: &str) -> Option<TimeUnit> {
50        let lower = unit_str.to_lowercase();
51        get_time_unit(&lower)
52    }
53}
54
55impl Default for RUTimeUnitRelativeParser {
56    fn default() -> Self {
57        Self::new()
58    }
59}
60
61impl Parser for RUTimeUnitRelativeParser {
62    fn name(&self) -> &'static str {
63        "RUTimeUnitRelativeParser"
64    }
65
66    fn should_apply(&self, _context: &ParsingContext) -> bool {
67        true
68    }
69
70    fn parse(&self, context: &ParsingContext) -> Result<Vec<ParsedResult>> {
71        let mut results = Vec::new();
72        let ref_date = context.reference.instant;
73
74        // Parse "N дней назад" patterns (time units ago)
75        let mut start = 0;
76        while start < context.text.len() {
77            let search_text = &context.text[start..];
78            let captures = match AGO_PATTERN.captures(search_text) {
79                Ok(Some(caps)) => caps,
80                Ok(None) => break,
81                Err(_) => break,
82            };
83
84            let full_match = match captures.get(0) {
85                Some(m) => m,
86                None => break,
87            };
88
89            let match_start = start + full_match.start();
90            let match_end = start + full_match.end();
91
92            let num_str = captures.name("num").map(|m| m.as_str()).unwrap_or("1");
93            let unit_str = captures
94                .name("unit")
95                .map(|m| m.as_str())
96                .unwrap_or_default();
97
98            let num = parse_number_pattern(num_str);
99            if let Some(unit) = Self::parse_unit(unit_str) {
100                let mut duration = Duration::new();
101                match unit {
102                    TimeUnit::Second => duration.second = Some(-num),
103                    TimeUnit::Minute => duration.minute = Some(-num),
104                    TimeUnit::Hour => duration.hour = Some(-num),
105                    TimeUnit::Day => duration.day = Some(-num),
106                    TimeUnit::Week => duration.week = Some(-num),
107                    TimeUnit::Month => duration.month = Some(-num),
108                    TimeUnit::Year => duration.year = Some(-num),
109                    _ => {}
110                }
111
112                let target_date = add_duration(ref_date, &duration);
113
114                let mut components = context.create_components();
115                components.assign(Component::Year, target_date.year());
116                components.assign(Component::Month, target_date.month() as i32);
117                components.assign(Component::Day, target_date.day() as i32);
118
119                if duration.has_time_component() {
120                    components.assign(Component::Hour, target_date.hour() as i32);
121                    components.assign(Component::Minute, target_date.minute() as i32);
122                    components.assign(Component::Second, target_date.second() as i32);
123                }
124
125                results.push(context.create_result(match_start, match_end, components, None));
126            }
127
128            start = match_end;
129        }
130
131        // Parse "следующие N единиц" patterns (relative future)
132        start = 0;
133        while start < context.text.len() {
134            let search_text = &context.text[start..];
135            let captures = match RELATIVE_PATTERN.captures(search_text) {
136                Ok(Some(caps)) => caps,
137                Ok(None) => break,
138                Err(_) => break,
139            };
140
141            let full_match = match captures.get(0) {
142                Some(m) => m,
143                None => break,
144            };
145
146            let match_start = start + full_match.start();
147            let match_end = start + full_match.end();
148
149            // Skip if overlaps with existing results
150            let overlaps = results.iter().any(|r| {
151                (match_start >= r.index && match_start < r.index + r.text.len())
152                    || (r.index >= match_start && r.index < match_end)
153            });
154            if overlaps {
155                start = match_end;
156                continue;
157            }
158
159            let modifier = captures.name("modifier").map(|m| m.as_str().to_lowercase());
160            let num_str = captures.name("num").map(|m| m.as_str()).unwrap_or("1");
161            let unit_str = captures
162                .name("unit")
163                .map(|m| m.as_str())
164                .unwrap_or_default();
165
166            let num = parse_number_pattern(num_str);
167            if let Some(unit) = Self::parse_unit(unit_str) {
168                // Determine direction based on modifier
169                let is_past = modifier.as_ref().is_some_and(|m| {
170                    m.starts_with("прошл") || m.starts_with("последн") || m.starts_with("предыдущ")
171                });
172
173                let multiplier = if is_past { -1.0 } else { 1.0 };
174                let adjusted_num = num * multiplier;
175
176                let mut duration = Duration::new();
177                match unit {
178                    TimeUnit::Second => duration.second = Some(adjusted_num),
179                    TimeUnit::Minute => duration.minute = Some(adjusted_num),
180                    TimeUnit::Hour => duration.hour = Some(adjusted_num),
181                    TimeUnit::Day => duration.day = Some(adjusted_num),
182                    TimeUnit::Week => duration.week = Some(adjusted_num),
183                    TimeUnit::Month => duration.month = Some(adjusted_num),
184                    TimeUnit::Year => duration.year = Some(adjusted_num),
185                    _ => {}
186                }
187
188                let target_date = add_duration(ref_date, &duration);
189
190                let mut components = context.create_components();
191                components.assign(Component::Year, target_date.year());
192                components.assign(Component::Month, target_date.month() as i32);
193                components.assign(Component::Day, target_date.day() as i32);
194
195                if duration.has_time_component() {
196                    components.assign(Component::Hour, target_date.hour() as i32);
197                    components.assign(Component::Minute, target_date.minute() as i32);
198                    components.assign(Component::Second, target_date.second() as i32);
199                } else {
200                    components.imply(Component::Hour, ref_date.hour() as i32);
201                    components.imply(Component::Minute, ref_date.minute() as i32);
202                }
203
204                results.push(context.create_result(match_start, match_end, components, None));
205            }
206
207            start = match_end;
208        }
209
210        // Parse "на этой/следующей неделе" patterns
211        start = 0;
212        while start < context.text.len() {
213            let search_text = &context.text[start..];
214            let captures = match THIS_WEEK_PATTERN.captures(search_text) {
215                Ok(Some(caps)) => caps,
216                Ok(None) => break,
217                Err(_) => break,
218            };
219
220            let full_match = match captures.get(0) {
221                Some(m) => m,
222                None => break,
223            };
224
225            let match_start = start + full_match.start();
226            let match_end = start + full_match.end();
227
228            // Skip if overlaps with existing results
229            let overlaps = results.iter().any(|r| {
230                (match_start >= r.index && match_start < r.index + r.text.len())
231                    || (r.index >= match_start && r.index < match_end)
232            });
233            if overlaps {
234                start = match_end;
235                continue;
236            }
237
238            let modifier = captures
239                .name("modifier")
240                .map(|m| m.as_str().to_lowercase())
241                .unwrap_or_default();
242            let unit_str = captures
243                .name("unit")
244                .map(|m| m.as_str())
245                .unwrap_or_default();
246
247            // Determine offset based on modifier
248            let offset = if modifier.starts_with("это") {
249                0 // this week/month/year
250            } else if modifier.starts_with("следующ") || modifier.starts_with("будущ") {
251                1 // next week/month/year
252            } else if modifier.starts_with("прошл") {
253                -1 // last week/month/year
254            } else {
255                0
256            };
257
258            // Determine unit
259            let unit = if unit_str.starts_with("недел") {
260                Some(TimeUnit::Week)
261            } else if unit_str.starts_with("месяц") {
262                Some(TimeUnit::Month)
263            } else if unit_str.starts_with("год") {
264                Some(TimeUnit::Year)
265            } else {
266                None
267            };
268
269            if let Some(time_unit) = unit {
270                let mut duration = Duration::new();
271                match time_unit {
272                    TimeUnit::Week => duration.week = Some(offset as f64),
273                    TimeUnit::Month => duration.month = Some(offset as f64),
274                    TimeUnit::Year => duration.year = Some(offset as f64),
275                    _ => {}
276                }
277
278                let target_date = add_duration(ref_date, &duration);
279
280                let mut components = context.create_components();
281                components.assign(Component::Year, target_date.year());
282                components.assign(Component::Month, target_date.month() as i32);
283                components.assign(Component::Day, target_date.day() as i32);
284                components.imply(Component::Hour, ref_date.hour() as i32);
285                components.imply(Component::Minute, ref_date.minute() as i32);
286
287                results.push(context.create_result(match_start, match_end, components, None));
288            }
289
290            start = match_end;
291        }
292
293        Ok(results)
294    }
295}