Skip to main content

whichtime_sys/parsers/en/
casual_time.rs

1//! Casual time parser: noon, midnight, morning, afternoon, evening, night
2//! Also handles compositional patterns: "last night", "this morning", "next evening"
3
4use crate::components::Component;
5use crate::context::ParsingContext;
6use crate::dictionaries::en::{get_casual_time, get_relative_modifier};
7use crate::dictionaries::{CasualTimeType, RelativeModifier};
8use crate::error::Result;
9use crate::parsers::Parser;
10use crate::results::ParsedResult;
11use crate::scanner::TokenType;
12use crate::types::Meridiem;
13use chrono::{Datelike, Duration, Timelike};
14use regex::Regex;
15use std::sync::LazyLock;
16
17// Pattern for standalone casual time or modifier + time_of_day
18static PATTERN: LazyLock<Regex> = LazyLock::new(|| {
19    Regex::new(r"(?i)\b(?:(this|last|next|past|previous)\s+)?(noon|midday|midnight|morning|afternoon|evening|night)\b").unwrap()
20});
21
22/// Parser for English casual time-of-day expressions.
23pub struct CasualTimeParser;
24
25impl Parser for CasualTimeParser {
26    fn name(&self) -> &'static str {
27        "CasualTimeParser"
28    }
29
30    fn should_apply(&self, context: &ParsingContext) -> bool {
31        context.has_token_type(TokenType::CasualTime)
32    }
33
34    fn parse(&self, context: &ParsingContext) -> Result<Vec<ParsedResult>> {
35        let mut results = Vec::new();
36
37        for mat in PATTERN.find_iter(context.text) {
38            let matched_text = mat.as_str();
39            let index = mat.start();
40
41            let Some(caps) = PATTERN.captures(matched_text) else {
42                continue;
43            };
44
45            let modifier_str = caps.get(1).map(|m| m.as_str().to_lowercase());
46            let time_word = caps
47                .get(2)
48                .map(|m| m.as_str().to_lowercase())
49                .unwrap_or_default();
50
51            let Some(time_type) = get_casual_time(&time_word) else {
52                continue;
53            };
54
55            let modifier = modifier_str.as_deref().and_then(get_relative_modifier);
56
57            let mut components = context.create_components();
58            let ref_date = context.reference.instant;
59
60            // Calculate target date based on modifier
61            let target_date = match modifier {
62                Some(RelativeModifier::Last) => {
63                    // "last night" - if currently before 6 AM, same night; otherwise yesterday
64                    if ref_date.hour() <= 6 && matches!(time_type, CasualTimeType::Night) {
65                        ref_date
66                    } else {
67                        ref_date - Duration::days(1)
68                    }
69                }
70                Some(RelativeModifier::Next) => ref_date + Duration::days(1),
71                Some(RelativeModifier::This) | None => ref_date,
72            };
73
74            // Set date components
75            components.assign(Component::Year, target_date.year());
76            components.assign(Component::Month, target_date.month() as i32);
77            components.assign(Component::Day, target_date.day() as i32);
78
79            // Set time components based on time_type
80            match time_type {
81                CasualTimeType::Noon => {
82                    components.assign(Component::Hour, 12);
83                    components.assign(Component::Minute, 0);
84                    components.assign(Component::Second, 0);
85                    components.assign(Component::Meridiem, Meridiem::PM as i32);
86                }
87                CasualTimeType::Midnight => {
88                    // For "this midnight" or standalone "midnight", use start of next day
89                    // For "last midnight", use start of current day
90                    if matches!(modifier, Some(RelativeModifier::Last)) {
91                        // Already adjusted date to yesterday, so midnight is at start of that day
92                        components.assign(Component::Day, target_date.day() as i32);
93                    } else if modifier.is_none() {
94                        // Standalone "midnight" typically means upcoming midnight
95                        let next_day = ref_date + Duration::days(1);
96                        components.assign(Component::Year, next_day.year());
97                        components.assign(Component::Month, next_day.month() as i32);
98                        components.assign(Component::Day, next_day.day() as i32);
99                    }
100                    components.assign(Component::Hour, 0);
101                    components.assign(Component::Minute, 0);
102                    components.assign(Component::Second, 0);
103                }
104                CasualTimeType::Morning => {
105                    components.imply(Component::Hour, 6);
106                    components.imply(Component::Minute, 0);
107                    components.assign(Component::Meridiem, Meridiem::AM as i32);
108                }
109                CasualTimeType::Afternoon => {
110                    components.imply(Component::Hour, 15);
111                    components.imply(Component::Minute, 0);
112                    components.assign(Component::Meridiem, Meridiem::PM as i32);
113                }
114                CasualTimeType::Evening => {
115                    components.imply(Component::Hour, 20);
116                    components.imply(Component::Minute, 0);
117                    components.assign(Component::Meridiem, Meridiem::PM as i32);
118                }
119                CasualTimeType::Night => {
120                    components.imply(Component::Hour, 0);
121                    components.imply(Component::Minute, 0);
122                    components.assign(Component::Meridiem, Meridiem::AM as i32);
123                }
124            }
125
126            results.push(context.create_result(
127                index,
128                index + matched_text.len(),
129                components,
130                None,
131            ));
132        }
133
134        Ok(results)
135    }
136}