Skip to main content

whichtime_sys/parsers/zh/
casual_date.rs

1//! Chinese casual date parser
2//!
3//! Handles Chinese casual date expressions like:
4//! - "今天", "今日" (today)
5//! - "明天", "明日" (tomorrow)
6//! - "昨天", "昨日" (yesterday)
7//! - "今晚", "今夜" (tonight)
8//! - "而家" (now - Cantonese)
9//! - "聽日" (tomorrow - Cantonese)
10//! - Combined: "今天下午5点", "明天早上8点"
11
12use crate::components::Component;
13use crate::context::ParsingContext;
14use crate::dictionaries::zh::{NUMBER_MAP, parse_number_pattern};
15use crate::error::Result;
16use crate::parsers::Parser;
17use crate::results::ParsedResult;
18use crate::types::Meridiem;
19use chrono::{Datelike, Duration, Timelike};
20use fancy_regex::Regex;
21use std::sync::LazyLock;
22
23// Pattern for casual date with optional time period and optional explicit time
24static PATTERN: LazyLock<Regex> = LazyLock::new(|| {
25    Regex::new(
26        r"(?P<date>今天|今日|今晚|今夜|明天|明日|昨天|昨日|后天|後天|前天|现在|現在|而家|聽日|尋日|琴日)(?P<time_part>早上|早晨|上午|中午|正午|下午|傍晚|晚上|晚间|晚間|夜里|夜裡|夜晚|凌晨|午夜|半夜)?(?:(?P<hour>[0-9一二三四五六七八九十零〇两兩]+)(?:点|點))?"
27    ).unwrap()
28});
29
30// Pattern for standalone time period (implies today)
31static TIME_ONLY_PATTERN: LazyLock<Regex> = LazyLock::new(|| {
32    Regex::new(
33        r"(?P<time_only>早上|早晨|上午|中午|正午|下午|傍晚|晚上|晚间|晚間|夜里|夜裡|夜晚|凌晨|午夜|半夜)"
34    ).unwrap()
35});
36
37/// Chinese casual date parser
38pub struct ZHCasualDateParser;
39
40impl ZHCasualDateParser {
41    pub fn new() -> Self {
42        Self
43    }
44
45    fn get_time_period_hour(period: &str) -> Option<(i32, Option<Meridiem>)> {
46        match period {
47            "早上" | "早晨" | "上午" => Some((6, Some(Meridiem::AM))),
48            "中午" | "正午" => Some((12, Some(Meridiem::PM))),
49            "下午" => Some((15, Some(Meridiem::PM))),
50            "傍晚" => Some((18, Some(Meridiem::PM))),
51            "晚上" | "晚间" | "晚間" => Some((22, Some(Meridiem::PM))),
52            "夜里" | "夜裡" | "夜晚" => Some((22, Some(Meridiem::PM))),
53            "凌晨" | "午夜" | "半夜" => Some((0, Some(Meridiem::AM))),
54            _ => None,
55        }
56    }
57
58    fn parse_hour(s: &str) -> i32 {
59        // First check if it's a single character Chinese number
60        if let Some(&val) = NUMBER_MAP.get(s) {
61            return val as i32;
62        }
63        // Then try full conversion
64        parse_number_pattern(s) as i32
65    }
66}
67
68impl Parser for ZHCasualDateParser {
69    fn name(&self) -> &'static str {
70        "ZHCasualDateParser"
71    }
72
73    fn should_apply(&self, _context: &ParsingContext) -> bool {
74        true
75    }
76
77    fn parse(&self, context: &ParsingContext) -> Result<Vec<ParsedResult>> {
78        let mut results = Vec::new();
79        let ref_date = context.reference.instant;
80
81        let mut start = 0;
82        while start < context.text.len() {
83            let search_text = &context.text[start..];
84
85            // First try the main pattern with date keywords
86            if let Ok(Some(caps)) = PATTERN.captures(search_text) {
87                let full_match = caps.get(0).unwrap();
88                let match_start = start + full_match.start();
89                let match_end = start + full_match.end();
90
91                let date_keyword = caps.name("date").map(|m| m.as_str()).unwrap_or_default();
92
93                let time_part = caps.name("time_part").map(|m| m.as_str());
94                let hour_str = caps.name("hour").map(|m| m.as_str());
95
96                let mut components = context.create_components();
97                let target_date;
98
99                // Process date keyword
100                match date_keyword {
101                    "今天" | "今日" => {
102                        components.assign(Component::Year, ref_date.year());
103                        components.assign(Component::Month, ref_date.month() as i32);
104                        components.assign(Component::Day, ref_date.day() as i32);
105                    }
106                    "明天" | "明日" | "聽日" => {
107                        target_date = ref_date + Duration::days(1);
108                        components.assign(Component::Year, target_date.year());
109                        components.assign(Component::Month, target_date.month() as i32);
110                        components.assign(Component::Day, target_date.day() as i32);
111                    }
112                    "昨天" | "昨日" | "尋日" | "琴日" => {
113                        target_date = ref_date - Duration::days(1);
114                        components.assign(Component::Year, target_date.year());
115                        components.assign(Component::Month, target_date.month() as i32);
116                        components.assign(Component::Day, target_date.day() as i32);
117                    }
118                    "后天" | "後天" => {
119                        target_date = ref_date + Duration::days(2);
120                        components.assign(Component::Year, target_date.year());
121                        components.assign(Component::Month, target_date.month() as i32);
122                        components.assign(Component::Day, target_date.day() as i32);
123                    }
124                    "前天" => {
125                        target_date = ref_date - Duration::days(2);
126                        components.assign(Component::Year, target_date.year());
127                        components.assign(Component::Month, target_date.month() as i32);
128                        components.assign(Component::Day, target_date.day() as i32);
129                    }
130                    "今晚" | "今夜" => {
131                        components.assign(Component::Year, ref_date.year());
132                        components.assign(Component::Month, ref_date.month() as i32);
133                        components.assign(Component::Day, ref_date.day() as i32);
134                        components.imply(Component::Hour, 22);
135                        components.assign(Component::Meridiem, Meridiem::PM as i32);
136                    }
137                    "现在" | "現在" | "而家" => {
138                        components.assign(Component::Year, ref_date.year());
139                        components.assign(Component::Month, ref_date.month() as i32);
140                        components.assign(Component::Day, ref_date.day() as i32);
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                        start = match_end;
147                        continue;
148                    }
149                }
150
151                // Apply time period if present
152                if let Some(period) = time_part
153                    && let Some((hour, meridiem)) = Self::get_time_period_hour(period)
154                {
155                    // If we also have explicit hour, use it with the meridiem adjustment
156                    if let Some(h_str) = hour_str {
157                        let mut h = Self::parse_hour(h_str);
158                        // Apply meridiem adjustment
159                        if let Some(Meridiem::PM) = meridiem
160                            && h < 12
161                        {
162                            h += 12;
163                        }
164                        components.assign(Component::Hour, h);
165                    } else {
166                        components.imply(Component::Hour, hour);
167                    }
168                    if let Some(m) = meridiem {
169                        components.assign(Component::Meridiem, m as i32);
170                    }
171                }
172
173                results.push(context.create_result(match_start, match_end, components, None));
174                start = match_end;
175                continue;
176            }
177
178            // Try standalone time period (implies today)
179            if let Ok(Some(caps)) = TIME_ONLY_PATTERN.captures(search_text) {
180                let full_match = caps.get(0).unwrap();
181                let match_start = start + full_match.start();
182                let match_end = start + full_match.end();
183
184                let time_only = caps.name("time_only").map(|m| m.as_str()).unwrap_or("");
185
186                if let Some((hour, meridiem)) = Self::get_time_period_hour(time_only) {
187                    let mut components = context.create_components();
188                    components.assign(Component::Year, ref_date.year());
189                    components.assign(Component::Month, ref_date.month() as i32);
190                    components.assign(Component::Day, ref_date.day() as i32);
191                    components.imply(Component::Hour, hour);
192                    if let Some(m) = meridiem {
193                        components.assign(Component::Meridiem, m as i32);
194                    }
195
196                    results.push(context.create_result(match_start, match_end, components, None));
197                    start = match_end;
198                    continue;
199                }
200            }
201
202            // No match - advance
203            if let Some(c) = search_text.chars().next() {
204                start += c.len_utf8();
205            } else {
206                break;
207            }
208        }
209
210        Ok(results)
211    }
212}
213
214impl Default for ZHCasualDateParser {
215    fn default() -> Self {
216        Self::new()
217    }
218}