Skip to main content

whichtime_sys/parsers/sv/
time_unit_relative.rs

1//! Swedish time unit relative parser
2//!
3//! Handles Swedish relative time expressions like:
4//! - "nästa 2 veckor" (next 2 weeks)
5//! - "förra 2 veckor" (past 2 weeks)
6//! - "efter en timme" (after one hour)
7//! - "+15 minuter" (plus 15 minutes)
8//! - "-3år" (minus 3 years)
9
10use crate::components::Component;
11use crate::context::ParsingContext;
12use crate::dictionaries::sv::{get_time_unit, parse_number_pattern};
13use crate::error::Result;
14use crate::parsers::Parser;
15use crate::results::ParsedResult;
16use crate::types::{Duration, TimeUnit, add_duration};
17use chrono::{Datelike, Timelike};
18use fancy_regex::Regex;
19use std::sync::LazyLock;
20
21// Pattern for "nästa/förra N units"
22static RELATIVE_PATTERN: LazyLock<Regex> = LazyLock::new(|| {
23    Regex::new(
24        r"(?i)(?<![a-zA-ZåäöÅÄÖ])(?P<modifier>nästa|nasta|förra|forra|kommande)\s+(?P<num>\d+|en|ett|två|tva|tre|fyra|fem|sex|sju|åtta|atta|nio|tio|elva|tolv)\s+(?P<unit>sekunder?|minuter?|timm(?:ar|e)?|dagar?|veckor?|månader?|manader?|år|ar)(?![a-zA-ZåäöÅÄÖ])"
25    ).unwrap()
26});
27
28// Pattern for "efter N units" (after N units)
29static EFTER_PATTERN: LazyLock<Regex> = LazyLock::new(|| {
30    Regex::new(
31        r"(?i)(?<![a-zA-ZåäöÅÄÖ])efter\s+(?P<num>\d+|en|ett|två|tva|tre|fyra|fem|sex|sju|åtta|atta|nio|tio|elva|tolv)\s+(?P<unit>sekunder?|minuter?|timm(?:ar|e)?|dagar?|veckor?|månader?|manader?|år|ar)(?![a-zA-ZåäöÅÄÖ])"
32    ).unwrap()
33});
34
35// Pattern for "+/-N units"
36static SIGN_PATTERN: LazyLock<Regex> = LazyLock::new(|| {
37    Regex::new(
38        r"(?<![a-zA-ZåäöÅÄÖ0-9])(?P<sign>[+\-])(?P<num>\d+)\s*(?P<unit>sekunder?|minuter?|timm(?:ar|e)?|dagar?|veckor?|månader?|manader?|år|ar)(?![a-zA-ZåäöÅÄÖ])"
39    ).unwrap()
40});
41
42/// Swedish time unit relative parser
43pub struct SVTimeUnitRelativeParser;
44
45impl SVTimeUnitRelativeParser {
46    pub fn new() -> Self {
47        Self
48    }
49
50    fn parse_unit(unit_str: &str) -> Option<TimeUnit> {
51        let lower = unit_str.to_lowercase();
52        get_time_unit(&lower)
53    }
54}
55
56impl Default for SVTimeUnitRelativeParser {
57    fn default() -> Self {
58        Self::new()
59    }
60}
61
62impl Parser for SVTimeUnitRelativeParser {
63    fn name(&self) -> &'static str {
64        "SVTimeUnitRelativeParser"
65    }
66
67    fn should_apply(&self, _context: &ParsingContext) -> bool {
68        true
69    }
70
71    fn parse(&self, context: &ParsingContext) -> Result<Vec<ParsedResult>> {
72        let mut results = Vec::new();
73        let ref_date = context.reference.instant;
74
75        // Parse "nästa/förra N units" patterns
76        let mut start = 0;
77        while start < context.text.len() {
78            let search_text = &context.text[start..];
79            let captures = match RELATIVE_PATTERN.captures(search_text) {
80                Ok(Some(caps)) => caps,
81                Ok(None) => break,
82                Err(_) => break,
83            };
84
85            let full_match = match captures.get(0) {
86                Some(m) => m,
87                None => break,
88            };
89
90            let match_start = start + full_match.start();
91            let match_end = start + full_match.end();
92
93            let modifier = captures
94                .name("modifier")
95                .map(|m| m.as_str().to_lowercase())
96                .unwrap_or_default();
97            let num_str = captures.name("num").map(|m| m.as_str()).unwrap_or("1");
98            let unit_str = captures
99                .name("unit")
100                .map(|m| m.as_str())
101                .unwrap_or_default();
102
103            let num = parse_number_pattern(num_str);
104            if let Some(unit) = Self::parse_unit(unit_str) {
105                let is_past = modifier.starts_with("förra") || modifier.starts_with("forra");
106                let multiplier = if is_past { -1.0 } else { 1.0 };
107                let adjusted_num = num * multiplier;
108
109                let mut duration = Duration::new();
110                match unit {
111                    TimeUnit::Second => duration.second = Some(adjusted_num),
112                    TimeUnit::Minute => duration.minute = Some(adjusted_num),
113                    TimeUnit::Hour => duration.hour = Some(adjusted_num),
114                    TimeUnit::Day => duration.day = Some(adjusted_num),
115                    TimeUnit::Week => duration.week = Some(adjusted_num),
116                    TimeUnit::Month => duration.month = Some(adjusted_num),
117                    TimeUnit::Year => duration.year = Some(adjusted_num),
118                    _ => {}
119                }
120
121                let target_date = add_duration(ref_date, &duration);
122
123                let mut components = context.create_components();
124                components.assign(Component::Year, target_date.year());
125                components.assign(Component::Month, target_date.month() as i32);
126                components.assign(Component::Day, target_date.day() as i32);
127
128                if duration.has_time_component() {
129                    components.assign(Component::Hour, target_date.hour() as i32);
130                    components.assign(Component::Minute, target_date.minute() as i32);
131                    components.assign(Component::Second, target_date.second() as i32);
132                } else {
133                    components.imply(Component::Hour, ref_date.hour() as i32);
134                    components.imply(Component::Minute, ref_date.minute() as i32);
135                }
136
137                results.push(context.create_result(match_start, match_end, components, None));
138            }
139
140            start = match_end;
141        }
142
143        // Parse "efter N units" patterns
144        start = 0;
145        while start < context.text.len() {
146            let search_text = &context.text[start..];
147            let captures = match EFTER_PATTERN.captures(search_text) {
148                Ok(Some(caps)) => caps,
149                Ok(None) => break,
150                Err(_) => break,
151            };
152
153            let full_match = match captures.get(0) {
154                Some(m) => m,
155                None => break,
156            };
157
158            let match_start = start + full_match.start();
159            let match_end = start + full_match.end();
160
161            // Skip if overlaps with existing results
162            let overlaps = results.iter().any(|r| {
163                (match_start >= r.index && match_start < r.index + r.text.len())
164                    || (r.index >= match_start && r.index < match_end)
165            });
166            if overlaps {
167                start = match_end;
168                continue;
169            }
170
171            let num_str = captures.name("num").map(|m| m.as_str()).unwrap_or("1");
172            let unit_str = captures
173                .name("unit")
174                .map(|m| m.as_str())
175                .unwrap_or_default();
176
177            let num = parse_number_pattern(num_str);
178            if let Some(unit) = Self::parse_unit(unit_str) {
179                let mut duration = Duration::new();
180                match unit {
181                    TimeUnit::Second => duration.second = Some(num),
182                    TimeUnit::Minute => duration.minute = Some(num),
183                    TimeUnit::Hour => duration.hour = Some(num),
184                    TimeUnit::Day => duration.day = Some(num),
185                    TimeUnit::Week => duration.week = Some(num),
186                    TimeUnit::Month => duration.month = Some(num),
187                    TimeUnit::Year => duration.year = Some(num),
188                    _ => {}
189                }
190
191                let target_date = add_duration(ref_date, &duration);
192
193                let mut components = context.create_components();
194                components.assign(Component::Year, target_date.year());
195                components.assign(Component::Month, target_date.month() as i32);
196                components.assign(Component::Day, target_date.day() as i32);
197
198                if duration.has_time_component() {
199                    components.assign(Component::Hour, target_date.hour() as i32);
200                    components.assign(Component::Minute, target_date.minute() as i32);
201                    components.assign(Component::Second, target_date.second() as i32);
202                } else {
203                    components.imply(Component::Hour, ref_date.hour() as i32);
204                    components.imply(Component::Minute, ref_date.minute() as i32);
205                }
206
207                results.push(context.create_result(match_start, match_end, components, None));
208            }
209
210            start = match_end;
211        }
212
213        // Parse "+/-N units" patterns
214        start = 0;
215        while start < context.text.len() {
216            let search_text = &context.text[start..];
217            let captures = match SIGN_PATTERN.captures(search_text) {
218                Ok(Some(caps)) => caps,
219                Ok(None) => break,
220                Err(_) => break,
221            };
222
223            let full_match = match captures.get(0) {
224                Some(m) => m,
225                None => break,
226            };
227
228            let match_start = start + full_match.start();
229            let match_end = start + full_match.end();
230
231            // Skip if overlaps with existing results
232            let overlaps = results.iter().any(|r| {
233                (match_start >= r.index && match_start < r.index + r.text.len())
234                    || (r.index >= match_start && r.index < match_end)
235            });
236            if overlaps {
237                start = match_end;
238                continue;
239            }
240
241            let sign = captures.name("sign").map(|m| m.as_str()).unwrap_or("+");
242            let num_str = captures.name("num").map(|m| m.as_str()).unwrap_or("1");
243            let unit_str = captures
244                .name("unit")
245                .map(|m| m.as_str())
246                .unwrap_or_default();
247
248            let num: f64 = num_str.parse().unwrap_or(0.0);
249            let adjusted_num = if sign == "-" { -num } else { num };
250
251            if let Some(unit) = Self::parse_unit(unit_str) {
252                let mut duration = Duration::new();
253                match unit {
254                    TimeUnit::Second => duration.second = Some(adjusted_num),
255                    TimeUnit::Minute => duration.minute = Some(adjusted_num),
256                    TimeUnit::Hour => duration.hour = Some(adjusted_num),
257                    TimeUnit::Day => duration.day = Some(adjusted_num),
258                    TimeUnit::Week => duration.week = Some(adjusted_num),
259                    TimeUnit::Month => duration.month = Some(adjusted_num),
260                    TimeUnit::Year => duration.year = Some(adjusted_num),
261                    _ => {}
262                }
263
264                let target_date = add_duration(ref_date, &duration);
265
266                let mut components = context.create_components();
267                components.assign(Component::Year, target_date.year());
268                components.assign(Component::Month, target_date.month() as i32);
269                components.assign(Component::Day, target_date.day() as i32);
270
271                // For signed operations, always include time
272                components.assign(Component::Hour, target_date.hour() as i32);
273                components.assign(Component::Minute, target_date.minute() as i32);
274
275                results.push(context.create_result(match_start, match_end, components, None));
276            }
277
278            start = match_end;
279        }
280
281        Ok(results)
282    }
283}