Skip to main content

whichtime_sys/parsers/zh/
time_expression.rs

1//! Chinese time expression parser
2//!
3//! Handles Chinese time formats like:
4//! - "上午6点13分" (AM 6:13)
5//! - "下午8点" (PM 8:00)
6//! - "6点30分" (6:30)
7//! - "下午三點半" (PM 3:30 with traditional characters)
8
9use crate::components::Component;
10use crate::context::ParsingContext;
11use crate::dictionaries::zh::{fullwidth_to_halfwidth, parse_number_pattern};
12use crate::error::Result;
13use crate::parsers::Parser;
14use crate::results::ParsedResult;
15use crate::types::Meridiem;
16use fancy_regex::Regex;
17use std::sync::LazyLock;
18
19// Pattern for Chinese time: [上午/下午]H点[M分][S秒]
20static PATTERN: LazyLock<Regex> = LazyLock::new(|| {
21    Regex::new(
22        r"(?P<meridiem>上午|下午|凌晨|早上|晚上|中午)?(?P<hour>[0-90-9一二三四五六七八九十两兩]+)(?:点|點)(?:(?P<minute>[0-90-9一二三四五六七八九十]+)分)?(?P<half>半)?(?:(?P<second>[0-90-9一二三四五六七八九十]+)秒)?"
23    ).unwrap()
24});
25
26/// Chinese time expression parser
27pub struct ZHTimeExpressionParser;
28
29impl ZHTimeExpressionParser {
30    pub fn new() -> Self {
31        Self
32    }
33
34    fn parse_number(s: &str) -> i32 {
35        let hankaku = fullwidth_to_halfwidth(s);
36        if let Ok(n) = hankaku.parse::<i32>() {
37            return n;
38        }
39        parse_number_pattern(s) as i32
40    }
41
42    fn get_meridiem_info(meridiem: &str) -> (Option<Meridiem>, bool) {
43        match meridiem {
44            "上午" | "凌晨" | "早上" => (Some(Meridiem::AM), true),
45            "下午" | "晚上" => (Some(Meridiem::PM), true),
46            "中午" => (Some(Meridiem::PM), false), // noon, don't adjust
47            _ => (None, false),
48        }
49    }
50}
51
52impl Parser for ZHTimeExpressionParser {
53    fn name(&self) -> &'static str {
54        "ZHTimeExpressionParser"
55    }
56
57    fn should_apply(&self, context: &ParsingContext) -> bool {
58        context.text.contains('点') || context.text.contains('點')
59    }
60
61    fn parse(&self, context: &ParsingContext) -> Result<Vec<ParsedResult>> {
62        let mut results = Vec::new();
63
64        let mut start = 0;
65        while start < context.text.len() {
66            let search_text = &context.text[start..];
67
68            if let Ok(Some(caps)) = PATTERN.captures(search_text) {
69                let full_match = caps.get(0).unwrap();
70                let match_start = start + full_match.start();
71                let match_end = start + full_match.end();
72
73                let mut hour = caps
74                    .name("hour")
75                    .map(|m| Self::parse_number(m.as_str()))
76                    .unwrap_or(0);
77                let mut minute = caps
78                    .name("minute")
79                    .map(|m| Self::parse_number(m.as_str()))
80                    .unwrap_or(0);
81                let second = caps
82                    .name("second")
83                    .map(|m| Self::parse_number(m.as_str()))
84                    .unwrap_or(0);
85
86                // Handle "半" (half) = 30 minutes
87                if caps.name("half").is_some() {
88                    minute = if minute == 0 { 30 } else { minute + 30 };
89                }
90
91                let meridiem_str = caps.name("meridiem").map(|m| m.as_str());
92
93                // Validate
94                if hour > 24 || minute >= 60 || second >= 60 {
95                    start = match_end;
96                    continue;
97                }
98
99                // Apply meridiem
100                let mut meridiem_val = None;
101                if let Some(m_str) = meridiem_str {
102                    let (mer, should_adjust) = Self::get_meridiem_info(m_str);
103                    meridiem_val = mer;
104
105                    if should_adjust {
106                        if let Some(Meridiem::PM) = mer {
107                            if hour < 12 {
108                                hour += 12;
109                            }
110                        } else if let Some(Meridiem::AM) = mer
111                            && hour == 12
112                        {
113                            hour = 0;
114                        }
115                    }
116                }
117
118                let mut components = context.create_components();
119                components.assign(Component::Hour, hour);
120                components.assign(Component::Minute, minute);
121                if second > 0 {
122                    components.assign(Component::Second, second);
123                }
124                if let Some(m) = meridiem_val {
125                    components.assign(Component::Meridiem, m as i32);
126                }
127
128                results.push(context.create_result(match_start, match_end, components, None));
129                start = match_end;
130                continue;
131            }
132
133            // No match - advance
134            if let Some(c) = search_text.chars().next() {
135                start += c.len_utf8();
136            } else {
137                break;
138            }
139        }
140
141        Ok(results)
142    }
143}
144
145impl Default for ZHTimeExpressionParser {
146    fn default() -> Self {
147        Self::new()
148    }
149}