Skip to main content

whichtime_sys/parsers/uk/
casual_date.rs

1//! Ukrainian casual date parser
2//!
3//! Handles Ukrainian casual date expressions like:
4//! - "сьогодні", "завтра", "вчора"
5//! - "завтра вранці", "вчора ввечері"
6//! - "завтра опівдні"
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)(?<![а-яА-ЯіїєґІЇЄҐ'])(?:(?P<date>зараз|сьогодні|сьогодни|завтра|післязавтра|вчора|позавчора)(?:\s+(?P<time>вранці|зранку|опівдні|вдень|ввечері|увечері|вночі|опівночі))?|(?P<time_only>вранці|зранку|опівдні|вдень|ввечері|увечері|вночі|опівночі))(?![а-яА-ЯіїєґІЇЄҐ'])"
21    ).unwrap()
22});
23
24/// Ukrainian casual date parser
25pub struct UKCasualDateParser;
26
27impl UKCasualDateParser {
28    pub fn new() -> Self {
29        Self
30    }
31
32    fn assign_time_part(components: &mut crate::components::FastComponents, time_part: &str) {
33        match time_part.to_lowercase().as_str() {
34            "вранці" | "зранку" => {
35                components.assign(Component::Hour, 6);
36                components.assign(Component::Minute, 0);
37                components.assign(Component::Meridiem, Meridiem::AM as i32);
38            }
39            "опівдні" => {
40                components.assign(Component::Hour, 12);
41                components.assign(Component::Minute, 0);
42                components.assign(Component::Meridiem, Meridiem::PM as i32);
43            }
44            "вдень" => {
45                components.assign(Component::Hour, 14);
46                components.assign(Component::Minute, 0);
47                components.assign(Component::Meridiem, Meridiem::PM as i32);
48            }
49            "ввечері" | "увечері" => {
50                components.assign(Component::Hour, 20);
51                components.assign(Component::Minute, 0);
52                components.assign(Component::Meridiem, Meridiem::PM as i32);
53            }
54            "вночі" => {
55                components.assign(Component::Hour, 2);
56                components.assign(Component::Minute, 0);
57                components.assign(Component::Meridiem, Meridiem::AM as i32);
58            }
59            "опівночі" => {
60                components.assign(Component::Hour, 0);
61                components.assign(Component::Minute, 0);
62                components.assign(Component::Second, 0);
63            }
64            _ => {}
65        }
66    }
67}
68
69impl Default for UKCasualDateParser {
70    fn default() -> Self {
71        Self::new()
72    }
73}
74
75impl Parser for UKCasualDateParser {
76    fn name(&self) -> &'static str {
77        "UKCasualDateParser"
78    }
79
80    fn should_apply(&self, _context: &ParsingContext) -> bool {
81        true
82    }
83
84    fn parse(&self, context: &ParsingContext) -> Result<Vec<ParsedResult>> {
85        let mut results = Vec::new();
86        let ref_date = context.reference.instant;
87
88        let mut start = 0;
89        while start < context.text.len() {
90            let search_text = &context.text[start..];
91            let captures = match PATTERN.captures(search_text) {
92                Ok(Some(caps)) => caps,
93                Ok(None) => break,
94                Err(_) => break,
95            };
96
97            let full_match = match captures.get(0) {
98                Some(m) => m,
99                None => break,
100            };
101
102            let match_start = start + full_match.start();
103            let match_end = start + full_match.end();
104
105            let date_keyword = captures.name("date").map(|m| m.as_str().to_lowercase());
106            let time_part = captures.name("time").map(|m| m.as_str());
107            let time_only = captures.name("time_only").map(|m| m.as_str());
108
109            let mut components = context.create_components();
110            let mut target_date = ref_date;
111
112            if let Some(ref kw) = date_keyword {
113                match kw.as_str() {
114                    "зараз" => {
115                        components.assign(Component::Hour, ref_date.hour() as i32);
116                        components.assign(Component::Minute, ref_date.minute() as i32);
117                        components.assign(Component::Second, ref_date.second() as i32);
118                    }
119                    "сьогодні" | "сьогодни" => {}
120                    "завтра" => {
121                        target_date = ref_date + Duration::days(1);
122                    }
123                    "післязавтра" => {
124                        target_date = ref_date + Duration::days(2);
125                    }
126                    "вчора" => {
127                        target_date = ref_date - Duration::days(1);
128                    }
129                    "позавчора" => {
130                        target_date = ref_date - Duration::days(2);
131                    }
132                    _ => {}
133                }
134            }
135
136            components.assign(Component::Year, target_date.year());
137            components.assign(Component::Month, target_date.month() as i32);
138            components.assign(Component::Day, target_date.day() as i32);
139
140            // Apply time part if present
141            if let Some(tp) = time_part.or(time_only) {
142                Self::assign_time_part(&mut components, tp);
143            }
144
145            results.push(context.create_result(match_start, match_end, components, None));
146            start = match_end;
147        }
148
149        Ok(results)
150    }
151}