Skip to main content

whichtime_sys/parsers/sv/
weekday.rs

1//! Swedish weekday parser
2//!
3//! Handles Swedish weekday expressions like:
4//! - "måndag", "tisdag", etc.
5//! - "på måndag" (on Monday)
6//! - "nästa måndag" (next Monday)
7//! - "förra måndag" (last Monday)
8
9use crate::components::Component;
10use crate::context::ParsingContext;
11use crate::dictionaries::sv::get_weekday;
12use crate::error::Result;
13use crate::parsers::Parser;
14use crate::results::ParsedResult;
15use chrono::{Datelike, Duration, Weekday as ChronoWeekday};
16use fancy_regex::Regex;
17use std::sync::LazyLock;
18
19static PATTERN: LazyLock<Regex> = LazyLock::new(|| {
20    Regex::new(
21        r"(?i)(?<![a-zA-ZåäöÅÄÖ])(?:(?P<modifier>nästa|nasta|förra|forra|kommande|denna|i)\s+)?(?P<prep>på\s+)?(?P<weekday>söndag|sondag|måndag|mandag|tisdag|onsdag|torsdag|fredag|lördag|lordag|sön\.?|son\.?|mån\.?|man\.?|tis\.?|ons\.?|tor\.?|tors\.?|fre\.?|lör\.?|lor\.?)(?![a-zA-ZåäöÅÄÖ])"
22    ).unwrap()
23});
24
25/// Swedish weekday parser
26pub struct SVWeekdayParser;
27
28impl SVWeekdayParser {
29    pub fn new() -> Self {
30        Self
31    }
32}
33
34impl Default for SVWeekdayParser {
35    fn default() -> Self {
36        Self::new()
37    }
38}
39
40impl Parser for SVWeekdayParser {
41    fn name(&self) -> &'static str {
42        "SVWeekdayParser"
43    }
44
45    fn should_apply(&self, _context: &ParsingContext) -> bool {
46        true
47    }
48
49    fn parse(&self, context: &ParsingContext) -> Result<Vec<ParsedResult>> {
50        let mut results = Vec::new();
51        let ref_date = context.reference.instant;
52
53        let mut start = 0;
54        while start < context.text.len() {
55            let search_text = &context.text[start..];
56            let captures = match PATTERN.captures(search_text) {
57                Ok(Some(caps)) => caps,
58                Ok(None) => break,
59                Err(_) => break,
60            };
61
62            let full_match = match captures.get(0) {
63                Some(m) => m,
64                None => break,
65            };
66
67            let match_start = start + full_match.start();
68            let match_end = start + full_match.end();
69
70            let modifier = captures.name("modifier").map(|m| m.as_str().to_lowercase());
71            let weekday_str = captures
72                .name("weekday")
73                .map(|m| m.as_str().to_lowercase())
74                .unwrap_or_default();
75
76            // Clean weekday string (remove trailing dots)
77            let clean_weekday = weekday_str.trim_end_matches('.');
78
79            let Some(weekday) = get_weekday(clean_weekday) else {
80                start = match_end;
81                continue;
82            };
83
84            let mut components = context.create_components();
85
86            // Convert our weekday to chrono weekday for calculation
87            let target_weekday = match weekday {
88                crate::types::Weekday::Sunday => ChronoWeekday::Sun,
89                crate::types::Weekday::Monday => ChronoWeekday::Mon,
90                crate::types::Weekday::Tuesday => ChronoWeekday::Tue,
91                crate::types::Weekday::Wednesday => ChronoWeekday::Wed,
92                crate::types::Weekday::Thursday => ChronoWeekday::Thu,
93                crate::types::Weekday::Friday => ChronoWeekday::Fri,
94                crate::types::Weekday::Saturday => ChronoWeekday::Sat,
95            };
96
97            let current_weekday = ref_date.weekday();
98            let current_day_num = current_weekday.num_days_from_sunday() as i64;
99            let target_day_num = target_weekday.num_days_from_sunday() as i64;
100
101            // Calculate days difference
102            let mut days_diff = target_day_num - current_day_num;
103
104            // Handle modifiers
105            let target_date = match modifier.as_deref() {
106                Some(m) if m.starts_with("nästa") || m.starts_with("nasta") || m == "kommande" => {
107                    // Next occurrence (always in the future, at least 1 day ahead)
108                    if days_diff <= 0 {
109                        days_diff += 7;
110                    }
111                    ref_date + Duration::days(days_diff)
112                }
113                Some(m) if m.starts_with("förra") || m.starts_with("forra") => {
114                    // Last occurrence (always in the past)
115                    if days_diff >= 0 {
116                        days_diff -= 7;
117                    }
118                    ref_date + Duration::days(days_diff)
119                }
120                _ => {
121                    // Default: find the closest occurrence (prefer past for same day)
122                    if days_diff > 0 {
123                        days_diff -= 7;
124                    }
125                    ref_date + Duration::days(days_diff)
126                }
127            };
128
129            components.assign(Component::Year, target_date.year());
130            components.assign(Component::Month, target_date.month() as i32);
131            components.assign(Component::Day, target_date.day() as i32);
132            components.assign(Component::Weekday, weekday as i32);
133
134            results.push(context.create_result(match_start, match_end, components, None));
135            start = match_end;
136        }
137
138        Ok(results)
139    }
140}