Skip to main content

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