whichtime_sys/parsers/ja/
time_expression.rs1use 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
20static 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
27static 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
34pub struct JATimeExpressionParser;
36
37impl JATimeExpressionParser {
38 pub fn new() -> Self {
39 Self
40 }
41
42 fn parse_number(s: &str) -> i32 {
43 let hankaku = to_hankaku(s);
45 if let Ok(n) = hankaku.parse::<i32>() {
46 return n;
47 }
48 ja_string_to_number(s) as i32
50 }
51
52 fn parse_minute(s: &str) -> i32 {
53 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 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 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 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}