Skip to main content

whichtime_sys/parsers/en/
time_unit_within.rs

1//! Time unit within parser: "in 5 days", "within 2 hours", etc.
2
3use crate::components::Component;
4use crate::context::ParsingContext;
5use crate::dictionaries::en::{get_time_unit, parse_number_pattern};
6use crate::error::Result;
7use crate::parsers::Parser;
8use crate::results::ParsedResult;
9use crate::scanner::TokenType;
10use crate::types::{Duration, TimeUnit, add_duration};
11use chrono::Datelike;
12use regex::Regex;
13use std::sync::LazyLock;
14
15static PATTERN: LazyLock<Regex> = LazyLock::new(|| {
16    Regex::new(
17        r"(?i)\b(?:in|within|for|about|around)\s+(\d+|one|two|three|four|five|six|seven|eight|nine|ten|eleven|twelve|a|an|the|few|several|couple)\s*(seconds?|minutes?|hours?|days?|weeks?|months?|years?|mins?|hrs?|secs?|h|m|s|d|w|y|mo)\b"
18    ).unwrap()
19});
20
21/// Parser for English future-relative expressions such as "in 5 days".
22pub struct TimeUnitWithinParser;
23
24impl Parser for TimeUnitWithinParser {
25    fn name(&self) -> &'static str {
26        "TimeUnitWithinParser"
27    }
28
29    fn should_apply(&self, context: &ParsingContext) -> bool {
30        (context.has_token_type(TokenType::Within) || context.has_token_type(TokenType::In))
31            && context.has_token_type(TokenType::TimeUnit)
32    }
33
34    fn parse(&self, context: &ParsingContext) -> Result<Vec<ParsedResult>> {
35        let mut results = Vec::new();
36        let ref_date = context.reference.instant;
37
38        for mat in PATTERN.find_iter(context.text) {
39            let matched_text = mat.as_str();
40            let index = mat.start();
41
42            let Some(caps) = PATTERN.captures(matched_text) else {
43                continue;
44            };
45
46            let num_str = caps.get(1).map(|m| m.as_str()).unwrap_or("1");
47            let unit_str = caps
48                .get(2)
49                .map(|m| m.as_str().to_lowercase())
50                .unwrap_or_default();
51
52            let num = parse_number_pattern(num_str);
53            let Some(unit) = get_time_unit(&unit_str) else {
54                continue;
55            };
56
57            // Create positive duration
58            let mut duration = Duration::new();
59            match unit {
60                TimeUnit::Second => duration.second = Some(num),
61                TimeUnit::Minute => duration.minute = Some(num),
62                TimeUnit::Hour => duration.hour = Some(num),
63                TimeUnit::Day => duration.day = Some(num),
64                TimeUnit::Week => duration.week = Some(num),
65                TimeUnit::Month => duration.month = Some(num),
66                TimeUnit::Year => duration.year = Some(num),
67                TimeUnit::Quarter => duration.quarter = Some(num),
68                TimeUnit::Millisecond => duration.millisecond = Some(num),
69            }
70
71            let target_date = add_duration(ref_date, &duration);
72
73            let mut components = context.create_components();
74            components.assign(Component::Year, target_date.year());
75            components.assign(Component::Month, target_date.month() as i32);
76            components.assign(Component::Day, target_date.day() as i32);
77
78            if duration.has_time_component() {
79                use chrono::Timelike;
80                components.assign(Component::Hour, target_date.hour() as i32);
81                components.assign(Component::Minute, target_date.minute() as i32);
82                components.assign(Component::Second, target_date.second() as i32);
83            }
84
85            results.push(context.create_result(
86                index,
87                index + matched_text.len(),
88                components,
89                None,
90            ));
91        }
92
93        Ok(results)
94    }
95}