Skip to main content

opening_hours_syntax/
parser.rs

1use alloc::string::{String, ToString};
2use alloc::vec::Vec;
3use core::cmp::Ord;
4use core::convert::TryInto;
5use core::fmt::Debug;
6use core::hash::Hash;
7use core::ops::RangeInclusive;
8
9use chrono::Duration;
10
11use pest::iterators::Pair;
12
13use crate::error::{Error, Result, err_empty};
14use crate::extended_time::ExtendedTime;
15use crate::rules::day::{self as ds, WeekNum, Year};
16use crate::rules::time as ts;
17use crate::util::pairs::PairsIterExtension;
18use crate::util::sign::Sign;
19use crate::util::text::{is_capitalized, is_lowercase};
20use crate::{Warning, rules as rl};
21
22#[derive(pest_derive::Parser)]
23#[grammar = "grammar.pest"]
24struct Grammar;
25
26/// Parse the expression with a default parser configuration (no warning handling).
27pub fn parse(data: &str) -> Result<rl::OpeningHoursExpression> {
28    Parser::default().parse(data)
29}
30
31impl alloc::str::FromStr for rl::OpeningHoursExpression {
32    type Err = Error;
33
34    fn from_str(s: &str) -> core::result::Result<Self, Self::Err> {
35        parse(s)
36    }
37}
38
39// --
40// -- Time domain
41// --
42
43/// A configured parser.
44pub struct Parser<F: FnMut(Warning) = fn(Warning)> {
45    warning_handler: F,
46}
47
48impl Default for Parser<fn(Warning)> {
49    fn default() -> Self {
50        Self { warning_handler: |_| {} }
51    }
52}
53
54impl<F: FnMut(Warning)> Parser<F> {
55    /// Parse an opening hours expression by using this parser.
56    pub fn parse(&mut self, data: &str) -> Result<rl::OpeningHoursExpression> {
57        use pest::Parser;
58
59        Grammar::parse(Rule::input_opening_hours, data)
60            .map_err(Error::from)?
61            .next()
62            .ok_or(Error::GrammarLogic {
63                rule: Rule::input_opening_hours,
64                invariant: "cannot be missing",
65            })
66            .and_then(|p| self.build_opening_hours(p))
67            .map(|rules| rl::OpeningHoursExpression { rules })
68            .inspect_err(|err| {
69                debug_assert!(
70                    !err.is_implementation_error(),
71                    "parser implementation error: {err:?}",
72                )
73            })
74    }
75
76    /// Attach a warning handler callback to this parser.
77    pub fn with_warning_handler<G: FnMut(Warning)>(self, warning_handler: G) -> Parser<G> {
78        Parser { warning_handler }
79    }
80
81    // --
82    // -- Implementation
83    // --
84
85    fn warn(&mut self, warning: Warning) {
86        (self.warning_handler)(warning)
87    }
88
89    fn build_opening_hours(&mut self, pair: Pair<Rule>) -> Result<Vec<rl::RuleSequence>> {
90        debug_assert_eq!(pair.as_rule(), Rule::opening_hours);
91        let mut pairs = pair.into_inner();
92        let mut rules = Vec::new();
93
94        while let Some(pair) = pairs.next() {
95            rules.push(match pair.as_rule() {
96                Rule::rule_sequence => self.build_rule_sequence(pair, rl::RuleOperator::Normal),
97                Rule::any_rule_separator => {
98                    let separator = self.build_any_rule_separator(pair)?;
99
100                    self.build_rule_sequence(
101                        pairs.next().ok_or(Error::GrammarLogic {
102                            rule: Rule::opening_hours,
103                            invariant: "a separator is always followed by a rule",
104                        })?,
105                        separator,
106                    )
107                }
108                unexpected => {
109                    return Err(Error::GrammarUnexpectedToken {
110                        rule: Rule::opening_hours,
111                        unexpected,
112                    });
113                }
114            }?)
115        }
116
117        Ok(rules)
118    }
119
120    fn build_rule_sequence(
121        &mut self,
122        pair: Pair<Rule>,
123        operator: rl::RuleOperator,
124    ) -> Result<rl::RuleSequence> {
125        debug_assert_eq!(pair.as_rule(), Rule::rule_sequence);
126        let mut pairs = pair.into_inner();
127        let root_pair = pairs.next().ok_or(err_empty(Rule::rule_sequence))?;
128        let (day_selector, time_selector, extra_comment) =
129            self.build_selector_sequence(root_pair)?;
130
131        let (kind, comment) = pairs
132            .next()
133            .map(|p| self.build_rules_modifier(p))
134            .transpose()?
135            .unwrap_or((rl::RuleKind::Open, None));
136
137        let comment = comment
138            .into_iter()
139            .chain(extra_comment)
140            .next()
141            .unwrap_or_default()
142            .into();
143
144        Ok(rl::RuleSequence {
145            day_selector,
146            time_selector,
147            kind,
148            operator,
149            comment,
150        })
151    }
152
153    fn build_any_rule_separator(&mut self, pair: Pair<Rule>) -> Result<rl::RuleOperator> {
154        debug_assert_eq!(pair.as_rule(), Rule::any_rule_separator);
155
156        let root_pair = pair
157            .into_inner()
158            .next()
159            .ok_or(err_empty(Rule::any_rule_separator))?;
160
161        match root_pair.as_rule() {
162            Rule::normal_rule_separator => Ok(rl::RuleOperator::Normal),
163            Rule::additional_rule_separator => Ok(rl::RuleOperator::Additional),
164            Rule::fallback_rule_separator => Ok(rl::RuleOperator::Fallback),
165            unexpected => {
166                Err(Error::GrammarUnexpectedToken { rule: Rule::any_rule_separator, unexpected })
167            }
168        }
169    }
170
171    // --
172    // -- Rule modifier
173    // --
174
175    fn build_rules_modifier(&mut self, pair: Pair<Rule>) -> Result<(rl::RuleKind, Option<String>)> {
176        debug_assert_eq!(pair.as_rule(), Rule::rules_modifier);
177        let mut pairs = pair.into_inner();
178
179        let kind = pairs
180            .next_if_rule(Rule::rules_modifier_enum)
181            .map(|p| self.build_rules_modifier_enum(p))
182            .transpose()?
183            .unwrap_or(rl::RuleKind::Open);
184
185        let comment = pairs.next().map(|p| self.build_comment(p)).transpose()?;
186        Ok((kind, comment))
187    }
188
189    fn build_rules_modifier_enum(&mut self, pair: Pair<Rule>) -> Result<rl::RuleKind> {
190        debug_assert_eq!(pair.as_rule(), Rule::rules_modifier_enum);
191
192        if !is_lowercase(pair.as_str()) {
193            self.warn(Warning::ShouldBeLowercase(pair.clone()));
194        }
195
196        let pair = (pair.into_inner())
197            .next()
198            .ok_or(err_empty(Rule::rules_modifier_enum))?;
199
200        match pair.as_rule() {
201            Rule::rules_modifier_enum_closed => Ok(rl::RuleKind::Closed),
202            Rule::rules_modifier_enum_open => Ok(rl::RuleKind::Open),
203            Rule::rules_modifier_enum_unknown => Ok(rl::RuleKind::Unknown),
204            unexpected => {
205                Err(Error::GrammarUnexpectedToken { rule: Rule::rules_modifier_enum, unexpected })
206            }
207        }
208    }
209
210    // --
211    // -- Selectors
212    // --
213
214    fn build_selector_sequence(
215        &mut self,
216        pair: Pair<Rule>,
217    ) -> Result<(ds::DaySelector, ts::TimeSelector, Option<String>)> {
218        debug_assert_eq!(pair.as_rule(), Rule::selector_sequence);
219        let mut pairs = pair.into_inner();
220
221        if pairs.next_if_rule(Rule::always_open).is_some() {
222            return Ok(Default::default());
223        }
224
225        let (year, monthday, week, comment) = pairs
226            .next_if_rule(Rule::wide_range_selectors)
227            .map(|p| self.build_wide_range_selectors(p))
228            .transpose()?
229            .unwrap_or_default();
230
231        let (weekday, time) = pairs
232            .next()
233            .map(|p| self.build_small_range_selectors(p))
234            .transpose()?
235            .unwrap_or_default();
236
237        Ok((
238            ds::DaySelector { year, monthday, week, weekday },
239            ts::TimeSelector::new(time),
240            comment,
241        ))
242    }
243
244    #[allow(clippy::type_complexity)]
245    fn build_wide_range_selectors(
246        &mut self,
247        pair: Pair<Rule>,
248    ) -> Result<(
249        Vec<ds::YearRange>,
250        Vec<ds::MonthdayRange>,
251        Vec<ds::WeekRange>,
252        Option<String>,
253    )> {
254        debug_assert_eq!(pair.as_rule(), Rule::wide_range_selectors);
255
256        let mut year_selector = Vec::new();
257        let mut monthday_selector = Vec::new();
258        let mut week_selector = Vec::new();
259        let mut comment = None;
260
261        for pair in pair.into_inner() {
262            match pair.as_rule() {
263                Rule::year_selector => year_selector = self.build_year_selector(pair)?,
264                Rule::monthday_selector => {
265                    monthday_selector = self.build_monthday_selector(pair)?
266                }
267                Rule::week_selector => week_selector = self.build_week_selector(pair)?,
268                Rule::comment => comment = Some(self.build_comment(pair)?),
269                unexpected => {
270                    return Err(Error::GrammarUnexpectedToken {
271                        rule: Rule::wide_range_selectors,
272                        unexpected,
273                    });
274                }
275            }
276        }
277
278        Ok((year_selector, monthday_selector, week_selector, comment))
279    }
280
281    fn build_small_range_selectors(
282        &mut self,
283        pair: Pair<Rule>,
284    ) -> Result<(Vec<ds::WeekDayRange>, Vec<ts::TimeSpan>)> {
285        debug_assert_eq!(pair.as_rule(), Rule::small_range_selectors);
286
287        let mut weekday_selector = Vec::new();
288        let mut time_selector = Vec::new();
289
290        for pair in pair.into_inner() {
291            match pair.as_rule() {
292                Rule::weekday_selector => weekday_selector = self.build_weekday_selector(pair)?,
293                Rule::time_selector => time_selector = self.build_time_selector(pair)?,
294                unexpected => {
295                    return Err(Error::GrammarUnexpectedToken {
296                        rule: Rule::wide_range_selectors,
297                        unexpected,
298                    });
299                }
300            }
301        }
302
303        Ok((weekday_selector, time_selector))
304    }
305
306    // --
307    // -- Time selector
308    // --
309
310    fn build_time_selector(&mut self, pair: Pair<Rule>) -> Result<Vec<ts::TimeSpan>> {
311        debug_assert_eq!(pair.as_rule(), Rule::time_selector);
312        pair.into_inner().map(|p| self.build_timespan(p)).collect()
313    }
314
315    fn build_timespan(&mut self, pair: Pair<Rule>) -> Result<ts::TimeSpan> {
316        debug_assert_eq!(pair.as_rule(), Rule::timespan);
317        let mut pairs = pair.into_inner();
318        let mut repeats = None;
319        let start = self.build_time(pairs.next().ok_or(err_empty(Rule::timespan))?)?;
320
321        let (mut open_end, end) = match pairs.next() {
322            None => {
323                return Err(Error::Unsupported("point in time"));
324            }
325            Some(pair) if pair.as_rule() == Rule::timespan_plus => {
326                // TODO: opening_hours.js handles this better: it will set the
327                //       state to unknown and add a warning comment.
328                (true, ts::Time::Fixed(ExtendedTime::MIDNIGHT_24))
329            }
330            Some(pair) => (false, self.build_extended_time(pair)?),
331        };
332
333        if let Some(pair_repetition) = pairs.next() {
334            match pair_repetition.as_rule() {
335                Rule::timespan_plus => open_end = true,
336                Rule::minute => repeats = Some(self.build_minute(pair_repetition)?),
337                Rule::hour_minutes => {
338                    repeats = Some(self.build_hour_minutes_as_duration(pair_repetition)?)
339                }
340                unexpected => {
341                    return Err(Error::GrammarUnexpectedToken { rule: Rule::timespan, unexpected });
342                }
343            }
344        }
345
346        debug_assert!(pairs.next().is_none());
347        Ok(ts::TimeSpan { range: start..end, repeats, open_end })
348    }
349
350    fn build_time(&mut self, pair: Pair<Rule>) -> Result<ts::Time> {
351        debug_assert_eq!(pair.as_rule(), Rule::time);
352        let root_pair = pair.into_inner().next().ok_or(err_empty(Rule::time))?;
353
354        Ok(match root_pair.as_rule() {
355            Rule::hour_minutes => ts::Time::Fixed(self.build_hour_minutes(root_pair)?),
356            Rule::variable_time => ts::Time::Variable(self.build_variable_time(root_pair)?),
357            unexpected => {
358                return Err(Error::GrammarUnexpectedToken { rule: Rule::time, unexpected });
359            }
360        })
361    }
362
363    fn build_extended_time(&mut self, pair: Pair<Rule>) -> Result<ts::Time> {
364        debug_assert_eq!(pair.as_rule(), Rule::extended_time);
365
366        let root_pair = pair
367            .into_inner()
368            .next()
369            .ok_or(err_empty(Rule::extended_time))?;
370
371        match root_pair.as_rule() {
372            Rule::extended_hour_minutes => self
373                .build_extended_hour_minutes(root_pair)
374                .map(ts::Time::Fixed),
375            Rule::variable_time => self.build_variable_time(root_pair).map(ts::Time::Variable),
376            unexpected => {
377                Err(Error::GrammarUnexpectedToken { rule: Rule::extended_time, unexpected })
378            }
379        }
380    }
381
382    fn build_variable_time(&mut self, pair: Pair<Rule>) -> Result<ts::VariableTime> {
383        debug_assert_eq!(pair.as_rule(), Rule::variable_time);
384        let mut pairs = pair.into_inner();
385        let event = self.build_event(pairs.next().ok_or(err_empty(Rule::variable_time))?)?;
386
387        let offset = {
388            if let Some(sign_pair) = pairs.next() {
389                let sign = self.build_plus_or_minus(sign_pair)?;
390
391                let hour_minutes_pair = pairs.next().ok_or(Error::GrammarLogic {
392                    rule: Rule::variable_time,
393                    invariant: "a sign is always followed by hours and minutes",
394                })?;
395
396                let mins: i16 = self
397                    .build_hour_minutes(hour_minutes_pair)?
398                    .mins_from_midnight()
399                    .try_into()
400                    .map_err(|_| Error::GrammarLogic {
401                        rule: Rule::variable_time,
402                        invariant: "daily number of minutes fits in an i16",
403                    })?;
404
405                match sign {
406                    Sign::Pos => mins,
407                    Sign::Neg => -mins,
408                }
409            } else {
410                0
411            }
412        };
413
414        Ok(ts::VariableTime { event, offset })
415    }
416
417    fn build_event(&mut self, pair: Pair<Rule>) -> Result<ts::TimeEvent> {
418        debug_assert_eq!(pair.as_rule(), Rule::event);
419
420        if !is_lowercase(pair.as_str()) {
421            self.warn(Warning::ShouldBeLowercase(pair.clone()))
422        }
423
424        let pair = (pair.clone().into_inner())
425            .next()
426            .ok_or(err_empty(Rule::event))?;
427
428        match pair.as_rule() {
429            Rule::dawn => Ok(ts::TimeEvent::Dawn),
430            Rule::sunrise => Ok(ts::TimeEvent::Sunrise),
431            Rule::sunset => Ok(ts::TimeEvent::Sunset),
432            Rule::dusk => Ok(ts::TimeEvent::Dusk),
433            unexpected => Err(Error::GrammarUnexpectedToken { rule: Rule::event, unexpected }),
434        }
435    }
436
437    // --
438    // -- WeekDay selector
439    // --
440
441    fn build_weekday_selector(&mut self, pair: Pair<Rule>) -> Result<Vec<ds::WeekDayRange>> {
442        debug_assert_eq!(pair.as_rule(), Rule::weekday_selector);
443        let mut ranges = Vec::new();
444
445        for pair in pair.into_inner() {
446            match pair.as_rule() {
447                Rule::weekday_sequence => {
448                    for pair in pair.into_inner() {
449                        ranges.push(self.build_weekday_range(pair)?)
450                    }
451                }
452                Rule::holiday_sequence => {
453                    for pair in pair.into_inner() {
454                        ranges.push(self.build_holiday(pair)?)
455                    }
456                }
457                unexpected => {
458                    return Err(Error::GrammarUnexpectedToken {
459                        rule: Rule::weekday_selector,
460                        unexpected,
461                    });
462                }
463            }
464        }
465
466        Ok(ranges)
467    }
468
469    fn build_weekday_range(&mut self, pair: Pair<Rule>) -> Result<ds::WeekDayRange> {
470        debug_assert_eq!(pair.as_rule(), Rule::weekday_range);
471        let mut pairs = pair.into_inner();
472        let start = self.build_wday(pairs.next().ok_or(err_empty(Rule::weekday_range))?)?;
473
474        let end = pairs
475            .next_if_rule(Rule::wday)
476            .map(|p| self.build_wday(p))
477            .transpose()?
478            .unwrap_or(start);
479
480        let mut nth_from_start = [false; 5];
481        let mut nth_from_end = [false; 5];
482
483        while let Some(pair_nth_entry) = pairs.next_if_rule(Rule::nth_entry) {
484            let (sign, indices) = self.build_nth_entry(pair_nth_entry)?;
485
486            let nth_array = match sign {
487                Sign::Neg => &mut nth_from_end,
488                Sign::Pos => &mut nth_from_start,
489            };
490
491            for i in indices {
492                nth_array[usize::from(i - 1)] = true;
493            }
494        }
495
496        if !nth_from_start.contains(&true) && !nth_from_end.contains(&true) {
497            nth_from_start = [true; 5];
498            nth_from_end = [true; 5];
499        }
500
501        let offset = {
502            if let Some(pair) = pairs.next() {
503                self.build_day_offset(pair)?
504            } else {
505                0
506            }
507        };
508
509        Ok(ds::WeekDayRange::Fixed {
510            range: start..=end,
511            offset,
512            nth_from_start,
513            nth_from_end,
514        })
515    }
516
517    fn build_holiday(&mut self, pair: Pair<Rule>) -> Result<ds::WeekDayRange> {
518        debug_assert_eq!(pair.as_rule(), Rule::holiday);
519        let mut pairs = pair.into_inner();
520
521        let kind = match pairs.next().ok_or(err_empty(Rule::holiday))?.as_rule() {
522            Rule::public_holiday => ds::HolidayKind::Public,
523            Rule::school_holiday => ds::HolidayKind::School,
524            unexpected => {
525                return Err(Error::GrammarUnexpectedToken { rule: Rule::holiday, unexpected });
526            }
527        };
528
529        let offset = pairs
530            .next()
531            .map(|p| self.build_day_offset(p))
532            .unwrap_or(Ok(0))?;
533
534        Ok(ds::WeekDayRange::Holiday { kind, offset })
535    }
536
537    fn build_nth_entry(&mut self, pair: Pair<Rule>) -> Result<(Sign, RangeInclusive<u8>)> {
538        debug_assert_eq!(pair.as_rule(), Rule::nth_entry);
539        let mut pairs = pair.into_inner();
540
541        let sign = {
542            if pairs.next_if_rule(Rule::nth_minus).is_some() {
543                Sign::Neg
544            } else {
545                Sign::Pos
546            }
547        };
548
549        let start = self.build_nth(pairs.next().ok_or(Error::GrammarLogic {
550            rule: Rule::nth_entry,
551            invariant: "a sign is always followed by a number",
552        })?)?;
553
554        let end = pairs
555            .next()
556            .map(|p| self.build_nth(p))
557            .transpose()?
558            .unwrap_or(start);
559
560        Ok((sign, start..=end))
561    }
562
563    fn build_nth(&mut self, pair: Pair<Rule>) -> Result<u8> {
564        debug_assert_eq!(pair.as_rule(), Rule::nth);
565
566        pair.as_str().parse().map_err(|_| Error::GrammarLogic {
567            rule: Rule::nth,
568            invariant: "must be valid number for 1 to 5",
569        })
570    }
571
572    fn build_day_offset(&mut self, pair: Pair<Rule>) -> Result<i16> {
573        debug_assert_eq!(pair.as_rule(), Rule::day_offset);
574        let mut pairs = pair.into_inner();
575        let sign = self.build_plus_or_minus(pairs.next().ok_or(err_empty(Rule::day_offset))?)?;
576
577        let val_abs = self.build_positive_number(pairs.next().ok_or(Error::GrammarLogic {
578            rule: Rule::day_offset,
579            invariant: "a sign is always followed by a number",
580        })?)?;
581
582        Ok(match sign {
583            Sign::Pos => val_abs,
584            Sign::Neg => -val_abs,
585        })
586    }
587
588    // --
589    // -- Week selector
590    // --
591
592    fn build_week_selector(&mut self, pair: Pair<Rule>) -> Result<Vec<ds::WeekRange>> {
593        debug_assert_eq!(pair.as_rule(), Rule::week_selector);
594        pair.into_inner().map(|p| self.build_week(p)).collect()
595    }
596
597    fn build_week(&mut self, pair: Pair<Rule>) -> Result<ds::WeekRange> {
598        debug_assert_eq!(pair.as_rule(), Rule::week);
599        let mut rules = pair.into_inner();
600        let start = self.build_weeknum(rules.next().ok_or(err_empty(Rule::week))?)?;
601
602        let end = rules
603            .next()
604            .map(|p| self.build_weeknum(p))
605            .transpose()?
606            .unwrap_or(start);
607
608        let step = rules
609            .next()
610            .map(|p| self.build_positive_number(p))
611            .transpose()?
612            .unwrap_or(1);
613
614        let step = step
615            .try_into()
616            .map_err(|_| Error::Overflow { value: step, expected_bounds: 0i16..=255i16 })?;
617
618        ds::WeekRange::new(start..=end, step).ok_or(Error::InvertedWeekRange { start, end, step })
619    }
620
621    // --
622    // -- Month selector
623    // --
624
625    fn build_monthday_selector(&mut self, pair: Pair<Rule>) -> Result<Vec<ds::MonthdayRange>> {
626        debug_assert_eq!(pair.as_rule(), Rule::monthday_selector);
627
628        pair.into_inner()
629            .map(|p| self.build_monthday_range(p))
630            .collect()
631    }
632
633    fn build_monthday_range(&mut self, pair: Pair<Rule>) -> Result<ds::MonthdayRange> {
634        debug_assert_eq!(pair.as_rule(), Rule::monthday_range);
635        let mut pairs = pair.into_inner();
636        let mut first_pair = pairs.next().ok_or(err_empty(Rule::monthday_range))?;
637
638        let year = {
639            if first_pair.as_rule() == Rule::year {
640                let year = self.build_year(first_pair)?;
641
642                first_pair = pairs.next().ok_or(Error::GrammarLogic {
643                    rule: Rule::monthday_range,
644                    invariant: "cannot contain just a year",
645                })?;
646
647                Some(year)
648            } else {
649                None
650            }
651        };
652
653        match first_pair.as_rule() {
654            Rule::month => {
655                let start = self.build_month(first_pair)?;
656
657                let end = (pairs.next())
658                    .map(|p| self.build_month(p))
659                    .transpose()?
660                    .unwrap_or(start);
661
662                Ok(ds::MonthdayRange::Month { year, range: start..=end })
663            }
664            Rule::date_from => {
665                let start = self.build_date_from(first_pair)?;
666
667                let start_offset = pairs
668                    .next_if_rule(Rule::date_offset)
669                    .map(|p| self.build_date_offset(p))
670                    .transpose()?
671                    .unwrap_or_default();
672
673                let Some(pair_end) = pairs.next() else {
674                    return Ok(ds::MonthdayRange::Date {
675                        start: (start, start_offset),
676                        end: (start, start_offset),
677                    });
678                };
679
680                let end = match pair_end.as_rule() {
681                    Rule::date_to => self.build_date_to(pair_end, start)?,
682                    Rule::monthday_range_plus => {
683                        if start.year().is_some() {
684                            ds::Date::ymd(31, ds::Month::December, Year(9999))
685                        } else {
686                            ds::Date::md(31, ds::Month::December)
687                        }
688                    }
689                    unexpected => {
690                        return Err(Error::GrammarUnexpectedToken {
691                            rule: Rule::monthday_range,
692                            unexpected,
693                        });
694                    }
695                };
696
697                let end_offset = pairs
698                    .next()
699                    .map(|p| self.build_date_offset(p))
700                    .unwrap_or_else(|| Ok(Default::default()))?;
701
702                Ok(ds::MonthdayRange::Date {
703                    start: (start, start_offset),
704                    end: (end, end_offset),
705                })
706            }
707            unexpected => {
708                Err(Error::GrammarUnexpectedToken { rule: Rule::monthday_range, unexpected })
709            }
710        }
711    }
712
713    fn build_date_offset(&mut self, pair: Pair<Rule>) -> Result<ds::DateOffset> {
714        debug_assert_eq!(pair.as_rule(), Rule::date_offset);
715        let mut pairs = pair.into_inner();
716
717        let wday_offset = {
718            if let Some(pair_sign) = pairs.next_if_rule(Rule::plus_or_minus) {
719                let sign = self.build_plus_or_minus(pair_sign)?;
720
721                let wday = self.build_wday(pairs.next().ok_or(Error::GrammarLogic {
722                    rule: Rule::date_offset,
723                    invariant: "a sign is always followed by a wday",
724                })?)?;
725
726                match sign {
727                    Sign::Pos => ds::WeekDayOffset::Next(wday),
728                    Sign::Neg => ds::WeekDayOffset::Prev(wday),
729                }
730            } else {
731                ds::WeekDayOffset::None
732            }
733        };
734
735        let day_offset = pairs
736            .next()
737            .map(|p| self.build_day_offset(p))
738            .unwrap_or(Ok(0))?;
739        Ok(ds::DateOffset { wday_offset, day_offset })
740    }
741
742    fn build_date_from(&mut self, pair: Pair<Rule>) -> Result<ds::Date> {
743        debug_assert_eq!(pair.as_rule(), Rule::date_from);
744        let mut pairs = pair.into_inner();
745        let year = pairs
746            .next_if_rule(Rule::year)
747            .map(|p| self.build_year(p))
748            .transpose()?;
749
750        let pair_month_or_variable = pairs.next().ok_or(Error::GrammarLogic {
751            rule: Rule::date_from,
752            invariant: "must have a month component",
753        })?;
754
755        if pair_month_or_variable.as_rule() == Rule::variable_date {
756            if !is_lowercase(pair_month_or_variable.as_str()) {
757                self.warn(Warning::ShouldBeLowercase(pair_month_or_variable));
758            }
759
760            return Ok(ds::Date::Easter { year });
761        }
762
763        let month = self.build_month(pair_month_or_variable)?;
764
765        let pair_day = pairs.next().ok_or(Error::GrammarLogic {
766            rule: Rule::date_from,
767            invariant: "must have a daynum or wday component",
768        })?;
769
770        match pair_day.as_rule() {
771            Rule::daynum => Ok(ds::Date::Fixed { year, month, day: self.build_daynum(pair_day)? }),
772            Rule::wday => {
773                let weekday = self.build_wday(pair_day)?;
774
775                let nth_sign = {
776                    if pairs.next_if_rule(Rule::nth_minus).is_some() {
777                        -1
778                    } else {
779                        1
780                    }
781                };
782
783                let nth: i8 = (pairs.next())
784                    .map(|p| self.build_nth(p))
785                    .transpose()?
786                    .ok_or(Error::GrammarLogic {
787                        rule: Rule::date_from,
788                        invariant: "a sign is always followed by a number",
789                    })?
790                    .try_into()
791                    .map_err(|_| Error::GrammarLogic {
792                        rule: Rule::date_from,
793                        invariant: "must be a valid number between 1 and 5",
794                    })?;
795
796                Ok(ds::Date::Weekday { year, month, wday: weekday, nth: nth_sign * nth })
797            }
798            unexpected => Err(Error::GrammarUnexpectedToken { rule: Rule::date_from, unexpected }),
799        }
800    }
801
802    fn build_date_to(&mut self, pair: Pair<Rule>, from: ds::Date) -> Result<ds::Date> {
803        debug_assert_eq!(pair.as_rule(), Rule::date_to);
804        let pair = pair.into_inner().next().ok_or(err_empty(Rule::date_to))?;
805
806        match pair.as_rule() {
807            Rule::date_from => self.build_date_from(pair),
808            Rule::daynum => {
809                let daynum = self.build_daynum(pair)?;
810
811                match from {
812                    ds::Date::Easter { .. } => {
813                        // NOTE: this is actually not a specified constraint, but allowing this could
814                        // be super ambiguous anyway as the resulting end month could vary depending on
815                        // current year's easter date.
816                        Err(Error::Unsupported("Easter followed by a day number"))
817                    }
818                    ds::Date::Weekday { year, month, .. } => {
819                        Ok(ds::Date::Fixed { year, month, day: daynum })
820                    }
821                    ds::Date::Fixed { mut year, mut month, day } => {
822                        if day > daynum {
823                            month = month.next();
824
825                            if month == ds::Month::January
826                                && let Some(x) = year.as_mut()
827                            {
828                                **x += 1
829                            }
830                        }
831
832                        Ok(ds::Date::Fixed { year, month, day: daynum })
833                    }
834                }
835            }
836            unexpected => Err(Error::GrammarUnexpectedToken { rule: Rule::date_to, unexpected }),
837        }
838    }
839
840    // --
841    // -- Year selector
842    // --
843
844    fn build_year_selector(&mut self, pair: Pair<Rule>) -> Result<Vec<ds::YearRange>> {
845        debug_assert_eq!(pair.as_rule(), Rule::year_selector);
846        pair.into_inner()
847            .map(|p| self.build_year_range(p))
848            .collect()
849    }
850
851    fn build_year_range(&mut self, pair: Pair<Rule>) -> Result<ds::YearRange> {
852        debug_assert_eq!(pair.as_rule(), Rule::year_range);
853        let mut rules = pair.into_inner();
854        let start = self.build_year(rules.next().ok_or(err_empty(Rule::year_range))?)?;
855
856        let end = rules
857            .next()
858            .map(|pair| match pair.as_rule() {
859                Rule::year => self.build_year(pair),
860                Rule::year_range_plus => Ok(Year(9999)),
861                unexpected => {
862                    Err(Error::GrammarUnexpectedToken { rule: Rule::year_range, unexpected })
863                }
864            })
865            .transpose()?
866            .unwrap_or(start);
867
868        let step = rules
869            .next()
870            .map(|p| self.build_positive_number(p))
871            .transpose()?
872            .unwrap_or(1)
873            .unsigned_abs();
874
875        ds::YearRange::new(start..=end, step).ok_or(Error::InvertedYearRange { start, end, step })
876    }
877
878    // --
879    // -- Basic elements
880    // --
881
882    fn build_plus_or_minus(&mut self, pair: Pair<Rule>) -> Result<Sign> {
883        debug_assert_eq!(pair.as_rule(), Rule::plus_or_minus);
884
885        let pair = pair
886            .into_inner()
887            .next()
888            .ok_or(err_empty(Rule::plus_or_minus))?;
889
890        match pair.as_rule() {
891            Rule::plus => Ok(Sign::Pos),
892            Rule::minus => Ok(Sign::Neg),
893            unexpected => {
894                Err(Error::GrammarUnexpectedToken { rule: Rule::plus_or_minus, unexpected })
895            }
896        }
897    }
898
899    fn build_minute(&mut self, pair: Pair<Rule>) -> Result<Duration> {
900        debug_assert_eq!(pair.as_rule(), Rule::minute);
901
902        pair.as_str()
903            .parse()
904            .map_err(|_| Error::GrammarLogic {
905                rule: Rule::minute,
906                invariant: "must be a valid number",
907            })
908            .map(Duration::minutes)
909    }
910
911    fn build_hour_minutes(&mut self, pair: Pair<Rule>) -> Result<ExtendedTime> {
912        debug_assert_eq!(pair.as_rule(), Rule::hour_minutes);
913        let mut pairs = pair.into_inner();
914
915        let Some(hour_rule) = pairs.next() else {
916            return Ok(ExtendedTime::MIDNIGHT_24);
917        };
918
919        let hour = hour_rule
920            .as_str()
921            .parse()
922            .map_err(|_| Error::GrammarLogic {
923                rule: Rule::hour,
924                invariant: "must be a valid number",
925            })?;
926
927        let minutes = pairs
928            .next()
929            .ok_or(Error::GrammarLogic {
930                rule: Rule::hour_minutes,
931                invariant: "hour must be followed by minutes",
932            })?
933            .as_str()
934            .parse()
935            .map_err(|_| Error::GrammarLogic {
936                rule: Rule::minute,
937                invariant: "must be a valid number",
938            })?;
939
940        ExtendedTime::new(hour, minutes).ok_or(Error::InvalidExtendedTime { hour, minutes })
941    }
942
943    fn build_extended_hour_minutes(&mut self, pair: Pair<Rule>) -> Result<ExtendedTime> {
944        debug_assert_eq!(pair.as_rule(), Rule::extended_hour_minutes);
945        let mut pairs = pair.into_inner();
946
947        let hour = pairs
948            .next()
949            .ok_or(err_empty(Rule::extended_hour_minutes))?
950            .as_str()
951            .parse()
952            .map_err(|_| Error::GrammarLogic {
953                rule: Rule::extended_hour,
954                invariant: "must be a valid number",
955            })?;
956
957        let minutes = pairs
958            .next()
959            .ok_or(Error::GrammarLogic {
960                rule: Rule::extended_hour_minutes,
961                invariant: "hour must be followed by minutes",
962            })?
963            .as_str()
964            .parse()
965            .map_err(|_| Error::GrammarLogic {
966                rule: Rule::minute,
967                invariant: "must be a valid number",
968            })?;
969
970        ExtendedTime::new(hour, minutes).ok_or(Error::InvalidExtendedTime { hour, minutes })
971    }
972
973    fn build_hour_minutes_as_duration(&mut self, pair: Pair<Rule>) -> Result<Duration> {
974        debug_assert_eq!(pair.as_rule(), Rule::hour_minutes);
975        let mut pairs = pair.into_inner();
976
977        let hour = pairs
978            .next()
979            .ok_or(err_empty(Rule::hour_minutes))?
980            .as_str()
981            .parse()
982            .map_err(|_| Error::GrammarLogic {
983                rule: Rule::hour,
984                invariant: "must be a valid number",
985            })?;
986
987        let minutes = pairs
988            .next()
989            .ok_or(Error::GrammarLogic {
990                rule: Rule::hour_minutes,
991                invariant: "hour must be followed by minutes",
992            })?
993            .as_str()
994            .parse()
995            .map_err(|_| Error::GrammarLogic {
996                rule: Rule::minute,
997                invariant: "must be a valid number",
998            })?;
999
1000        Ok(Duration::hours(hour) + Duration::minutes(minutes))
1001    }
1002
1003    fn build_wday(&mut self, pair: Pair<Rule>) -> Result<ds::Weekday> {
1004        debug_assert_eq!(pair.as_rule(), Rule::wday);
1005
1006        if !is_capitalized(pair.as_str()) {
1007            self.warn(Warning::ShouldBeCapitalized(pair.clone()));
1008        }
1009
1010        let pair = pair.into_inner().next().ok_or(err_empty(Rule::wday))?;
1011
1012        match pair.as_rule() {
1013            Rule::sunday => Ok(ds::Weekday::Sun),
1014            Rule::monday => Ok(ds::Weekday::Mon),
1015            Rule::tuesday => Ok(ds::Weekday::Tue),
1016            Rule::wednesday => Ok(ds::Weekday::Wed),
1017            Rule::thursday => Ok(ds::Weekday::Thu),
1018            Rule::friday => Ok(ds::Weekday::Fri),
1019            Rule::saturday => Ok(ds::Weekday::Sat),
1020            unexpected => Err(Error::GrammarUnexpectedToken { rule: Rule::wday, unexpected }),
1021        }
1022    }
1023
1024    fn build_daynum(&mut self, pair: Pair<Rule>) -> Result<u8> {
1025        debug_assert_eq!(pair.as_rule(), Rule::daynum);
1026
1027        let daynum = pair.as_str().parse().map_err(|_| Error::GrammarLogic {
1028            rule: Rule::daynum,
1029            invariant: "must be a valid number",
1030        })?;
1031
1032        if daynum < 1 {
1033            return Err(Error::GrammarLogic {
1034                rule: Rule::daynum,
1035                invariant: "cannot be less than 1",
1036            });
1037        }
1038
1039        if daynum > 31 {
1040            return Err(Error::GrammarLogic {
1041                rule: Rule::daynum,
1042                invariant: "cannot be greater than 31",
1043            });
1044        }
1045
1046        Ok(daynum)
1047    }
1048
1049    fn build_weeknum(&mut self, pair: Pair<Rule>) -> Result<WeekNum> {
1050        debug_assert_eq!(pair.as_rule(), Rule::weeknum);
1051
1052        pair.as_str()
1053            .parse()
1054            .map_err(|_| Error::GrammarLogic {
1055                rule: Rule::weeknum,
1056                invariant: "must be a valid number",
1057            })
1058            .map(WeekNum)
1059    }
1060
1061    fn build_month(&mut self, pair: Pair<Rule>) -> Result<ds::Month> {
1062        debug_assert_eq!(pair.as_rule(), Rule::month);
1063
1064        if !is_capitalized(pair.as_str()) {
1065            self.warn(Warning::ShouldBeCapitalized(pair.clone()));
1066        }
1067
1068        let pair = pair.into_inner().next().ok_or(err_empty(Rule::month))?;
1069
1070        match pair.as_rule() {
1071            Rule::january => Ok(ds::Month::January),
1072            Rule::february => Ok(ds::Month::February),
1073            Rule::march => Ok(ds::Month::March),
1074            Rule::april => Ok(ds::Month::April),
1075            Rule::may => Ok(ds::Month::May),
1076            Rule::june => Ok(ds::Month::June),
1077            Rule::july => Ok(ds::Month::July),
1078            Rule::august => Ok(ds::Month::August),
1079            Rule::september => Ok(ds::Month::September),
1080            Rule::october => Ok(ds::Month::October),
1081            Rule::november => Ok(ds::Month::November),
1082            Rule::december => Ok(ds::Month::December),
1083            unexpected => Err(Error::GrammarUnexpectedToken { rule: Rule::month, unexpected }),
1084        }
1085    }
1086
1087    fn build_year(&mut self, pair: Pair<Rule>) -> Result<Year> {
1088        debug_assert_eq!(pair.as_rule(), Rule::year);
1089
1090        pair.as_str()
1091            .parse()
1092            .map_err(|_| Error::GrammarLogic {
1093                rule: Rule::year,
1094                invariant: "must be a valid number",
1095            })
1096            .map(Year)
1097    }
1098
1099    fn build_positive_number(&mut self, pair: Pair<Rule>) -> Result<i16> {
1100        debug_assert_eq!(pair.as_rule(), Rule::positive_number);
1101
1102        let val = pair.as_str().parse().map_err(|_| Error::GrammarLogic {
1103            rule: Rule::positive_number,
1104            invariant: "must be a valid 16 bits number",
1105        })?;
1106
1107        debug_assert!(val >= 0);
1108        Ok(val)
1109    }
1110
1111    fn build_comment(&mut self, pair: Pair<Rule>) -> Result<String> {
1112        debug_assert_eq!(pair.as_rule(), Rule::comment);
1113
1114        pair.into_inner()
1115            .next()
1116            .ok_or(err_empty(Rule::comment))
1117            .map(|p| self.build_comment_inner(p))
1118    }
1119
1120    fn build_comment_inner(&mut self, pair: Pair<Rule>) -> String {
1121        debug_assert_eq!(pair.as_rule(), Rule::comment_inner);
1122        pair.as_str().to_string()
1123    }
1124}