whichtime_sys/parsers/ja/
slash_date.rs1use crate::components::Component;
10use crate::context::ParsingContext;
11use crate::error::Result;
12use crate::parsers::Parser;
13use crate::results::ParsedResult;
14use chrono::Datelike;
15use regex::Regex;
16use std::sync::LazyLock;
17
18static FULL_PATTERN: LazyLock<Regex> =
20 LazyLock::new(|| Regex::new(r"(\d{4})/(\d{1,2})/(\d{1,2})").unwrap());
21
22static SHORT_PATTERN: LazyLock<Regex> =
24 LazyLock::new(|| Regex::new(r"(\d{1,2})/(\d{1,2})").unwrap());
25
26static RANGE_PATTERN: LazyLock<Regex> = LazyLock::new(|| {
28 Regex::new(r"(\d{4})/(\d{1,2})/(\d{1,2})\s*[~~ー-]\s*(\d{4})/(\d{1,2})/(\d{1,2})").unwrap()
29});
30
31pub struct JASlashDateParser;
33
34impl JASlashDateParser {
35 pub fn new() -> Self {
36 Self
37 }
38
39 fn is_valid_date(year: i32, month: i32, day: i32) -> bool {
40 if !(1..=12).contains(&month) || !(1..=31).contains(&day) {
41 return false;
42 }
43 let days_in_month = match month {
44 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
45 4 | 6 | 9 | 11 => 30,
46 2 => {
47 if (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0) {
48 29
49 } else {
50 28
51 }
52 }
53 _ => return false,
54 };
55 day <= days_in_month
56 }
57}
58
59impl Parser for JASlashDateParser {
60 fn name(&self) -> &'static str {
61 "JASlashDateParser"
62 }
63
64 fn should_apply(&self, context: &ParsingContext) -> bool {
65 context.text.contains('/') && context.text.bytes().any(|b| b.is_ascii_digit())
66 }
67
68 fn parse(&self, context: &ParsingContext) -> Result<Vec<ParsedResult>> {
69 let mut results = Vec::new();
70 let ref_date = context.reference.instant;
71
72 for mat in RANGE_PATTERN.find_iter(context.text) {
74 let matched_text = mat.as_str();
75 let index = mat.start();
76
77 let Some(caps) = RANGE_PATTERN.captures(matched_text) else {
78 continue;
79 };
80
81 let year1: i32 = caps
82 .get(1)
83 .and_then(|m| m.as_str().parse().ok())
84 .unwrap_or(0);
85 let month1: i32 = caps
86 .get(2)
87 .and_then(|m| m.as_str().parse().ok())
88 .unwrap_or(0);
89 let day1: i32 = caps
90 .get(3)
91 .and_then(|m| m.as_str().parse().ok())
92 .unwrap_or(0);
93
94 let year2: i32 = caps
95 .get(4)
96 .and_then(|m| m.as_str().parse().ok())
97 .unwrap_or(0);
98 let month2: i32 = caps
99 .get(5)
100 .and_then(|m| m.as_str().parse().ok())
101 .unwrap_or(0);
102 let day2: i32 = caps
103 .get(6)
104 .and_then(|m| m.as_str().parse().ok())
105 .unwrap_or(0);
106
107 if Self::is_valid_date(year1, month1, day1) && Self::is_valid_date(year2, month2, day2)
108 {
109 let mut components = context.create_components();
110 components.assign(Component::Year, year1);
111 components.assign(Component::Month, month1);
112 components.assign(Component::Day, day1);
113
114 let mut end_comp = context.create_components();
115 end_comp.assign(Component::Year, year2);
116 end_comp.assign(Component::Month, month2);
117 end_comp.assign(Component::Day, day2);
118
119 results.push(context.create_result(
120 index,
121 index + matched_text.len(),
122 components,
123 Some(end_comp),
124 ));
125 }
126 }
127
128 if !results.is_empty() {
130 return Ok(results);
131 }
132
133 for mat in FULL_PATTERN.find_iter(context.text) {
135 let matched_text = mat.as_str();
136 let index = mat.start();
137
138 let Some(caps) = FULL_PATTERN.captures(matched_text) else {
139 continue;
140 };
141
142 let year: i32 = caps
143 .get(1)
144 .and_then(|m| m.as_str().parse().ok())
145 .unwrap_or(0);
146 let month: i32 = caps
147 .get(2)
148 .and_then(|m| m.as_str().parse().ok())
149 .unwrap_or(0);
150 let day: i32 = caps
151 .get(3)
152 .and_then(|m| m.as_str().parse().ok())
153 .unwrap_or(0);
154
155 if !Self::is_valid_date(year, month, day) {
156 continue;
157 }
158
159 let mut components = context.create_components();
160 components.assign(Component::Year, year);
161 components.assign(Component::Month, month);
162 components.assign(Component::Day, day);
163
164 results.push(context.create_result(
165 index,
166 index + matched_text.len(),
167 components,
168 None,
169 ));
170 }
171
172 for mat in SHORT_PATTERN.find_iter(context.text) {
174 let matched_text = mat.as_str();
175 let index = mat.start();
176 let end_index = index + matched_text.len();
177
178 if results
179 .iter()
180 .any(|r| index < r.end_index && end_index > r.index)
181 {
182 continue;
183 }
184
185 let Some(caps) = SHORT_PATTERN.captures(matched_text) else {
186 continue;
187 };
188
189 let month: i32 = caps
190 .get(1)
191 .and_then(|m| m.as_str().parse().ok())
192 .unwrap_or(0);
193 let day: i32 = caps
194 .get(2)
195 .and_then(|m| m.as_str().parse().ok())
196 .unwrap_or(0);
197 let year = ref_date.year();
198
199 if !Self::is_valid_date(year, month, day) {
200 continue;
201 }
202
203 let mut components = context.create_components();
204 components.imply(Component::Year, year);
205 components.assign(Component::Month, month);
206 components.assign(Component::Day, day);
207
208 results.push(context.create_result(index, end_index, components, None));
209 }
210
211 Ok(results)
212 }
213}
214
215impl Default for JASlashDateParser {
216 fn default() -> Self {
217 Self::new()
218 }
219}
220
221#[cfg(test)]
222mod tests {
223 use super::*;
224 use crate::dictionaries::Locale;
225 use crate::results::ReferenceWithTimezone;
226 use chrono::Local;
227
228 #[test]
229 fn parses_full_slash_date() {
230 let parser = JASlashDateParser::new();
231 let reference = ReferenceWithTimezone::new(Local::now(), None);
232 let context = ParsingContext::with_locale("2012/3/31", &reference, Locale::Ja);
233 let results = parser.parse(&context).unwrap();
234 assert_eq!(results.len(), 1);
235 assert_eq!(results[0].text, "2012/3/31");
236 }
237}