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