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 i32);
105
106impl Deref for Year {
107    type Target = i32;
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
122/// A year range that ensures that bounds are always in the right order
123#[derive(Clone, Debug, Hash, PartialEq, Eq)]
124pub struct YearRange {
125    range: RangeInclusive<Year>,
126    step: u16,
127}
128
129impl YearRange {
130    /// Create a new `YearRange`. Return `None` if the bounds are in the wrong order.
131    pub fn new(range: RangeInclusive<Year>, mut step: u16) -> Option<Self> {
132        if range.start() > range.end() {
133            return None;
134        }
135
136        if range.start().abs_diff(**range.end()) < step.into() {
137            step = 1;
138        }
139
140        Some(Self { range, step })
141    }
142
143    /// Extract range and step from this object.
144    pub fn into_parts(&self) -> (RangeInclusive<Year>, u16) {
145        (self.range.clone(), self.step)
146    }
147}
148
149impl Display for YearRange {
150    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
151        let start = **self.range.start();
152        let end = **self.range.end();
153
154        write!(f, "{start}")?;
155
156        if start != end {
157            write!(f, "-{end}")?;
158
159            if self.step != 1 {
160                write!(f, "/{}", self.step)?;
161            }
162        }
163
164        Ok(())
165    }
166}
167
168// --
169// -- Enum: MonthdayRange
170// --
171
172#[derive(Clone, Debug, Hash, PartialEq, Eq)]
173pub enum MonthdayRange {
174    Month {
175        range: RangeInclusive<Month>,
176        year: Option<Year>,
177    },
178    Date {
179        start: (Date, DateOffset),
180        end: (Date, DateOffset),
181    },
182}
183
184impl Display for MonthdayRange {
185    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
186        match self {
187            Self::Month { range, year } => {
188                if let Some(year) = year {
189                    write!(f, "{}", **year)?;
190                }
191
192                write!(f, "{}", range.start())?;
193
194                if range.start() != range.end() {
195                    write!(f, "-{}", range.end())?;
196                }
197            }
198            Self::Date { start, end } => {
199                write!(f, "{}{}", start.0, start.1)?;
200
201                if start != end {
202                    write!(f, "-{}{}", end.0, end.1)?;
203                }
204            }
205        }
206
207        Ok(())
208    }
209}
210
211// --
212// -- Enum: Date
213// --
214
215#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
216pub enum Date {
217    Fixed {
218        year: Option<Year>,
219        month: Month,
220        day: u8,
221    },
222    Weekday {
223        year: Option<Year>,
224        month: Month,
225        wday: Weekday,
226        nth: i8,
227    },
228    Easter {
229        year: Option<Year>,
230    },
231}
232
233impl Date {
234    #[inline]
235    pub fn ymd(day: u8, month: Month, year: Year) -> Self {
236        Self::Fixed { day, month, year: Some(year) }
237    }
238
239    #[inline]
240    pub fn md(day: u8, month: Month) -> Self {
241        Self::Fixed { day, month, year: None }
242    }
243
244    #[inline]
245    pub fn year(&self) -> Option<Year> {
246        match *self {
247            Self::Fixed { year, .. } | Self::Weekday { year, .. } | Self::Easter { year } => year,
248        }
249    }
250}
251
252impl Display for Date {
253    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
254        match self {
255            Self::Fixed { year, month, day } => {
256                if let Some(year) = year {
257                    write!(f, "{} ", **year)?;
258                }
259
260                write!(f, "{month} {day}")
261            }
262            Self::Weekday { year, month, wday: weekday, nth } => {
263                if let Some(year) = year {
264                    write!(f, "{} ", **year)?;
265                }
266
267                write!(f, "{month} {}", wday_str(*weekday))?;
268
269                if *nth != 0 {
270                    write!(f, "[{nth}]")?;
271                }
272
273                Ok(())
274            }
275            Self::Easter { year } => {
276                if let Some(year) = year {
277                    write!(f, "{} ", **year)?;
278                }
279
280                write!(f, "easter")
281            }
282        }
283    }
284}
285
286// --
287// -- Struct: DateOffset
288// --
289
290#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq)]
291pub struct DateOffset {
292    pub wday_offset: WeekDayOffset,
293    pub day_offset: i16,
294}
295
296impl DateOffset {
297    #[inline]
298    pub fn apply(&self, mut date: NaiveDate) -> NaiveDate {
299        date += Duration::days(self.day_offset.into());
300
301        match self.wday_offset {
302            WeekDayOffset::None => {}
303            WeekDayOffset::Prev(target) => {
304                let diff = (7 + date.weekday().days_since(Weekday::Mon)
305                    - target.days_since(Weekday::Mon))
306                    % 7;
307
308                date -= Duration::days(diff.into());
309                debug_assert_eq!(date.weekday(), target);
310            }
311            WeekDayOffset::Next(target) => {
312                let diff = (7 + target.days_since(Weekday::Mon)
313                    - date.weekday().days_since(Weekday::Mon))
314                    % 7;
315
316                date += Duration::days(diff.into());
317                debug_assert_eq!(date.weekday(), target);
318            }
319        }
320
321        date
322    }
323}
324
325impl Display for DateOffset {
326    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
327        write!(f, "{}", self.wday_offset)?;
328        write_days_offset(f, self.day_offset)?;
329        Ok(())
330    }
331}
332
333// WeekDayOffset
334
335#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
336pub enum WeekDayOffset {
337    None,
338    Next(Weekday),
339    Prev(Weekday),
340}
341
342impl Default for WeekDayOffset {
343    #[inline]
344    fn default() -> Self {
345        Self::None
346    }
347}
348
349impl Display for WeekDayOffset {
350    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
351        match self {
352            Self::None => {}
353            Self::Next(wday) => write!(f, "+{}", wday_str(*wday))?,
354            Self::Prev(wday) => write!(f, "-{}", wday_str(*wday))?,
355        }
356
357        Ok(())
358    }
359}
360
361// --
362// -- Enum: WeekDayRange
363// --
364
365#[derive(Clone, Debug, Hash, PartialEq, Eq)]
366pub enum WeekDayRange {
367    Fixed {
368        range: RangeInclusive<Weekday>,
369        offset: i16,
370        nth_from_start: [bool; 5],
371        nth_from_end: [bool; 5],
372    },
373    Holiday {
374        kind: HolidayKind,
375        offset: i16,
376    },
377}
378
379impl Display for WeekDayRange {
380    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
381        match self {
382            Self::Fixed { range, offset, nth_from_start, nth_from_end } => {
383                write!(f, "{}", wday_str(*range.start()))?;
384
385                if range.start() != range.end() {
386                    write!(f, "-{}", wday_str(*range.end()))?;
387                }
388
389                if nth_from_start.contains(&false) || nth_from_end.contains(&false) {
390                    let pos_weeknum_iter = nth_from_start
391                        .iter()
392                        .enumerate()
393                        .filter(|(_, x)| **x)
394                        .map(|(idx, _)| (idx + 1) as isize);
395
396                    let neg_weeknum_iter = nth_from_end
397                        .iter()
398                        .enumerate()
399                        .filter(|(_, x)| **x)
400                        .map(|(idx, _)| -(idx as isize) - 1);
401
402                    let mut weeknum_iter = pos_weeknum_iter.chain(neg_weeknum_iter);
403
404                    if let Some(first_num) = weeknum_iter.next() {
405                        write!(f, "[{first_num}")?;
406
407                        for num in weeknum_iter {
408                            write!(f, ",{num}")?;
409                        }
410
411                        write!(f, "]")?;
412                    }
413                }
414
415                write_days_offset(f, *offset)?;
416            }
417            Self::Holiday { kind, offset } => {
418                write!(f, "{kind}")?;
419
420                if *offset != 0 {
421                    write!(f, " {offset}")?;
422                }
423            }
424        }
425
426        Ok(())
427    }
428}
429
430// --
431// -- Enum: HolidayKind
432// --
433
434#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
435pub enum HolidayKind {
436    Public,
437    School,
438}
439
440impl Display for HolidayKind {
441    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
442        match self {
443            Self::Public => write!(f, "PH"),
444            Self::School => write!(f, "SH"),
445        }
446    }
447}
448
449// --
450// -- Struct: WeekNum (newtype)
451// --
452
453#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
454pub struct WeekNum(pub u8);
455
456impl Deref for WeekNum {
457    type Target = u8;
458
459    fn deref(&self) -> &Self::Target {
460        &self.0
461    }
462}
463
464impl DerefMut for WeekNum {
465    fn deref_mut(&mut self) -> &mut Self::Target {
466        &mut self.0
467    }
468}
469
470// --
471// -- Struct: WeekRange
472// --
473
474// TODO: ensure there can't be wrapping with step
475#[derive(Clone, Debug, Hash, PartialEq, Eq)]
476pub struct WeekRange {
477    range: RangeInclusive<WeekNum>,
478    step: u8,
479}
480
481impl WeekRange {
482    /// Create a new `WeekRange`. Return `None` if the bounds are in the wrong order.
483    pub fn new(range: RangeInclusive<WeekNum>, mut step: u8) -> Option<Self> {
484        if range.start() > range.end() {
485            return None;
486        }
487
488        if range.start().abs_diff(**range.end()) < step {
489            step = 1;
490        }
491
492        Some(Self { range, step })
493    }
494
495    /// Extract range and step from this object.
496    pub fn into_parts(&self) -> (RangeInclusive<WeekNum>, u8) {
497        (self.range.clone(), self.step)
498    }
499}
500
501impl Display for WeekRange {
502    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
503        if self.range.start() == self.range.end() && self.step == 1 {
504            return write!(f, "{:02}", **self.range.start());
505        }
506
507        write!(f, "{:02}", **self.range.start())?;
508
509        if self.range.start() != self.range.end() {
510            write!(f, "-{:02}", **self.range.end())?;
511
512            if self.step != 1 {
513                write!(f, "/{}", self.step)?;
514            }
515        }
516
517        Ok(())
518    }
519}
520
521// Enum: Month
522
523#[derive(Copy, Clone, Debug, Hash, Eq, Ord, PartialEq, PartialOrd)]
524pub enum Month {
525    January = 1,
526    February = 2,
527    March = 3,
528    April = 4,
529    May = 5,
530    June = 6,
531    July = 7,
532    August = 8,
533    September = 9,
534    October = 10,
535    November = 11,
536    December = 12,
537}
538
539impl Month {
540    #[inline]
541    pub fn next(self) -> Self {
542        let num = self as u8;
543        #[allow(clippy::unwrap_used)] // (x % 12) + 1 is guaranteed to be in [1, 12]
544        ((num % 12) + 1).try_into().unwrap()
545    }
546
547    #[inline]
548    pub fn prev(self) -> Self {
549        let num = self as u8;
550        #[allow(clippy::unwrap_used)] // (x % 12) + 1 is guaranteed to be in [1, 12]
551        (((num + 10) % 12) + 1).try_into().unwrap()
552    }
553
554    /// Extract a month from a [`chrono::Datelike`].
555    #[inline]
556    pub fn from_date(date: impl Datelike) -> Self {
557        match date.month() {
558            1 => Self::January,
559            2 => Self::February,
560            3 => Self::March,
561            4 => Self::April,
562            5 => Self::May,
563            6 => Self::June,
564            7 => Self::July,
565            8 => Self::August,
566            9 => Self::September,
567            10 => Self::October,
568            11 => Self::November,
569            12 => Self::December,
570            other => unreachable!("Unexpected month for date `{other}`"),
571        }
572    }
573
574    /// Stringify the month (`"January"`, `"February"`, ...).
575    #[inline]
576    pub fn as_str(self) -> &'static str {
577        match self {
578            Self::January => "January",
579            Self::February => "February",
580            Self::March => "March",
581            Self::April => "April",
582            Self::May => "May",
583            Self::June => "June",
584            Self::July => "July",
585            Self::August => "August",
586            Self::September => "September",
587            Self::October => "October",
588            Self::November => "November",
589            Self::December => "December",
590        }
591    }
592}
593
594impl Display for Month {
595    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
596        write!(f, "{}", &self.as_str()[..3])
597    }
598}
599
600macro_rules! impl_convert_for_month {
601    ( $from_type: ty ) => {
602        impl TryFrom<$from_type> for Month {
603            type Error = InvalidMonth;
604
605            #[inline]
606            fn try_from(value: $from_type) -> Result<Self, Self::Error> {
607                let value: u8 = value.try_into().map_err(|_| InvalidMonth)?;
608
609                Ok(match value {
610                    1 => Self::January,
611                    2 => Self::February,
612                    3 => Self::March,
613                    4 => Self::April,
614                    5 => Self::May,
615                    6 => Self::June,
616                    7 => Self::July,
617                    8 => Self::August,
618                    9 => Self::September,
619                    10 => Self::October,
620                    11 => Self::November,
621                    12 => Self::December,
622                    _ => return Err(InvalidMonth),
623                })
624            }
625        }
626
627        impl From<Month> for $from_type {
628            fn from(val: Month) -> Self {
629                val as _
630            }
631        }
632    };
633    ( $from_type: ty, $( $tail: tt )+ ) => {
634        impl_convert_for_month!($from_type);
635        impl_convert_for_month!($($tail)+);
636    };
637}
638
639impl_convert_for_month!(u8, u16, u32, u64, u128, i8, i16, i32, i64, i128, usize, isize);