Skip to main content

whichtime_sys/parsers/ja/
slash_date.rs

1//! Japanese slash date parser
2//!
3//! Handles Japanese slash date formats like:
4//! - "2012/3/31" (YYYY/M/D)
5//! - "12/31" (M/D)
6//! - "8/5" (M/D)
7//! - "2013/12/26~2014/1/7" (date range)
8
9use 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
18// Pattern for YYYY/M/D
19static FULL_PATTERN: LazyLock<Regex> =
20    LazyLock::new(|| Regex::new(r"(\d{4})/(\d{1,2})/(\d{1,2})").unwrap());
21
22// Pattern for M/D
23static SHORT_PATTERN: LazyLock<Regex> =
24    LazyLock::new(|| Regex::new(r"(\d{1,2})/(\d{1,2})").unwrap());
25
26// Pattern for date range
27static 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
31/// Japanese slash date parser
32pub 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        // Try range pattern first
73        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 we found ranges, don't look for individual dates that overlap
129        if !results.is_empty() {
130            return Ok(results);
131        }
132
133        // Try full YYYY/M/D pattern first
134        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        // Then match M/D pattern (without year), avoiding overlaps with existing matches
173        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}