whichtime_sys/parsers/fr/
slash_date.rs1use crate::components::Component;
8use crate::context::ParsingContext;
9use crate::dictionaries::fr as dict;
10use crate::error::Result;
11use crate::parsers::Parser;
12use crate::results::ParsedResult;
13use chrono::Datelike;
14use regex::Regex;
15use std::sync::LazyLock;
16
17static PATTERN: LazyLock<Regex> = LazyLock::new(|| {
19 Regex::new(r"(?i)(?:^|[^\wàâäéèêëïîôùûüÿçœæ])(?:(lundi|mardi|mercredi|jeudi|vendredi|samedi|dimanche)\s+)?(\d{1,2})[/\-.](\d{1,2})(?:[/\-.](\d{2,4}))?(?:[^\d]|$)").unwrap()
20});
21
22static AM_PM_PATTERN: LazyLock<Regex> =
24 LazyLock::new(|| Regex::new(r"(?i)^\s*(?:a\.?m\.?|p\.?m\.?)").unwrap());
25
26pub struct FRSlashDateParser;
27
28impl FRSlashDateParser {
29 pub fn new() -> Self {
30 Self
31 }
32
33 fn parse_year(year_str: &str) -> Option<i32> {
34 let year: i32 = year_str.parse().ok()?;
35 Some(if year < 100 {
36 if year > 50 { 1900 + year } else { 2000 + year }
37 } else {
38 year
39 })
40 }
41
42 fn is_valid_date(year: i32, month: i32, day: i32) -> bool {
43 if !(1..=12).contains(&month) || !(1..=31).contains(&day) {
44 return false;
45 }
46 let days_in_month = match month {
47 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
48 4 | 6 | 9 | 11 => 30,
49 2 => {
50 if (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0) {
51 29
52 } else {
53 28
54 }
55 }
56 _ => return false,
57 };
58 day <= days_in_month
59 }
60}
61
62impl Parser for FRSlashDateParser {
63 fn name(&self) -> &'static str {
64 "FRSlashDateParser"
65 }
66
67 fn should_apply(&self, context: &ParsingContext) -> bool {
68 let text = context.text;
69 (text.contains('/') || text.contains('-') || text.contains('.'))
71 && text.bytes().any(|b| b.is_ascii_digit())
72 }
73
74 fn parse(&self, context: &ParsingContext) -> Result<Vec<ParsedResult>> {
75 let mut results = Vec::new();
76 let ref_date = context.reference.instant;
77
78 for mat in PATTERN.find_iter(context.text) {
79 let matched_text = mat.as_str();
80 let index = mat.start();
81 let match_end = mat.end();
82
83 let remaining = &context.text[match_end..];
85 if AM_PM_PATTERN.is_match(remaining) {
86 continue;
87 }
88
89 let Some(caps) = PATTERN.captures(matched_text) else {
90 continue;
91 };
92
93 let weekday = caps
95 .get(1)
96 .and_then(|m| dict::get_weekday(&m.as_str().to_lowercase()));
97
98 let day: i32 = caps
100 .get(2)
101 .and_then(|m| m.as_str().parse().ok())
102 .unwrap_or(0);
103 let month: i32 = caps
104 .get(3)
105 .and_then(|m| m.as_str().parse().ok())
106 .unwrap_or(0);
107
108 let year = caps.get(4).and_then(|m| Self::parse_year(m.as_str()));
110
111 let actual_year = year.unwrap_or(ref_date.year());
113 if !Self::is_valid_date(actual_year, month, day) {
114 continue;
115 }
116
117 let mut components = context.create_components();
118 if let Some(y) = year {
119 components.assign(Component::Year, y);
120 } else {
121 components.imply(Component::Year, ref_date.year());
123 }
124 components.assign(Component::Month, month);
125 components.assign(Component::Day, day);
126
127 if let Some(wd) = weekday {
129 components.assign(Component::Weekday, wd as i32);
130 }
131
132 let actual_start = matched_text
134 .find(|c: char| c.is_ascii_alphanumeric() || c.is_alphabetic())
135 .unwrap_or(0);
136 let actual_end = matched_text
137 .rfind(|c: char| c.is_ascii_digit())
138 .map(|i| i + matched_text[i..].chars().next().map_or(1, char::len_utf8))
139 .unwrap_or(matched_text.len());
140 let clean_text = &matched_text[actual_start..actual_end];
141
142 results.push(context.create_result(
143 index + actual_start,
144 index + actual_start + clean_text.len(),
145 components,
146 None,
147 ));
148 }
149
150 Ok(results)
151 }
152}
153
154impl Default for FRSlashDateParser {
155 fn default() -> Self {
156 Self::new()
157 }
158}