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