Skip to main content

whichtime_sys/parsers/en/
iso_format.rs

1//! ISO 8601 format parser
2
3use crate::components::Component;
4use crate::context::ParsingContext;
5use crate::error::Result;
6use crate::parsers::Parser;
7use crate::results::ParsedResult;
8use regex::Regex;
9use std::sync::LazyLock;
10
11// ISO 8601 pattern: YYYY-MM-DD[THH:MM[:SS[.sss]]][Z|±HH:MM]
12static PATTERN: LazyLock<Regex> = LazyLock::new(|| {
13    Regex::new(
14        r"(?i)(\d{4})-(\d{1,2})-(\d{1,2})(?:T(\d{1,2}):(\d{2})(?::(\d{2}))?(?:\.(\d{1,3}))?)?(?:(Z)|([+-])(\d{2}):?(\d{2}))?"
15    ).unwrap()
16});
17
18/// Parser for ISO 8601-style date strings.
19pub struct ISOFormatParser;
20
21impl Parser for ISOFormatParser {
22    fn name(&self) -> &'static str {
23        "ISOFormatParser"
24    }
25
26    fn should_apply(&self, context: &ParsingContext) -> bool {
27        // Quick check: must contain digits and dashes in right pattern
28        let text = context.lower_text();
29        text.contains('-') && text.bytes().any(|b| b.is_ascii_digit())
30    }
31
32    fn parse(&self, context: &ParsingContext) -> Result<Vec<ParsedResult>> {
33        let mut results = Vec::new();
34
35        for mat in PATTERN.find_iter(context.text) {
36            let matched_text = mat.as_str();
37            let index = mat.start();
38
39            // Re-capture to get groups
40            let Some(caps) = PATTERN.captures(matched_text) else {
41                continue;
42            };
43
44            let mut components = context.create_components();
45
46            // Year (group 1)
47            if let Some(year_match) = caps.get(1)
48                && let Ok(year) = year_match.as_str().parse::<i32>()
49            {
50                components.assign(Component::Year, year);
51            }
52
53            // Month (group 2)
54            if let Some(month_match) = caps.get(2)
55                && let Ok(month) = month_match.as_str().parse::<i32>()
56            {
57                if !(1..=12).contains(&month) {
58                    continue;
59                }
60                components.assign(Component::Month, month);
61            }
62
63            // Day (group 3)
64            if let Some(day_match) = caps.get(3)
65                && let Ok(day) = day_match.as_str().parse::<i32>()
66            {
67                if !(1..=31).contains(&day) {
68                    continue;
69                }
70                components.assign(Component::Day, day);
71            }
72
73            // Hour (group 4)
74            if let Some(hour_match) = caps.get(4)
75                && let Ok(hour) = hour_match.as_str().parse::<i32>()
76            {
77                if !(0..=23).contains(&hour) {
78                    continue;
79                }
80                components.assign(Component::Hour, hour);
81            }
82
83            // Minute (group 5)
84            if let Some(min_match) = caps.get(5)
85                && let Ok(min) = min_match.as_str().parse::<i32>()
86            {
87                components.assign(Component::Minute, min);
88            }
89
90            // Second (group 6)
91            if let Some(sec_match) = caps.get(6)
92                && let Ok(sec) = sec_match.as_str().parse::<i32>()
93            {
94                components.assign(Component::Second, sec);
95            }
96
97            // Millisecond (group 7)
98            if let Some(ms_match) = caps.get(7)
99                && let Ok(ms) = ms_match.as_str().parse::<i32>()
100            {
101                components.assign(Component::Millisecond, ms);
102            }
103
104            // Timezone Z (group 8)
105            if caps.get(8).is_some() {
106                components.assign(Component::TimezoneOffset, 0);
107            }
108            // Timezone offset (groups 9, 10, 11)
109            else if let (Some(sign), Some(tz_hour), Some(tz_min)) =
110                (caps.get(9), caps.get(10), caps.get(11))
111            {
112                let sign = if sign.as_str() == "-" { -1 } else { 1 };
113                let hours: i32 = tz_hour.as_str().parse().unwrap_or(0);
114                let mins: i32 = tz_min.as_str().parse().unwrap_or(0);
115                let offset = sign * (hours * 60 + mins);
116                components.assign(Component::TimezoneOffset, offset);
117            }
118
119            // Validate date
120            if !components.is_valid_date() {
121                continue;
122            }
123
124            results.push(context.create_result(
125                index,
126                index + matched_text.len(),
127                components,
128                None,
129            ));
130        }
131
132        Ok(results)
133    }
134}