Skip to main content

whichtime_sys/parsers/fr/
weekday.rs

1//! French weekday parser
2//!
3//! Handles French weekday expressions like:
4//! - "lundi", "mardi", etc.
5//! - "vendredi prochain"
6//! - "vendredi dernier"
7//! - "vendredi prochain à 15h"
8
9use crate::components::Component;
10use crate::context::ParsingContext;
11use crate::dictionaries::fr as dict;
12use crate::error::Result;
13use crate::parsers::Parser;
14use crate::results::ParsedResult;
15use crate::scanner::TokenType;
16use chrono::{Datelike, Duration};
17use fancy_regex::Regex;
18use std::sync::LazyLock;
19
20static PATTERN: LazyLock<Regex> = LazyLock::new(|| {
21    Regex::new(
22        r"(?i)(?P<weekday>lundi|mardi|mercredi|jeudi|vendredi|samedi|dimanche)(?:\s+(?P<modifier>prochain|dernier|passé|passe))?(?:\s+(?:à\s+)?(?P<hour>\d{1,2})(?:h(?P<minute>\d{2})?)?)?"
23    ).unwrap()
24});
25
26/// French weekday parser
27pub struct FRWeekdayParser;
28
29impl FRWeekdayParser {
30    pub fn new() -> Self {
31        Self
32    }
33}
34
35impl Parser for FRWeekdayParser {
36    fn name(&self) -> &'static str {
37        "FRWeekdayParser"
38    }
39
40    fn should_apply(&self, context: &ParsingContext) -> bool {
41        context.has_token_type(TokenType::Weekday)
42    }
43
44    fn parse(&self, context: &ParsingContext) -> Result<Vec<ParsedResult>> {
45        let mut results = Vec::new();
46        let ref_date = context.reference.instant;
47
48        let mut start = 0;
49        while start < context.text.len() {
50            let search_text = &context.text[start..];
51            let mat = match PATTERN.find(search_text) {
52                Ok(Some(m)) => m,
53                Ok(None) => break,
54                Err(_) => break,
55            };
56
57            let matched_text = mat.as_str();
58            let index = start + mat.start();
59
60            let caps = match PATTERN.captures(matched_text) {
61                Ok(Some(c)) => c,
62                Ok(None) => {
63                    start = index + 1;
64                    continue;
65                }
66                Err(_) => {
67                    start = index + 1;
68                    continue;
69                }
70            };
71
72            let weekday_str = caps
73                .name("weekday")
74                .map(|m| m.as_str().to_lowercase())
75                .unwrap_or_default();
76
77            let weekday = match dict::get_weekday(&weekday_str) {
78                Some(w) => w,
79                None => {
80                    start = index + 1;
81                    continue;
82                }
83            };
84
85            let modifier = caps.name("modifier").map(|m| m.as_str().to_lowercase());
86
87            // Calculate target weekday (using Monday as start of week for French)
88            let ref_weekday = ref_date.weekday().num_days_from_monday() as i32;
89            // Convert our Weekday enum (Sunday=0) to Monday-based (Monday=0, Sunday=6)
90            let target_weekday = match weekday as i32 {
91                0 => 6,     // Sunday
92                n => n - 1, // Monday=0, Tuesday=1, etc.
93            };
94
95            let days_diff = match modifier.as_deref() {
96                Some("prochain") => {
97                    // Next occurrence, always in the future (at least 1 day ahead)
98                    let diff = target_weekday - ref_weekday;
99                    if diff <= 0 { diff + 7 } else { diff }
100                }
101                Some("dernier") | Some("passé") | Some("passe") => {
102                    // Last occurrence, always in the past
103                    let diff = target_weekday - ref_weekday;
104                    if diff >= 0 { diff - 7 } else { diff }
105                }
106                None | Some(_) => {
107                    // Default: find the weekday in the current week
108                    // This means the difference is simply target - reference
109                    // Monday of this week could be in the past, Sunday could be in the future
110                    target_weekday - ref_weekday
111                }
112            };
113
114            let target_date = ref_date + Duration::days(days_diff as i64);
115
116            let mut components = context.create_components();
117            components.assign(Component::Year, target_date.year());
118            components.assign(Component::Month, target_date.month() as i32);
119            components.assign(Component::Day, target_date.day() as i32);
120            components.assign(Component::Weekday, weekday as i32);
121
122            // Handle time if present
123            if let Some(hour_match) = caps.name("hour") {
124                let hour: i32 = hour_match.as_str().parse().unwrap_or(0);
125                let minute: i32 = caps
126                    .name("minute")
127                    .and_then(|m| m.as_str().parse().ok())
128                    .unwrap_or(0);
129                components.assign(Component::Hour, hour);
130                components.assign(Component::Minute, minute);
131            }
132
133            results.push(context.create_result(
134                index,
135                index + matched_text.len(),
136                components,
137                None,
138            ));
139
140            start = index + matched_text.len();
141        }
142
143        Ok(results)
144    }
145}
146
147impl Default for FRWeekdayParser {
148    fn default() -> Self {
149        Self::new()
150    }
151}