opening_hours_syntax/rules/
day.rs

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