Skip to main content

whichtime_sys/parsers/it/
year_month_day.rs

1//! Italian year.month.day parser
2//!
3//! Handles Italian date format: YYYY.MM.DD
4
5use crate::components::Component;
6use crate::context::ParsingContext;
7use crate::error::Result;
8use crate::parsers::Parser;
9use crate::results::ParsedResult;
10use regex::Regex;
11use std::sync::LazyLock;
12
13// Pattern: YYYY.MM.DD
14static PATTERN: LazyLock<Regex> =
15    LazyLock::new(|| Regex::new(r"(?:^|[^\d])(\d{4})\.(\d{2})\.(\d{2})(?:[^\d]|$)").unwrap());
16
17/// Italian year.month.day parser
18pub struct ITYearMonthDayParser;
19
20impl ITYearMonthDayParser {
21    pub fn new() -> Self {
22        Self
23    }
24
25    fn is_valid_date(year: i32, month: i32, day: i32) -> bool {
26        if !(1..=12).contains(&month) || !(1..=31).contains(&day) {
27            return false;
28        }
29        let days_in_month = match month {
30            1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
31            4 | 6 | 9 | 11 => 30,
32            2 => {
33                if (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0) {
34                    29
35                } else {
36                    28
37                }
38            }
39            _ => return false,
40        };
41        day <= days_in_month
42    }
43}
44
45impl Parser for ITYearMonthDayParser {
46    fn name(&self) -> &'static str {
47        "ITYearMonthDayParser"
48    }
49
50    fn should_apply(&self, context: &ParsingContext) -> bool {
51        context.text.contains('.') && context.text.bytes().any(|b| b.is_ascii_digit())
52    }
53
54    fn parse(&self, context: &ParsingContext) -> Result<Vec<ParsedResult>> {
55        let mut results = Vec::new();
56
57        for mat in PATTERN.find_iter(context.text) {
58            let matched_text = mat.as_str();
59            let index = mat.start();
60
61            let Some(caps) = PATTERN.captures(matched_text) else {
62                continue;
63            };
64
65            let year: i32 = caps
66                .get(1)
67                .and_then(|m| m.as_str().parse().ok())
68                .unwrap_or(0);
69            let month: i32 = caps
70                .get(2)
71                .and_then(|m| m.as_str().parse().ok())
72                .unwrap_or(0);
73            let day: i32 = caps
74                .get(3)
75                .and_then(|m| m.as_str().parse().ok())
76                .unwrap_or(0);
77
78            // Validate
79            if !Self::is_valid_date(year, month, day) {
80                continue;
81            }
82
83            let mut components = context.create_components();
84            components.assign(Component::Year, year);
85            components.assign(Component::Month, month);
86            components.assign(Component::Day, day);
87
88            // Trim the leading/trailing non-digit characters from the matched text
89            let actual_start = matched_text.find(|c: char| c.is_ascii_digit()).unwrap_or(0);
90            let actual_end = matched_text
91                .rfind(|c: char| c.is_ascii_digit())
92                .map(|i| i + 1)
93                .unwrap_or(matched_text.len());
94            let clean_text = &matched_text[actual_start..actual_end];
95
96            results.push(context.create_result(
97                index + actual_start,
98                index + actual_start + clean_text.len(),
99                components,
100                None,
101            ));
102        }
103
104        Ok(results)
105    }
106}
107
108impl Default for ITYearMonthDayParser {
109    fn default() -> Self {
110        Self::new()
111    }
112}