Skip to main content

whichtime_sys/parsers/ru/
casual_date.rs

1//! Russian casual date parser
2//!
3//! Handles Russian casual date expressions like:
4//! - "сегодня", "завтра", "вчера"
5//! - "сегодня вечером", "завтра утром"
6//! - "вчера в 18:00"
7
8use crate::components::Component;
9use crate::context::ParsingContext;
10use crate::error::Result;
11use crate::parsers::Parser;
12use crate::results::ParsedResult;
13use crate::types::Meridiem;
14use chrono::{Datelike, Duration, Timelike};
15use fancy_regex::Regex;
16use std::sync::LazyLock;
17
18static PATTERN: LazyLock<Regex> = LazyLock::new(|| {
19    Regex::new(
20        r"(?i)(?<![a-zA-Zа-яА-Я])(?:(сейчас|сегодня|завтра|послезавтра|вчера|позавчера)(?:\s+(утром|днем|днём|вечером|ночью))?|(?P<time_only>утром|днем|днём|вечером|ночью))(?:\s+(?:в|к)\s+)?(?:\s*(?P<noon>полдень|полудень|полночь))?(?:\s+(\d{1,2})(?::(\d{1,2}))?(?:\s*ч(?:\.|асов)?)?)?(?=\W|$)"
21    ).unwrap()
22});
23
24const DATE_GROUP: usize = 1;
25const TIME_PART_GROUP: usize = 2;
26const TIME_ONLY_GROUP: usize = 3;
27const NOON_GROUP: usize = 4;
28const HOUR_GROUP: usize = 5;
29const MINUTE_GROUP: usize = 6;
30
31/// Russian casual date parser
32pub struct RUCasualDateParser;
33
34impl RUCasualDateParser {
35    pub fn new() -> Self {
36        Self
37    }
38
39    fn assign_time_part(components: &mut crate::components::FastComponents, time_part: &str) {
40        match time_part {
41            "утром" => {
42                components.imply(Component::Hour, 6);
43                components.imply(Component::Minute, 0);
44                components.imply(Component::Second, 0);
45                components.assign(Component::Meridiem, Meridiem::AM as i32);
46            }
47            "днем" | "днём" => {
48                components.imply(Component::Hour, 14);
49                components.imply(Component::Minute, 0);
50                components.imply(Component::Second, 0);
51                components.assign(Component::Meridiem, Meridiem::PM as i32);
52            }
53            "вечером" => {
54                components.imply(Component::Hour, 20);
55                components.imply(Component::Minute, 0);
56                components.imply(Component::Second, 0);
57                components.assign(Component::Meridiem, Meridiem::PM as i32);
58            }
59            "ночью" => {
60                components.imply(Component::Hour, 23);
61                components.imply(Component::Minute, 0);
62                components.imply(Component::Second, 0);
63                components.assign(Component::Meridiem, Meridiem::PM as i32);
64            }
65            _ => {}
66        }
67    }
68}
69
70impl Parser for RUCasualDateParser {
71    fn name(&self) -> &'static str {
72        "RUCasualDateParser"
73    }
74
75    fn should_apply(&self, _context: &ParsingContext) -> bool {
76        true
77    }
78
79    fn parse(&self, context: &ParsingContext) -> Result<Vec<ParsedResult>> {
80        let mut results = Vec::new();
81        let ref_date = context.reference.instant;
82
83        let mut start = 0;
84        while start < context.text.len() {
85            let search_text = &context.text[start..];
86            let captures = match PATTERN.captures(search_text) {
87                Ok(Some(caps)) => caps,
88                Ok(None) => break,
89                Err(_) => break,
90            };
91
92            let full_match = match captures.get(0) {
93                Some(m) => m,
94                None => break,
95            };
96
97            let match_start = start + full_match.start();
98            let match_end = start + full_match.end();
99
100            let date_keyword = captures.get(DATE_GROUP).map(|m| m.as_str().to_lowercase());
101
102            let time_part_opt = captures
103                .get(TIME_PART_GROUP)
104                .map(|m| m.as_str().to_lowercase());
105
106            let time_only = captures
107                .get(TIME_ONLY_GROUP)
108                .map(|m| m.as_str().to_lowercase());
109
110            let time_part = time_part_opt.or(time_only);
111
112            let noon_part = captures.get(NOON_GROUP).map(|m| m.as_str().to_lowercase());
113
114            let explicit_hour: Option<i32> = captures
115                .get(HOUR_GROUP)
116                .and_then(|m| m.as_str().parse().ok());
117
118            let explicit_minute: Option<i32> = captures
119                .get(MINUTE_GROUP)
120                .and_then(|m| m.as_str().parse().ok());
121
122            let mut components = context.create_components();
123            let mut target_date = ref_date;
124
125            if let Some(kw) = date_keyword {
126                match kw.as_str() {
127                    "сегодня" => {}
128                    "завтра" => {
129                        target_date = ref_date + Duration::days(1);
130                    }
131                    "послезавтра" => {
132                        target_date = ref_date + Duration::days(2);
133                    }
134                    "вчера" => {
135                        target_date = ref_date - Duration::days(1);
136                    }
137                    "позавчера" => {
138                        target_date = ref_date - Duration::days(2);
139                    }
140                    "сейчас" => {
141                        components.assign(Component::Hour, ref_date.hour() as i32);
142                        components.assign(Component::Minute, ref_date.minute() as i32);
143                        components.assign(Component::Second, ref_date.second() as i32);
144                    }
145                    _ => {}
146                }
147            }
148
149            components.assign(Component::Year, target_date.year());
150            components.assign(Component::Month, target_date.month() as i32);
151            components.assign(Component::Day, target_date.day() as i32);
152
153            // Apply time part implications
154            if let Some(ref tp) = time_part {
155                Self::assign_time_part(&mut components, tp);
156            }
157
158            // Explicit hour overrides
159            if let Some(hour) = explicit_hour {
160                let mut adjusted_hour = hour;
161
162                // Context-based adjustment
163                if let Some(ref tp) = time_part {
164                    match tp.as_str() {
165                        "вечером" | "днем" | "днём" => {
166                            // For 12-hour numbers, convert to PM (1 -> 13)
167                            // But if number is e.g. 18, keep it.
168                            if adjusted_hour < 12 {
169                                adjusted_hour += 12;
170                            }
171                        }
172                        "ночью" => {
173                            if adjusted_hour < 6 {
174                                // "ночью в 2" usually means 2am
175                            } else if adjusted_hour < 12 {
176                                // "ночью в 11" -> 23:00
177                                adjusted_hour += 12;
178                            }
179                        }
180                        _ => {}
181                    }
182                } else if adjusted_hour < 12 {
183                    // No explicit time part context.
184                }
185
186                components.assign(Component::Hour, adjusted_hour);
187                components.assign(Component::Minute, explicit_minute.unwrap_or(0));
188            }
189
190            // Check for "полдень"/"полночь"
191            if let Some(noon) = noon_part {
192                if noon.contains("полдень") || noon.contains("полудень") {
193                    components.assign(Component::Hour, 12);
194                    components.assign(Component::Minute, 0);
195                    components.assign(Component::Meridiem, Meridiem::PM as i32);
196                } else if noon.contains("полночь") {
197                    // Midnight usually implies start of next day if mentioned as a target time "at midnight"
198                    // But if just "midnight", can be ambiguous.
199                    // Test `test_casual_v_polden` ("в полдень") expects noon.
200                    // If no date keyword, imply today? Yes.
201                    // If combined with "завтра в полдень", date is tomorrow.
202
203                    components.assign(Component::Hour, 0);
204                    components.assign(Component::Minute, 0);
205                    components.assign(Component::Meridiem, Meridiem::AM as i32);
206                }
207            }
208
209            results.push(context.create_result(match_start, match_end, components, None));
210
211            start = match_end;
212        }
213
214        Ok(results)
215    }
216}
217
218impl Default for RUCasualDateParser {
219    fn default() -> Self {
220        Self::new()
221    }
222}