Skip to main content

opening_hours_syntax/rules/
day.rs

1use alloc::vec::Vec;
2use core::convert::{TryFrom, TryInto};
3use core::fmt::Display;
4use core::ops::{Deref, DerefMut, RangeInclusive};
5
6use chrono::prelude::Datelike;
7use chrono::{Duration, NaiveDate};
8
9// Reexport Weekday from chrono as part of the public type.
10pub use chrono::Weekday;
11
12use crate::display::{write_days_offset, write_selector};
13
14// Display
15
16fn wday_str(wday: Weekday) -> &'static str {
17    match wday {
18        Weekday::Mon => "Mo",
19        Weekday::Tue => "Tu",
20        Weekday::Wed => "We",
21        Weekday::Thu => "Th",
22        Weekday::Fri => "Fr",
23        Weekday::Sat => "Sa",
24        Weekday::Sun => "Su",
25    }
26}
27
28// Errors
29
30#[derive(Clone, Debug)]
31pub struct InvalidMonth;
32
33// DaySelector
34
35#[derive(Clone, Debug, Default, Hash, PartialEq, Eq)]
36pub struct DaySelector {
37    pub year: Vec<YearRange>,
38    pub monthday: Vec<MonthdayRange>,
39    pub week: Vec<WeekRange>,
40    pub weekday: Vec<WeekDayRange>,
41}
42
43impl DaySelector {
44    /// Return `true` if there is no date filter in this expression.
45    pub fn is_empty(&self) -> bool {
46        self.year.is_empty()
47            && self.monthday.is_empty()
48            && self.week.is_empty()
49            && self.weekday.is_empty()
50    }
51
52    /// Format this day selector into given formatter.
53    ///
54    /// If `force` is set to true, this is guaranteed to yield a non-empty string by adding "Mo-Su"
55    /// as fallback.
56    pub(crate) fn display(
57        &self,
58        f: &mut core::fmt::Formatter<'_>,
59        force: bool,
60    ) -> core::fmt::Result {
61        if force && self.is_empty() {
62            return write!(f, "Mo-Su");
63        }
64
65        if !(self.year.is_empty() && self.monthday.is_empty() && self.week.is_empty()) {
66            write_selector(f, &self.year)?;
67            write_selector(f, &self.monthday)?;
68
69            if !self.week.is_empty() {
70                if !self.year.is_empty() || !self.monthday.is_empty() {
71                    write!(f, " ")?;
72                }
73
74                write!(f, "week")?;
75                write_selector(f, &self.week)?;
76            }
77
78            if !self.weekday.is_empty() {
79                write!(f, " ")?;
80            }
81        }
82
83        write_selector(f, &self.weekday)
84    }
85}
86
87impl Display for DaySelector {
88    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
89        self.display(f, false)
90    }
91}
92
93// Year (newtype)
94
95#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
96pub struct Year(pub u16);
97
98impl Deref for Year {
99    type Target = u16;
100
101    fn deref(&self) -> &Self::Target {
102        &self.0
103    }
104}
105
106impl DerefMut for Year {
107    fn deref_mut(&mut self) -> &mut Self::Target {
108        &mut self.0
109    }
110}
111
112// YearRange
113#[derive(Clone, Debug, Hash, PartialEq, Eq)]
114pub struct YearRange {
115    pub range: RangeInclusive<Year>,
116    pub step: u16,
117}
118
119impl Display for YearRange {
120    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
121        write!(f, "{}", self.range.start().deref())?;
122
123        if self.range.start() != self.range.end() {
124            write!(f, "-{}", self.range.end().deref())?;
125        }
126
127        if self.step != 1 {
128            write!(f, "/{}", self.step)?;
129        }
130
131        Ok(())
132    }
133}
134
135// MonthdayRange
136
137#[derive(Clone, Debug, Hash, PartialEq, Eq)]
138pub enum MonthdayRange {
139    Month {
140        range: RangeInclusive<Month>,
141        year: Option<u16>,
142    },
143    Date {
144        start: (Date, DateOffset),
145        end: (Date, DateOffset),
146    },
147}
148
149impl Display for MonthdayRange {
150    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
151        match self {
152            Self::Month { range, year } => {
153                if let Some(year) = year {
154                    write!(f, "{year}")?;
155                }
156
157                write!(f, "{}", range.start())?;
158
159                if range.start() != range.end() {
160                    write!(f, "-{}", range.end())?;
161                }
162            }
163            Self::Date { start, end } => {
164                write!(f, "{}{}", start.0, start.1)?;
165
166                if start != end {
167                    write!(f, "-{}{}", end.0, end.1)?;
168                }
169            }
170        }
171
172        Ok(())
173    }
174}
175
176// Date
177
178#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
179pub enum Date {
180    Fixed {
181        year: Option<u16>,
182        month: Month,
183        day: u8,
184    },
185    Easter {
186        year: Option<u16>,
187    },
188}
189
190impl Date {
191    #[inline]
192    pub fn ymd(day: u8, month: Month, year: u16) -> Self {
193        Self::Fixed { day, month, year: Some(year) }
194    }
195
196    #[inline]
197    pub fn md(day: u8, month: Month) -> Self {
198        Self::Fixed { day, month, year: None }
199    }
200
201    #[inline]
202    pub fn has_year(&self) -> bool {
203        matches!(
204            self,
205            Self::Fixed { year: Some(_), .. } | Self::Easter { year: Some(_) }
206        )
207    }
208}
209
210impl Display for Date {
211    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
212        match self {
213            Date::Fixed { year, month, day } => {
214                if let Some(year) = year {
215                    write!(f, "{year} ")?;
216                }
217
218                write!(f, "{month} {day}")?;
219            }
220            Date::Easter { year } => {
221                if let Some(year) = year {
222                    write!(f, "{year} ")?;
223                }
224
225                write!(f, "easter")?;
226            }
227        }
228
229        Ok(())
230    }
231}
232
233// DateOffset
234
235#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq)]
236pub struct DateOffset {
237    pub wday_offset: WeekDayOffset,
238    pub day_offset: i64,
239}
240
241impl DateOffset {
242    #[inline]
243    pub fn apply(&self, mut date: NaiveDate) -> NaiveDate {
244        date += Duration::days(self.day_offset);
245
246        match self.wday_offset {
247            WeekDayOffset::None => {}
248            WeekDayOffset::Prev(target) => {
249                let diff = (7 + date.weekday().days_since(Weekday::Mon)
250                    - target.days_since(Weekday::Mon))
251                    % 7;
252
253                date -= Duration::days(diff.into());
254                debug_assert_eq!(date.weekday(), target);
255            }
256            WeekDayOffset::Next(target) => {
257                let diff = (7 + target.days_since(Weekday::Mon)
258                    - date.weekday().days_since(Weekday::Mon))
259                    % 7;
260
261                date += Duration::days(diff.into());
262                debug_assert_eq!(date.weekday(), target);
263            }
264        }
265
266        date
267    }
268}
269
270impl Display for DateOffset {
271    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
272        write!(f, "{}", self.wday_offset)?;
273        write_days_offset(f, self.day_offset)?;
274        Ok(())
275    }
276}
277
278// WeekDayOffset
279
280#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
281pub enum WeekDayOffset {
282    None,
283    Next(Weekday),
284    Prev(Weekday),
285}
286
287impl Default for WeekDayOffset {
288    #[inline]
289    fn default() -> Self {
290        Self::None
291    }
292}
293
294impl Display for WeekDayOffset {
295    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
296        match self {
297            Self::None => {}
298            Self::Next(wday) => write!(f, "+{}", wday_str(*wday))?,
299            Self::Prev(wday) => write!(f, "-{}", wday_str(*wday))?,
300        }
301
302        Ok(())
303    }
304}
305
306// WeekDayRange
307
308#[derive(Clone, Debug, Hash, PartialEq, Eq)]
309pub enum WeekDayRange {
310    Fixed {
311        range: RangeInclusive<Weekday>,
312        offset: i64,
313        nth_from_start: [bool; 5],
314        nth_from_end: [bool; 5],
315    },
316    Holiday {
317        kind: HolidayKind,
318        offset: i64,
319    },
320}
321
322impl Display for WeekDayRange {
323    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
324        match self {
325            Self::Fixed { range, offset, nth_from_start, nth_from_end } => {
326                write!(f, "{}", wday_str(*range.start()))?;
327
328                if range.start() != range.end() {
329                    write!(f, "-{}", wday_str(*range.end()))?;
330                }
331
332                if nth_from_start.contains(&false) || nth_from_end.contains(&false) {
333                    let pos_weeknum_iter = nth_from_start
334                        .iter()
335                        .enumerate()
336                        .filter(|(_, x)| **x)
337                        .map(|(idx, _)| (idx + 1) as isize);
338
339                    let neg_weeknum_iter = nth_from_end
340                        .iter()
341                        .enumerate()
342                        .filter(|(_, x)| **x)
343                        .map(|(idx, _)| -(idx as isize) - 1);
344
345                    let mut weeknum_iter = pos_weeknum_iter.chain(neg_weeknum_iter);
346                    write!(f, "[{}", weeknum_iter.next().unwrap())?;
347
348                    for num in weeknum_iter {
349                        write!(f, ",{num}")?;
350                    }
351
352                    write!(f, "]")?;
353                }
354
355                write_days_offset(f, *offset)?;
356            }
357            Self::Holiday { kind, offset } => {
358                write!(f, "{kind}")?;
359
360                if *offset != 0 {
361                    write!(f, " {offset}")?;
362                }
363            }
364        }
365
366        Ok(())
367    }
368}
369
370// HolidayKind
371
372#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
373pub enum HolidayKind {
374    Public,
375    School,
376}
377
378impl Display for HolidayKind {
379    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
380        match self {
381            Self::Public => write!(f, "PH"),
382            Self::School => write!(f, "SH"),
383        }
384    }
385}
386
387// WeekNum (newtype)
388
389#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
390pub struct WeekNum(pub u8);
391
392impl Deref for WeekNum {
393    type Target = u8;
394
395    fn deref(&self) -> &Self::Target {
396        &self.0
397    }
398}
399
400impl DerefMut for WeekNum {
401    fn deref_mut(&mut self) -> &mut Self::Target {
402        &mut self.0
403    }
404}
405
406// WeekRange
407
408#[derive(Clone, Debug, Hash, PartialEq, Eq)]
409pub struct WeekRange {
410    pub range: RangeInclusive<WeekNum>,
411    pub step: u8,
412}
413
414impl Display for WeekRange {
415    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
416        if self.range.start() == self.range.end() && self.step == 1 {
417            return write!(f, "{:02}", **self.range.start());
418        }
419
420        write!(f, "{:02}-{:02}", **self.range.start(), **self.range.end())?;
421
422        if self.step != 1 {
423            write!(f, "/{}", self.step)?;
424        }
425
426        Ok(())
427    }
428}
429
430// Month
431
432#[derive(Copy, Clone, Debug, Hash, Eq, Ord, PartialEq, PartialOrd)]
433pub enum Month {
434    January = 1,
435    February = 2,
436    March = 3,
437    April = 4,
438    May = 5,
439    June = 6,
440    July = 7,
441    August = 8,
442    September = 9,
443    October = 10,
444    November = 11,
445    December = 12,
446}
447
448impl Month {
449    #[inline]
450    pub fn next(self) -> Self {
451        let num = self as u8;
452        ((num % 12) + 1).try_into().unwrap()
453    }
454
455    #[inline]
456    pub fn prev(self) -> Self {
457        let num = self as u8;
458        (((num + 10) % 12) + 1).try_into().unwrap()
459    }
460
461    /// Extract a month from a [`chrono::Datelike`].
462    #[inline]
463    pub fn from_date(date: impl Datelike) -> Self {
464        match date.month() {
465            1 => Self::January,
466            2 => Self::February,
467            3 => Self::March,
468            4 => Self::April,
469            5 => Self::May,
470            6 => Self::June,
471            7 => Self::July,
472            8 => Self::August,
473            9 => Self::September,
474            10 => Self::October,
475            11 => Self::November,
476            12 => Self::December,
477            other => unreachable!("Unexpected month for date `{other}`"),
478        }
479    }
480
481    /// Stringify the month (`"January"`, `"February"`, ...).
482    #[inline]
483    pub fn as_str(self) -> &'static str {
484        match self {
485            Self::January => "January",
486            Self::February => "February",
487            Self::March => "March",
488            Self::April => "April",
489            Self::May => "May",
490            Self::June => "June",
491            Self::July => "July",
492            Self::August => "August",
493            Self::September => "September",
494            Self::October => "October",
495            Self::November => "November",
496            Self::December => "December",
497        }
498    }
499}
500
501impl Display for Month {
502    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
503        write!(f, "{}", &self.as_str()[..3])
504    }
505}
506
507macro_rules! impl_convert_for_month {
508    ( $from_type: ty ) => {
509        impl TryFrom<$from_type> for Month {
510            type Error = InvalidMonth;
511
512            #[inline]
513            fn try_from(value: $from_type) -> Result<Self, Self::Error> {
514                let value: u8 = value.try_into().map_err(|_| InvalidMonth)?;
515
516                Ok(match value {
517                    1 => Self::January,
518                    2 => Self::February,
519                    3 => Self::March,
520                    4 => Self::April,
521                    5 => Self::May,
522                    6 => Self::June,
523                    7 => Self::July,
524                    8 => Self::August,
525                    9 => Self::September,
526                    10 => Self::October,
527                    11 => Self::November,
528                    12 => Self::December,
529                    _ => return Err(InvalidMonth),
530                })
531            }
532        }
533
534        impl From<Month> for $from_type {
535            fn from(val: Month) -> Self {
536                val as _
537            }
538        }
539    };
540    ( $from_type: ty, $( $tail: tt )+ ) => {
541        impl_convert_for_month!($from_type);
542        impl_convert_for_month!($($tail)+);
543    };
544}
545
546impl_convert_for_month!(u8, u16, u32, u64, u128, i8, i16, i32, i64, i128, usize, isize);