Skip to main content

whichtime_sys/parsers/fr/
time_unit_relative.rs

1//! French time unit relative parser
2//!
3//! Handles French relative time expressions like:
4//! - "la semaine prochaine"
5//! - "le mois dernier"
6//! - "les 2 prochaines semaines"
7//! - "les trois prochaines semaines"
8
9use crate::components::Component;
10use crate::context::ParsingContext;
11use crate::error::Result;
12use crate::parsers::Parser;
13use crate::results::ParsedResult;
14use chrono::{Datelike, Duration, Timelike};
15use fancy_regex::Regex;
16use std::sync::LazyLock;
17
18// Pattern for "la semaine prochaine", "le mois dernier", "les 2 prochaines semaines"
19static PATTERN: LazyLock<Regex> = LazyLock::new(|| {
20    Regex::new(
21        r"(?i)(?:l[ae]s?)\s+(?:(\d+|deux|trois|quatre|cinq|six|sept|huit|neuf|dix|une?)\s+)?(?:(prochaine?s?|dernière?s?|dernier|passée?s?|passe)\s+)?(semaines?|mois|ans?|années?|annees?)(?:\s+(prochaine?|dernière?|dernier|passée?|passe))?\b"
22    ).unwrap()
23});
24
25/// French time unit relative parser
26pub struct FRTimeUnitRelativeParser;
27
28impl FRTimeUnitRelativeParser {
29    pub fn new() -> Self {
30        Self
31    }
32
33    fn parse_number(s: &str) -> i32 {
34        match s.to_lowercase().as_str() {
35            "un" | "une" => 1,
36            "deux" => 2,
37            "trois" => 3,
38            "quatre" => 4,
39            "cinq" => 5,
40            "six" => 6,
41            "sept" => 7,
42            "huit" => 8,
43            "neuf" => 9,
44            "dix" => 10,
45            _ => s.parse().unwrap_or(1),
46        }
47    }
48}
49
50impl Parser for FRTimeUnitRelativeParser {
51    fn name(&self) -> &'static str {
52        "FRTimeUnitRelativeParser"
53    }
54
55    fn should_apply(&self, _context: &ParsingContext) -> bool {
56        true
57    }
58
59    fn parse(&self, context: &ParsingContext) -> Result<Vec<ParsedResult>> {
60        let mut results = Vec::new();
61        let ref_date = context.reference.instant;
62
63        let mut start = 0;
64        while start < context.text.len() {
65            let search_text = &context.text[start..];
66            let mat = match PATTERN.find(search_text) {
67                Ok(Some(m)) => m,
68                Ok(None) => break,
69                Err(_) => break,
70            };
71
72            let matched_text = mat.as_str();
73            let index = start + mat.start();
74
75            let caps = match PATTERN.captures(matched_text) {
76                Ok(Some(c)) => c,
77                Ok(None) => {
78                    start = index + 1;
79                    continue;
80                }
81                Err(_) => {
82                    start = index + 1;
83                    continue;
84                }
85            };
86
87            // Get number (default 1)
88            let num = caps
89                .get(1)
90                .map(|m| Self::parse_number(m.as_str()))
91                .unwrap_or(1);
92
93            // Get modifier (prochaine/dernier before or after unit)
94            let modifier_before = caps.get(2).map(|m| m.as_str().to_lowercase());
95            let modifier_after = caps.get(4).map(|m| m.as_str().to_lowercase());
96            let modifier = modifier_before.or(modifier_after).unwrap_or_default();
97
98            // Get time unit
99            let unit = caps
100                .get(3)
101                .map(|m| m.as_str().to_lowercase())
102                .unwrap_or_default();
103
104            // Determine direction
105            let is_future = modifier.contains("prochain");
106            let multiplier = if is_future { 1 } else { -1 };
107            let amount = num * multiplier;
108
109            // Calculate target date
110            let target_date = match unit.as_str() {
111                "semaine" | "semaines" => ref_date + Duration::weeks(amount as i64),
112                "mois" => {
113                    let new_month = ref_date.month() as i32 + amount;
114                    let (year_offset, month) = if new_month <= 0 {
115                        ((new_month - 12) / 12, ((new_month - 1) % 12 + 13) as u32)
116                    } else if new_month > 12 {
117                        ((new_month - 1) / 12, ((new_month - 1) % 12 + 1) as u32)
118                    } else {
119                        (0, new_month as u32)
120                    };
121                    let new_year = ref_date.year() + year_offset;
122                    ref_date
123                        .with_year(new_year)
124                        .and_then(|d| d.with_month(month))
125                        .unwrap_or(ref_date)
126                }
127                "an" | "année" | "annee" | "ans" | "années" | "annees" => {
128                    let new_year = ref_date.year() + amount;
129                    ref_date.with_year(new_year).unwrap_or(ref_date)
130                }
131                _ => {
132                    start = index + matched_text.len();
133                    continue;
134                }
135            };
136
137            let mut components = context.create_components();
138            components.assign(Component::Year, target_date.year());
139            components.assign(Component::Month, target_date.month() as i32);
140            components.assign(Component::Day, target_date.day() as i32);
141
142            // Preserve time from reference
143            components.imply(Component::Hour, ref_date.hour() as i32);
144            components.imply(Component::Minute, ref_date.minute() as i32);
145            components.imply(Component::Second, ref_date.second() as i32);
146
147            results.push(context.create_result(
148                index,
149                index + matched_text.len(),
150                components,
151                None,
152            ));
153
154            start = index + matched_text.len();
155        }
156
157        Ok(results)
158    }
159}
160
161impl Default for FRTimeUnitRelativeParser {
162    fn default() -> Self {
163        Self::new()
164    }
165}