Skip to main content

whichtime_sys/parsers/ja/
weekday.rs

1//! Japanese weekday parser
2//!
3//! Handles Japanese weekday expressions like:
4//! - "木曜日", "木曜", "木" (Thursday)
5//! - "(木)", "(木)" (Thursday in parentheses)
6//! - "前の水曜日" (last Wednesday)
7//! - "来週の金曜日" (next Friday)
8
9use crate::components::Component;
10use crate::context::ParsingContext;
11use crate::dictionaries::ja as dict;
12use crate::error::Result;
13use crate::parsers::Parser;
14use crate::results::ParsedResult;
15use crate::scanner::TokenType;
16use chrono::{Datelike, Duration};
17use fancy_regex::Regex;
18use std::sync::LazyLock;
19
20// Pattern for weekday with optional modifier
21static PATTERN: LazyLock<Regex> = LazyLock::new(|| {
22    Regex::new(
23        r"(?:(?P<modifier>前|先|次|来|今|この|来週|先週|今週)(?:の)?)?(?P<weekday>日曜日|月曜日|火曜日|水曜日|木曜日|金曜日|土曜日|日曜|月曜|火曜|水曜|木曜|金曜|土曜)"
24    ).unwrap()
25});
26
27// Pattern for weekday in parentheses
28static PAREN_PATTERN: LazyLock<Regex> =
29    LazyLock::new(|| Regex::new(r"[((](?P<weekday>日|月|火|水|木|金|土)[))]").unwrap());
30
31/// Japanese weekday parser
32pub struct JAWeekdayParser;
33
34impl JAWeekdayParser {
35    pub fn new() -> Self {
36        Self
37    }
38}
39
40impl Parser for JAWeekdayParser {
41    fn name(&self) -> &'static str {
42        "JAWeekdayParser"
43    }
44
45    fn should_apply(&self, context: &ParsingContext) -> bool {
46        context.has_token_type(TokenType::Weekday)
47            || context.text.contains('曜')
48            || context.text.contains('(')
49            || context.text.contains('(')
50    }
51
52    fn parse(&self, context: &ParsingContext) -> Result<Vec<ParsedResult>> {
53        let mut results = Vec::new();
54        let ref_date = context.reference.instant;
55
56        let mut start = 0;
57        while start < context.text.len() {
58            let search_text = &context.text[start..];
59
60            // Try parentheses pattern first
61            if let Ok(Some(caps)) = PAREN_PATTERN.captures(search_text) {
62                let full_match = caps.get(0).unwrap();
63                let match_start = start + full_match.start();
64                let match_end = start + full_match.end();
65
66                let weekday_str = caps.name("weekday").map(|m| m.as_str()).unwrap_or_default();
67
68                if let Some(weekday) = dict::get_weekday(weekday_str) {
69                    // For parenthesized weekday, find the nearest occurrence
70                    let ref_weekday = ref_date.weekday().num_days_from_monday() as i32;
71                    let target_weekday = match weekday as i32 {
72                        0 => 6, // Sunday
73                        n => n - 1,
74                    };
75                    let diff = target_weekday - ref_weekday;
76                    let target_date = ref_date + Duration::days(diff as i64);
77
78                    let mut components = context.create_components();
79                    components.assign(Component::Year, target_date.year());
80                    components.assign(Component::Month, target_date.month() as i32);
81                    components.assign(Component::Day, target_date.day() as i32);
82                    components.assign(Component::Weekday, weekday as i32);
83
84                    results.push(context.create_result(match_start, match_end, components, None));
85                    start = match_end;
86                    continue;
87                }
88            }
89
90            // Try main pattern
91            if let Ok(Some(caps)) = PATTERN.captures(search_text) {
92                let full_match = caps.get(0).unwrap();
93                let match_start = start + full_match.start();
94                let match_end = start + full_match.end();
95
96                let weekday_str = caps.name("weekday").map(|m| m.as_str()).unwrap_or_default();
97                let modifier = caps.name("modifier").map(|m| m.as_str());
98
99                if let Some(weekday) = dict::get_weekday(weekday_str) {
100                    let ref_weekday = ref_date.weekday().num_days_from_monday() as i32;
101                    let target_weekday = match weekday as i32 {
102                        0 => 6, // Sunday
103                        n => n - 1,
104                    };
105
106                    let days_diff = match modifier {
107                        Some("次") | Some("来") | Some("来週") => {
108                            // Next occurrence
109                            let diff = target_weekday - ref_weekday;
110                            if diff <= 0 { diff + 7 } else { diff }
111                        }
112                        Some("前") | Some("先") | Some("先週") => {
113                            // Previous occurrence
114                            let diff = target_weekday - ref_weekday;
115                            if diff >= 0 { diff - 7 } else { diff }
116                        }
117                        Some("今") | Some("この") | Some("今週") | None => {
118                            // Current week
119                            target_weekday - ref_weekday
120                        }
121                        _ => target_weekday - ref_weekday,
122                    };
123
124                    let target_date = ref_date + Duration::days(days_diff as i64);
125
126                    let mut components = context.create_components();
127                    components.assign(Component::Year, target_date.year());
128                    components.assign(Component::Month, target_date.month() as i32);
129                    components.assign(Component::Day, target_date.day() as i32);
130                    components.assign(Component::Weekday, weekday as i32);
131
132                    results.push(context.create_result(match_start, match_end, components, None));
133                    start = match_end;
134                    continue;
135                }
136            }
137
138            // No match - advance
139            if let Some(c) = search_text.chars().next() {
140                start += c.len_utf8();
141            } else {
142                break;
143            }
144        }
145
146        Ok(results)
147    }
148}
149
150impl Default for JAWeekdayParser {
151    fn default() -> Self {
152        Self::new()
153    }
154}