Skip to main content

whichtime_sys/parsers/ja/
casual_time.rs

1//! Japanese casual time parser
2//!
3//! Handles expressions like:
4//! - "正午", "真夜中", "昼"
5//! - "午前", "午後", "夕方", "夜"
6//! - Phrases with simple modifiers such as "来週の午後"
7
8use crate::components::Component;
9use crate::context::ParsingContext;
10use crate::dictionaries::ja::{get_casual_time, get_relative_modifier};
11use crate::dictionaries::{CasualTimeType, RelativeModifier};
12use crate::error::Result;
13use crate::parsers::Parser;
14use crate::results::ParsedResult;
15use crate::scanner::TokenType;
16use crate::types::Meridiem;
17use chrono::{Datelike, Duration, Timelike};
18use fancy_regex::Regex;
19use std::sync::LazyLock;
20
21static PATTERN: LazyLock<Regex> = LazyLock::new(|| {
22    Regex::new(
23        r"(?:(?P<modifier>今|この|今週|次|来|来週|前|先|先週)(?:\s*の)?)?(?P<time>正午|昼|真夜中|夜中|朝|午前|午後|夕方|夜)",
24    )
25    .unwrap()
26});
27
28pub struct JACasualTimeParser;
29
30impl JACasualTimeParser {
31    pub fn new() -> Self {
32        Self
33    }
34
35    fn is_digit_like(ch: char) -> bool {
36        ch.is_ascii_digit()
37            || ('0'..='9').contains(&ch)
38            || matches!(
39                ch,
40                '〇' | '一' | '二' | '三' | '四' | '五' | '六' | '七' | '八' | '九' | '十'
41            )
42    }
43
44    fn has_trailing_number(text: &str, idx: usize) -> bool {
45        if idx >= text.len() {
46            return false;
47        }
48        let chars = text[idx..].chars();
49        for ch in chars {
50            if ch.is_whitespace() {
51                continue;
52            }
53            return Self::is_digit_like(ch);
54        }
55        false
56    }
57}
58
59impl Parser for JACasualTimeParser {
60    fn name(&self) -> &'static str {
61        "JACasualTimeParser"
62    }
63
64    fn should_apply(&self, context: &ParsingContext) -> bool {
65        context.has_token_type(TokenType::CasualTime)
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        let mut start = 0;
73        while start < context.text.len() {
74            let search_text = &context.text[start..];
75            let mat = match PATTERN.find(search_text) {
76                Ok(Some(m)) => m,
77                _ => break,
78            };
79
80            let match_start = start + mat.start();
81            let match_end = start + mat.end();
82
83            // Skip if immediately followed by a numeric time (e.g., "午前8時")
84            if Self::has_trailing_number(context.text, match_end) {
85                start = match_end;
86                continue;
87            }
88
89            let Some(caps) = PATTERN.captures(mat.as_str()).ok().flatten() else {
90                start = match_end;
91                continue;
92            };
93
94            let time_word = match caps.name("time") {
95                Some(m) => m.as_str(),
96                None => {
97                    start = match_end;
98                    continue;
99                }
100            };
101
102            let Some(time_type) = get_casual_time(time_word) else {
103                start = match_end;
104                continue;
105            };
106
107            let modifier = caps
108                .name("modifier")
109                .and_then(|m| get_relative_modifier(m.as_str()));
110
111            let mut target_date = ref_date;
112            match modifier {
113                Some(RelativeModifier::Last) => {
114                    if !(ref_date.hour() <= 6 && matches!(time_type, CasualTimeType::Night)) {
115                        target_date = ref_date - Duration::days(1);
116                    }
117                }
118                Some(RelativeModifier::Next) => {
119                    target_date = ref_date + Duration::days(1);
120                }
121                _ => {}
122            }
123
124            let mut components = context.create_components();
125            components.assign(Component::Year, target_date.year());
126            components.assign(Component::Month, target_date.month() as i32);
127            components.assign(Component::Day, target_date.day() as i32);
128
129            match time_type {
130                CasualTimeType::Noon => {
131                    components.assign(Component::Hour, 12);
132                    components.assign(Component::Minute, 0);
133                    components.assign(Component::Second, 0);
134                    components.assign(Component::Meridiem, Meridiem::PM as i32);
135                }
136                CasualTimeType::Midnight => {
137                    if modifier.is_none() && ref_date.hour() >= 6 {
138                        let next_day = ref_date + Duration::days(1);
139                        components.assign(Component::Year, next_day.year());
140                        components.assign(Component::Month, next_day.month() as i32);
141                        components.assign(Component::Day, next_day.day() as i32);
142                    }
143                    components.assign(Component::Hour, 0);
144                    components.assign(Component::Minute, 0);
145                    components.assign(Component::Second, 0);
146                }
147                CasualTimeType::Morning => {
148                    components.imply(Component::Hour, 6);
149                    components.imply(Component::Minute, 0);
150                    components.assign(Component::Meridiem, Meridiem::AM as i32);
151                }
152                CasualTimeType::Afternoon => {
153                    components.imply(Component::Hour, 15);
154                    components.imply(Component::Minute, 0);
155                    components.assign(Component::Meridiem, Meridiem::PM as i32);
156                }
157                CasualTimeType::Evening => {
158                    components.imply(Component::Hour, 20);
159                    components.imply(Component::Minute, 0);
160                    components.assign(Component::Meridiem, Meridiem::PM as i32);
161                }
162                CasualTimeType::Night => {
163                    components.imply(Component::Hour, 22);
164                    components.imply(Component::Minute, 0);
165                    components.assign(Component::Meridiem, Meridiem::PM as i32);
166                }
167            }
168
169            let result = context.create_result(match_start, match_end, components, None);
170            results.push(result);
171
172            start = match_end;
173        }
174
175        Ok(results)
176    }
177}
178
179impl Default for JACasualTimeParser {
180    fn default() -> Self {
181        Self::new()
182    }
183}