Skip to main content

whichtime_sys/parsers/ja/
time_expression.rs

1//! Japanese time expression parser
2//!
3//! Handles Japanese time formats like:
4//! - "午前6時13分" (AM 6:13)
5//! - "午後8時" (PM 8:00)
6//! - "午後三時半五十九秒" (PM 3:30:59 with kanji numbers)
7//! - "6時30分PM" (6:30 PM)
8//! - Time ranges: "午前八時十分から午後11時32分"
9
10use crate::components::Component;
11use crate::context::ParsingContext;
12use crate::dictionaries::ja::{ja_string_to_number, to_hankaku};
13use crate::error::Result;
14use crate::parsers::Parser;
15use crate::results::ParsedResult;
16use crate::types::Meridiem;
17use fancy_regex::Regex;
18use std::sync::LazyLock;
19
20// Pattern for Japanese time: [午前/午後]H時[M分][S秒][AM/PM]
21static PATTERN: LazyLock<Regex> = LazyLock::new(|| {
22    Regex::new(
23        r"(?P<meridiem1>午前|午後)?(?P<hour>[0-90-9一二三四五六七八九十]+)時(?!間)(?P<minute>[0-90-9一二三四五六七八九十]+)?(?:分)?(?P<half>半)?(?P<second>[0-90-9一二三四五六七八九十]+秒)?(?P<meridiem2>AM|PM|am|pm)?"
24    ).unwrap()
25});
26
27// Pattern for time range with から (from)
28static RANGE_PATTERN: LazyLock<Regex> = LazyLock::new(|| {
29    Regex::new(
30        r"(?P<meridiem1>午前|午後)?(?P<hour1>[0-90-9一二三四五六七八九十]+)時(?!間)(?P<minute1>[0-90-9一二三四五六七八九十]+)?(?:分)?(?P<half1>半)?(?P<second1>[0-90-9一二三四五六七八九十]+秒)?(?P<pm1>AM|PM|am|pm)?(?:から|[-~~ー])(?P<meridiem2>午前|午後)?(?P<hour2>[0-90-9一二三四五六七八九十]+)時(?!間)(?P<minute2>[0-90-9一二三四五六七八九十]+)?(?:分)?(?P<half2>半)?(?P<second2>[0-90-9一二三四五六七八九十]+秒)?(?P<pm2>AM|PM|am|pm)?"
31    ).unwrap()
32});
33
34/// Japanese time expression parser
35pub struct JATimeExpressionParser;
36
37impl JATimeExpressionParser {
38    pub fn new() -> Self {
39        Self
40    }
41
42    fn parse_number(s: &str) -> i32 {
43        // First try as regular number (with hankaku conversion)
44        let hankaku = to_hankaku(s);
45        if let Ok(n) = hankaku.parse::<i32>() {
46            return n;
47        }
48        // Try as Japanese kanji number
49        ja_string_to_number(s) as i32
50    }
51
52    fn parse_minute(s: &str) -> i32 {
53        // Handle "半" (half) = 30
54        if s.contains('半') {
55            return 30;
56        }
57        Self::parse_number(s)
58    }
59
60    fn apply_meridiem(
61        hour: i32,
62        meridiem: Option<&str>,
63        pm_suffix: Option<&str>,
64        fallback: Option<Meridiem>,
65    ) -> Option<(i32, Option<Meridiem>)> {
66        if !(0..=23).contains(&hour) {
67            return None;
68        }
69
70        let suffix_upper = pm_suffix.map(|s| s.to_ascii_uppercase());
71        let suffix_ref = suffix_upper.as_deref();
72
73        let is_pm = matches!(meridiem, Some("午後")) || suffix_ref == Some("PM");
74        let is_am = matches!(meridiem, Some("午前")) || suffix_ref == Some("AM");
75
76        if is_pm {
77            if hour > 12 {
78                return None;
79            }
80            let adjusted_hour = if hour < 12 { hour + 12 } else { hour };
81            return Some((adjusted_hour, Some(Meridiem::PM)));
82        }
83
84        if is_am {
85            if hour > 12 {
86                return None;
87            }
88            let adjusted_hour = if hour == 12 { 0 } else { hour };
89            return Some((adjusted_hour, Some(Meridiem::AM)));
90        }
91
92        if let Some(fallback_mer) = fallback {
93            if hour > 12 {
94                return None;
95            }
96            let adjusted_hour = match fallback_mer {
97                Meridiem::PM => {
98                    if hour < 12 {
99                        hour + 12
100                    } else {
101                        hour
102                    }
103                }
104                Meridiem::AM => {
105                    if hour == 12 {
106                        0
107                    } else {
108                        hour
109                    }
110                }
111            };
112            return Some((adjusted_hour, Some(fallback_mer)));
113        }
114
115        Some((
116            hour,
117            if hour >= 12 {
118                Some(Meridiem::PM)
119            } else {
120                Some(Meridiem::AM)
121            },
122        ))
123    }
124}
125
126impl Parser for JATimeExpressionParser {
127    fn name(&self) -> &'static str {
128        "JATimeExpressionParser"
129    }
130
131    fn should_apply(&self, context: &ParsingContext) -> bool {
132        context.text.contains('時')
133            || context.text.contains("午前")
134            || context.text.contains("午後")
135    }
136
137    fn parse(&self, context: &ParsingContext) -> Result<Vec<ParsedResult>> {
138        let mut results = Vec::new();
139
140        let mut start = 0;
141        while start < context.text.len() {
142            let search_text = &context.text[start..];
143
144            // Try range pattern first
145            if let Ok(Some(caps)) = RANGE_PATTERN.captures(search_text) {
146                let full_match = caps.get(0).unwrap();
147                let match_start = start + full_match.start();
148                let match_end = start + full_match.end();
149
150                let hour1 = caps
151                    .name("hour1")
152                    .map(|m| Self::parse_number(m.as_str()))
153                    .unwrap_or(0);
154                let mut minute1 = caps
155                    .name("minute1")
156                    .map(|m| Self::parse_minute(m.as_str()))
157                    .unwrap_or(0);
158                let second1 = caps
159                    .name("second1")
160                    .map(|m| Self::parse_number(m.as_str().trim_end_matches('秒')))
161                    .unwrap_or(0);
162                let meridiem1 = caps.name("meridiem1").map(|m| m.as_str());
163                let pm1 = caps.name("pm1").map(|m| m.as_str());
164
165                let hour2 = caps
166                    .name("hour2")
167                    .map(|m| Self::parse_number(m.as_str()))
168                    .unwrap_or(0);
169                let mut minute2 = caps
170                    .name("minute2")
171                    .map(|m| Self::parse_minute(m.as_str()))
172                    .unwrap_or(0);
173                let second2 = caps
174                    .name("second2")
175                    .map(|m| Self::parse_number(m.as_str().trim_end_matches('秒')))
176                    .unwrap_or(0);
177                let meridiem2 = caps.name("meridiem2").map(|m| m.as_str());
178                let pm2 = caps.name("pm2").map(|m| m.as_str());
179
180                if caps.name("half1").is_some() {
181                    minute1 = if minute1 == 0 { 30 } else { minute1 + 30 };
182                }
183                if caps.name("half2").is_some() {
184                    minute2 = if minute2 == 0 { 30 } else { minute2 + 30 };
185                }
186
187                if minute1 >= 60 || minute2 >= 60 || second1 >= 60 || second2 >= 60 {
188                    start = match_end;
189                    continue;
190                }
191
192                let Some((adj_hour1, mer1)) = Self::apply_meridiem(hour1, meridiem1, pm1, None)
193                else {
194                    start = match_end;
195                    continue;
196                };
197
198                let fallback_meridiem = if meridiem2.is_none() && pm2.is_none() {
199                    mer1
200                } else {
201                    None
202                };
203
204                let Some((adj_hour2, mer2)) =
205                    Self::apply_meridiem(hour2, meridiem2, pm2, fallback_meridiem)
206                else {
207                    start = match_end;
208                    continue;
209                };
210
211                let mut components = context.create_components();
212                components.assign(Component::Hour, adj_hour1);
213                components.assign(Component::Minute, minute1);
214                if second1 > 0 {
215                    components.assign(Component::Second, second1);
216                }
217                if let Some(m) = mer1 {
218                    components.assign(Component::Meridiem, m as i32);
219                }
220
221                let mut end_comp = context.create_components();
222                end_comp.assign(Component::Hour, adj_hour2);
223                end_comp.assign(Component::Minute, minute2);
224                if second2 > 0 {
225                    end_comp.assign(Component::Second, second2);
226                }
227                if let Some(m) = mer2 {
228                    end_comp.assign(Component::Meridiem, m as i32);
229                }
230
231                results.push(context.create_result(
232                    match_start,
233                    match_end,
234                    components,
235                    Some(end_comp),
236                ));
237                start = match_end;
238                continue;
239            }
240
241            // Try single time pattern
242            if let Ok(Some(caps)) = PATTERN.captures(search_text) {
243                let full_match = caps.get(0).unwrap();
244                let match_start = start + full_match.start();
245                let match_end = start + full_match.end();
246
247                let hour = caps
248                    .name("hour")
249                    .map(|m| Self::parse_number(m.as_str()))
250                    .unwrap_or(0);
251                let mut minute = caps
252                    .name("minute")
253                    .map(|m| Self::parse_minute(m.as_str()))
254                    .unwrap_or(0);
255                let second = caps
256                    .name("second")
257                    .map(|m| Self::parse_number(m.as_str().trim_end_matches('秒')))
258                    .unwrap_or(0);
259                let meridiem = caps.name("meridiem1").map(|m| m.as_str());
260                let pm_suffix = caps.name("meridiem2").map(|m| m.as_str());
261
262                if caps.name("half").is_some() {
263                    minute = if minute == 0 { 30 } else { minute + 30 };
264                }
265
266                if minute >= 60 || second >= 60 {
267                    start = match_end;
268                    continue;
269                }
270
271                let Some((adj_hour, mer)) = Self::apply_meridiem(hour, meridiem, pm_suffix, None)
272                else {
273                    start = match_end;
274                    continue;
275                };
276
277                let mut components = context.create_components();
278                components.assign(Component::Hour, adj_hour);
279                components.assign(Component::Minute, minute);
280                if second > 0 {
281                    components.assign(Component::Second, second);
282                }
283                if let Some(m) = mer {
284                    components.assign(Component::Meridiem, m as i32);
285                }
286
287                results.push(context.create_result(match_start, match_end, components, None));
288                start = match_end;
289                continue;
290            }
291
292            // No match - advance
293            if let Some(c) = search_text.chars().next() {
294                start += c.len_utf8();
295            } else {
296                break;
297            }
298        }
299
300        Ok(results)
301    }
302}
303
304impl Default for JATimeExpressionParser {
305    fn default() -> Self {
306        Self::new()
307    }
308}