Skip to main content

opening_hours_syntax/
parser.rs

1use alloc::string::{String, ToString};
2use alloc::sync::Arc;
3use alloc::vec::Vec;
4use core::cmp::Ord;
5use core::convert::TryInto;
6use core::fmt::Debug;
7use core::hash::Hash;
8use core::ops::RangeInclusive;
9
10use chrono::Duration;
11
12use pest::iterators::Pair;
13use pest::Parser;
14
15use crate::error::{Error, Result};
16use crate::extended_time::ExtendedTime;
17use crate::rules as rl;
18use crate::rules::day::{self as ds, WeekNum, Year};
19use crate::rules::time as ts;
20
21#[cfg(feature = "log")]
22use core::sync::atomic::{AtomicBool, Ordering};
23
24/// Keep track of whether a feature warning was emitted for easter support. This ensures that a
25/// warning is not spammed for a single process session.
26#[cfg(feature = "log")]
27static WARNING_EASTER_EMITTED: AtomicBool = AtomicBool::new(false);
28
29#[derive(Parser)]
30#[grammar = "grammar.pest"]
31struct OHParser;
32
33/// Just used while collecting parsed expression
34enum Sign {
35    Neg,
36    Pos,
37}
38
39pub fn parse(data: &str) -> Result<rl::OpeningHoursExpression> {
40    let opening_hours_pair = OHParser::parse(Rule::input_opening_hours, data)
41        .map_err(Error::from)?
42        .next()
43        .expect("grammar error: no opening_hours found");
44
45    let rules = build_opening_hours(opening_hours_pair)?;
46    Ok(rl::OpeningHoursExpression { rules })
47}
48
49// ---
50// --- Time domain
51// ---
52
53fn unexpected_token<T>(token: Rule, parent: Rule) -> T {
54    unreachable!("Grammar error: found `{token:?}` inside of `{parent:?}`")
55}
56
57fn build_opening_hours(pair: Pair<Rule>) -> Result<Vec<rl::RuleSequence>> {
58    assert_eq!(pair.as_rule(), Rule::opening_hours);
59    let mut pairs = pair.into_inner();
60    let mut rules = Vec::new();
61
62    while let Some(pair) = pairs.next() {
63        rules.push(match pair.as_rule() {
64            Rule::rule_sequence => build_rule_sequence(pair, rl::RuleOperator::Normal),
65            Rule::any_rule_separator => build_rule_sequence(
66                pairs.next().expect("separator not followed by any rule"),
67                build_any_rule_separator(pair),
68            ),
69            other => unexpected_token(other, Rule::opening_hours),
70        }?)
71    }
72
73    Ok(rules)
74}
75
76fn build_rule_sequence(pair: Pair<Rule>, operator: rl::RuleOperator) -> Result<rl::RuleSequence> {
77    assert_eq!(pair.as_rule(), Rule::rule_sequence);
78    let mut pairs = pair.into_inner();
79
80    let (day_selector, time_selector, extra_comment) =
81        build_selector_sequence(pairs.next().expect("grammar error: empty rule sequence"))?;
82
83    let (kind, comment) = pairs
84        .next()
85        .map(build_rules_modifier)
86        .unwrap_or((rl::RuleKind::Open, None));
87
88    let comments = comment
89        .into_iter()
90        .chain(extra_comment)
91        .map(|s| Arc::from(s.into_boxed_str()))
92        .collect::<Vec<_>>()
93        .into();
94
95    Ok(rl::RuleSequence {
96        day_selector,
97        time_selector,
98        kind,
99        operator,
100        comments,
101    })
102}
103
104fn build_any_rule_separator(pair: Pair<Rule>) -> rl::RuleOperator {
105    assert_eq!(pair.as_rule(), Rule::any_rule_separator);
106
107    match pair
108        .into_inner()
109        .next()
110        .expect("empty rule separator")
111        .as_rule()
112    {
113        Rule::normal_rule_separator => rl::RuleOperator::Normal,
114        Rule::additional_rule_separator => rl::RuleOperator::Additional,
115        Rule::fallback_rule_separator => rl::RuleOperator::Fallback,
116        other => unexpected_token(other, Rule::any_rule_separator),
117    }
118}
119
120// ---
121// --- Rule modifier
122// ---
123
124fn build_rules_modifier(pair: Pair<Rule>) -> (rl::RuleKind, Option<String>) {
125    assert_eq!(pair.as_rule(), Rule::rules_modifier);
126    let mut pairs = pair.into_inner();
127
128    let kind = {
129        if pairs.peek().expect("empty rules_modifier").as_rule() == Rule::rules_modifier_enum {
130            build_rules_modifier_enum(pairs.next().unwrap())
131        } else {
132            rl::RuleKind::Open
133        }
134    };
135
136    let comment = pairs.next().map(build_comment);
137    (kind, comment)
138}
139
140fn build_rules_modifier_enum(pair: Pair<Rule>) -> rl::RuleKind {
141    assert_eq!(pair.as_rule(), Rule::rules_modifier_enum);
142
143    let pair = pair
144        .into_inner()
145        .next()
146        .expect("grammar error: empty rules modifier enum");
147
148    match pair.as_rule() {
149        Rule::rules_modifier_enum_closed => rl::RuleKind::Closed,
150        Rule::rules_modifier_enum_open => rl::RuleKind::Open,
151        Rule::rules_modifier_enum_unknown => rl::RuleKind::Unknown,
152        other => unexpected_token(other, Rule::rules_modifier_enum),
153    }
154}
155
156// ---
157// --- Selectors
158// ---
159
160fn build_selector_sequence(
161    pair: Pair<Rule>,
162) -> Result<(ds::DaySelector, ts::TimeSelector, Option<String>)> {
163    assert_eq!(pair.as_rule(), Rule::selector_sequence);
164    let mut pairs = pair.into_inner();
165
166    if pairs.peek().map(|x| x.as_rule()).expect("empty selector") == Rule::always_open {
167        return Ok(Default::default());
168    }
169
170    let (year, monthday, week, comment) = {
171        if pairs.peek().map(|x| x.as_rule()).unwrap() == Rule::wide_range_selectors {
172            build_wide_range_selectors(pairs.next().unwrap())?
173        } else {
174            (Vec::new(), Vec::new(), Vec::new(), None)
175        }
176    };
177
178    let (weekday, time) = {
179        if let Some(pair) = pairs.next() {
180            build_small_range_selectors(pair)?
181        } else {
182            (Vec::new(), Vec::new())
183        }
184    };
185
186    Ok((
187        ds::DaySelector { year, monthday, week, weekday },
188        ts::TimeSelector::new(time),
189        comment,
190    ))
191}
192
193#[allow(clippy::type_complexity)]
194fn build_wide_range_selectors(
195    pair: Pair<Rule>,
196) -> Result<(
197    Vec<ds::YearRange>,
198    Vec<ds::MonthdayRange>,
199    Vec<ds::WeekRange>,
200    Option<String>,
201)> {
202    assert_eq!(pair.as_rule(), Rule::wide_range_selectors);
203
204    let mut year_selector = Vec::new();
205    let mut monthday_selector = Vec::new();
206    let mut week_selector = Vec::new();
207    let mut comment = None;
208
209    for pair in pair.into_inner() {
210        match pair.as_rule() {
211            Rule::year_selector => year_selector = build_year_selector(pair)?,
212            Rule::monthday_selector => monthday_selector = build_monthday_selector(pair)?,
213            Rule::week_selector => week_selector = build_week_selector(pair)?,
214            Rule::comment => comment = Some(build_comment(pair)),
215            other => unexpected_token(other, Rule::wide_range_selectors),
216        }
217    }
218
219    Ok((year_selector, monthday_selector, week_selector, comment))
220}
221
222fn build_small_range_selectors(
223    pair: Pair<Rule>,
224) -> Result<(Vec<ds::WeekDayRange>, Vec<ts::TimeSpan>)> {
225    assert_eq!(pair.as_rule(), Rule::small_range_selectors);
226
227    let mut weekday_selector = Vec::new();
228    let mut time_selector = Vec::new();
229
230    for pair in pair.into_inner() {
231        match pair.as_rule() {
232            Rule::weekday_selector => weekday_selector = build_weekday_selector(pair)?,
233            Rule::time_selector => time_selector = build_time_selector(pair)?,
234            other => unexpected_token(other, Rule::wide_range_selectors),
235        }
236    }
237
238    Ok((weekday_selector, time_selector))
239}
240
241// ---
242// --- Time selector
243// ---
244
245fn build_time_selector(pair: Pair<Rule>) -> Result<Vec<ts::TimeSpan>> {
246    assert_eq!(pair.as_rule(), Rule::time_selector);
247    pair.into_inner().map(build_timespan).collect()
248}
249
250fn build_timespan(pair: Pair<Rule>) -> Result<ts::TimeSpan> {
251    assert_eq!(pair.as_rule(), Rule::timespan);
252    let mut pairs = pair.into_inner();
253    let start = build_time(pairs.next().expect("empty timespan"))?;
254    let mut repeats = None;
255
256    let (mut open_end, end) = match pairs.next() {
257        None => {
258            return Err(Error::Unsupported("point in time"));
259        }
260        Some(pair) if pair.as_rule() == Rule::timespan_plus => {
261            // TODO: opening_hours.js handles this better: it will set the
262            //       state to unknown and add a warning comment.
263            (true, ts::Time::Fixed(ExtendedTime::MIDNIGHT_24))
264        }
265        Some(pair) => (false, build_extended_time(pair)?),
266    };
267
268    if let Some(pair_repetition) = pairs.next() {
269        match pair_repetition.as_rule() {
270            Rule::timespan_plus => open_end = true,
271            Rule::minute => repeats = Some(build_minute(pair_repetition)),
272            Rule::hour_minutes => repeats = Some(build_hour_minutes_as_duration(pair_repetition)),
273            other => return Err(unexpected_token(other, Rule::timespan)),
274        }
275    }
276
277    assert!(pairs.next().is_none());
278    Ok(ts::TimeSpan { range: start..end, repeats, open_end })
279}
280
281fn build_time(pair: Pair<Rule>) -> Result<ts::Time> {
282    assert_eq!(pair.as_rule(), Rule::time);
283    let inner = pair.into_inner().next().expect("empty time");
284
285    Ok(match inner.as_rule() {
286        Rule::hour_minutes => ts::Time::Fixed(build_hour_minutes(inner)?),
287        Rule::variable_time => ts::Time::Variable(build_variable_time(inner)?),
288        other => unexpected_token(other, Rule::time),
289    })
290}
291
292fn build_extended_time(pair: Pair<Rule>) -> Result<ts::Time> {
293    assert_eq!(pair.as_rule(), Rule::extended_time);
294    let inner = pair.into_inner().next().expect("empty extended time");
295
296    Ok(match inner.as_rule() {
297        Rule::extended_hour_minutes => ts::Time::Fixed(build_extended_hour_minutes(inner)?),
298        Rule::variable_time => ts::Time::Variable(build_variable_time(inner)?),
299        other => unexpected_token(other, Rule::extended_time),
300    })
301}
302
303fn build_variable_time(pair: Pair<Rule>) -> Result<ts::VariableTime> {
304    assert_eq!(pair.as_rule(), Rule::variable_time);
305    let mut pairs = pair.into_inner();
306
307    let event = build_event(pairs.next().expect("empty variable time"));
308
309    let offset = {
310        if pairs.peek().is_some() {
311            let sign = build_plus_or_minus(pairs.next().unwrap());
312
313            let mins: i16 = build_hour_minutes(pairs.next().expect("missing hour minutes"))?
314                .mins_from_midnight()
315                .try_into()
316                .expect("offset overflow");
317
318            match sign {
319                PlusOrMinus::Plus => mins,
320                PlusOrMinus::Minus => -mins,
321            }
322        } else {
323            0
324        }
325    };
326
327    Ok(ts::VariableTime { event, offset })
328}
329
330fn build_event(pair: Pair<Rule>) -> ts::TimeEvent {
331    assert_eq!(pair.as_rule(), Rule::event);
332
333    match pair.into_inner().next().expect("empty event").as_rule() {
334        Rule::dawn => ts::TimeEvent::Dawn,
335        Rule::sunrise => ts::TimeEvent::Sunrise,
336        Rule::sunset => ts::TimeEvent::Sunset,
337        Rule::dusk => ts::TimeEvent::Dusk,
338        other => unexpected_token(other, Rule::event),
339    }
340}
341
342// ---
343// --- WeekDay selector
344// ---
345
346fn build_weekday_selector(pair: Pair<Rule>) -> Result<Vec<ds::WeekDayRange>> {
347    assert_eq!(pair.as_rule(), Rule::weekday_selector);
348
349    pair.into_inner()
350        .flat_map(|pair| match pair.as_rule() {
351            Rule::weekday_sequence => pair.into_inner().map(build_weekday_range as fn(_) -> _),
352            Rule::holiday_sequence => pair.into_inner().map(build_holiday as _),
353            other => unexpected_token(other, Rule::weekday_sequence),
354        })
355        .collect()
356}
357
358fn build_weekday_range(pair: Pair<Rule>) -> Result<ds::WeekDayRange> {
359    assert_eq!(pair.as_rule(), Rule::weekday_range);
360    let mut pairs = pair.into_inner();
361
362    let start = build_wday(pairs.next().expect("empty weekday range"));
363
364    let end = {
365        if pairs.peek().map(|x| x.as_rule()) == Some(Rule::wday) {
366            build_wday(pairs.next().unwrap())
367        } else {
368            start
369        }
370    };
371
372    let mut nth_from_start = [false; 5];
373    let mut nth_from_end = [false; 5];
374
375    while pairs.peek().map(|x| x.as_rule()) == Some(Rule::nth_entry) {
376        let (sign, indices) = build_nth_entry(pairs.next().unwrap())?;
377
378        let nth_array = match sign {
379            Sign::Neg => &mut nth_from_end,
380            Sign::Pos => &mut nth_from_start,
381        };
382
383        for i in indices {
384            nth_array[usize::from(i - 1)] = true;
385        }
386    }
387
388    if !nth_from_start.contains(&true) && !nth_from_end.contains(&true) {
389        nth_from_start = [true; 5];
390        nth_from_end = [true; 5];
391    }
392
393    let offset = {
394        if let Some(pair) = pairs.next() {
395            build_day_offset(pair)?
396        } else {
397            0
398        }
399    };
400
401    Ok(ds::WeekDayRange::Fixed {
402        range: start..=end,
403        offset,
404        nth_from_start,
405        nth_from_end,
406    })
407}
408
409fn build_holiday(pair: Pair<Rule>) -> Result<ds::WeekDayRange> {
410    assert_eq!(pair.as_rule(), Rule::holiday);
411    let mut pairs = pair.into_inner();
412
413    let kind = match pairs.next().expect("empty holiday").as_rule() {
414        Rule::public_holiday => ds::HolidayKind::Public,
415        Rule::school_holiday => ds::HolidayKind::School,
416        other => unexpected_token(other, Rule::holiday),
417    };
418
419    let offset = pairs.next().map(build_day_offset).unwrap_or(Ok(0))?;
420    Ok(ds::WeekDayRange::Holiday { kind, offset })
421}
422
423fn build_nth_entry(pair: Pair<Rule>) -> Result<(Sign, RangeInclusive<u8>)> {
424    assert_eq!(pair.as_rule(), Rule::nth_entry);
425    let mut pairs = pair.into_inner();
426
427    let sign = {
428        if pairs.peek().map(|x| x.as_rule()) == Some(Rule::nth_minus) {
429            pairs.next();
430            Sign::Neg
431        } else {
432            Sign::Pos
433        }
434    };
435
436    let start = build_nth(pairs.next().expect("empty nth entry"));
437    let end = pairs.next().map(build_nth).unwrap_or(start);
438    Ok((sign, start..=end))
439}
440
441fn build_nth(pair: Pair<Rule>) -> u8 {
442    assert_eq!(pair.as_rule(), Rule::nth);
443    pair.as_str().parse().expect("invalid nth format")
444}
445
446fn build_day_offset(pair: Pair<Rule>) -> Result<i64> {
447    assert_eq!(pair.as_rule(), Rule::day_offset);
448    let mut pairs = pair.into_inner();
449
450    let sign = build_plus_or_minus(pairs.next().expect("empty day offset"));
451    let val_abs = build_positive_number(pairs.next().expect("missing value"))?;
452
453    let val_abs: i64 = val_abs.try_into().map_err(|_| Error::Overflow {
454        value: format!("{}", val_abs),
455        expected: "an integer in [-2**63, 2**63[".to_string(),
456    })?;
457
458    Ok(match sign {
459        PlusOrMinus::Plus => val_abs,
460        PlusOrMinus::Minus => -val_abs,
461    })
462}
463
464// ---
465// --- Week selector
466// ---
467
468fn build_week_selector(pair: Pair<Rule>) -> Result<Vec<ds::WeekRange>> {
469    assert_eq!(pair.as_rule(), Rule::week_selector);
470    pair.into_inner().map(build_week).collect()
471}
472
473fn build_week(pair: Pair<Rule>) -> Result<ds::WeekRange> {
474    assert_eq!(pair.as_rule(), Rule::week);
475    let mut rules = pair.into_inner();
476
477    let start = build_weeknum(rules.next().expect("empty weeknum range"));
478    let end = rules.next().map(build_weeknum);
479
480    let step = rules.next().map(build_positive_number).transpose()?;
481    let step = step.unwrap_or(1).try_into().map_err(|_| Error::Overflow {
482        value: format!("{}", step.unwrap()),
483        expected: "an integer in [0, 255]".to_string(),
484    })?;
485
486    Ok(ds::WeekRange {
487        range: WeekNum(start)..=WeekNum(end.unwrap_or(start)),
488        step,
489    })
490}
491
492// ---
493// --- Month selector
494// ---
495
496fn build_monthday_selector(pair: Pair<Rule>) -> Result<Vec<ds::MonthdayRange>> {
497    assert_eq!(pair.as_rule(), Rule::monthday_selector);
498    pair.into_inner().map(build_monthday_range).collect()
499}
500
501fn build_monthday_range(pair: Pair<Rule>) -> Result<ds::MonthdayRange> {
502    assert_eq!(pair.as_rule(), Rule::monthday_range);
503    let mut pairs = pair.into_inner();
504
505    let year = {
506        if pairs.peek().map(|x| x.as_rule()) == Some(Rule::year) {
507            Some(build_year(pairs.next().unwrap()))
508        } else {
509            None
510        }
511    };
512
513    match pairs.peek().expect("empty monthday range").as_rule() {
514        Rule::month => {
515            let start = build_month(pairs.next().unwrap());
516            let end = pairs.next().map(build_month).unwrap_or(start);
517
518            Ok(ds::MonthdayRange::Month { year, range: start..=end })
519        }
520        Rule::date_from => {
521            let start = build_date_from(pairs.next().unwrap());
522
523            let start_offset = {
524                if pairs.peek().map(|x| x.as_rule()) == Some(Rule::date_offset) {
525                    build_date_offset(pairs.next().unwrap())?
526                } else {
527                    ds::DateOffset::default()
528                }
529            };
530
531            let end = match pairs.peek().map(|x| x.as_rule()) {
532                Some(Rule::date_to) => build_date_to(pairs.next().unwrap(), start)?,
533                Some(Rule::monthday_range_plus) => {
534                    pairs.next();
535
536                    if start.has_year() {
537                        ds::Date::ymd(31, ds::Month::December, 9999)
538                    } else {
539                        ds::Date::md(31, ds::Month::December)
540                    }
541                }
542                None => {
543                    return Ok(ds::MonthdayRange::Date {
544                        start: (start, start_offset),
545                        end: (start, start_offset),
546                    });
547                }
548                Some(other) => unexpected_token(other, Rule::monthday_range),
549            };
550
551            let end_offset = pairs
552                .next()
553                .map(build_date_offset)
554                .unwrap_or_else(|| Ok(Default::default()))?;
555
556            Ok(ds::MonthdayRange::Date {
557                start: (start, start_offset),
558                end: (end, end_offset),
559            })
560        }
561        other => unexpected_token(other, Rule::monthday_range),
562    }
563}
564
565fn build_date_offset(pair: Pair<Rule>) -> Result<ds::DateOffset> {
566    assert_eq!(pair.as_rule(), Rule::date_offset);
567    let mut pairs = pair.into_inner();
568
569    let wday_offset = {
570        if pairs.peek().map(|x| x.as_rule()) == Some(Rule::plus_or_minus) {
571            let sign = build_plus_or_minus(pairs.next().unwrap());
572            let wday = build_wday(pairs.next().expect("missing wday after sign"));
573
574            match sign {
575                PlusOrMinus::Plus => ds::WeekDayOffset::Next(wday),
576                PlusOrMinus::Minus => ds::WeekDayOffset::Prev(wday),
577            }
578        } else {
579            ds::WeekDayOffset::None
580        }
581    };
582
583    let day_offset = pairs.next().map(build_day_offset).unwrap_or(Ok(0))?;
584
585    Ok(ds::DateOffset { wday_offset, day_offset })
586}
587
588fn build_date_from(pair: Pair<Rule>) -> ds::Date {
589    assert_eq!(pair.as_rule(), Rule::date_from);
590    let mut pairs = pair.into_inner();
591
592    let year = {
593        if pairs.peek().map(|x| x.as_rule()) == Some(Rule::year) {
594            Some(build_year(pairs.next().unwrap()))
595        } else {
596            None
597        }
598    };
599
600    match pairs.peek().expect("empty date (from)").as_rule() {
601        Rule::variable_date => {
602            #[cfg(feature = "log")]
603            if !WARNING_EASTER_EMITTED.swap(true, Ordering::Relaxed) {
604                log::warn!("Easter is not supported yet");
605            }
606
607            ds::Date::Easter { year }
608        }
609        Rule::month => ds::Date::Fixed {
610            year,
611            month: build_month(pairs.next().expect("missing month")),
612            day: build_daynum(pairs.next().expect("missing day")),
613        },
614        other => unexpected_token(other, Rule::date_from),
615    }
616}
617
618fn build_date_to(pair: Pair<Rule>, from: ds::Date) -> Result<ds::Date> {
619    assert_eq!(pair.as_rule(), Rule::date_to);
620    let pair = pair.into_inner().next().expect("empty date (to)");
621
622    Ok(match pair.as_rule() {
623        Rule::date_from => build_date_from(pair),
624        Rule::daynum => {
625            let daynum = build_daynum(pair);
626
627            match from {
628                ds::Date::Easter { .. } => {
629                    // NOTE: this is actually not a specified constraint, but it is quite confusing
630                    //       that this is allowed
631                    return Err(Error::Unsupported("Easter followed by a day number"));
632                }
633                ds::Date::Fixed { mut year, mut month, day } => {
634                    if day > daynum {
635                        month = month.next();
636
637                        if month == ds::Month::January {
638                            if let Some(x) = year.as_mut() {
639                                *x += 1
640                            }
641                        }
642                    }
643
644                    ds::Date::Fixed { year, month, day: daynum }
645                }
646            }
647        }
648        other => unexpected_token(other, Rule::date_to),
649    })
650}
651
652// ---
653// --- Year selector
654// ---
655
656fn build_year_selector(pair: Pair<Rule>) -> Result<Vec<ds::YearRange>> {
657    assert_eq!(pair.as_rule(), Rule::year_selector);
658    pair.into_inner().map(build_year_range).collect()
659}
660
661fn build_year_range(pair: Pair<Rule>) -> Result<ds::YearRange> {
662    assert_eq!(pair.as_rule(), Rule::year_range);
663    let mut rules = pair.into_inner();
664
665    let start = build_year(rules.next().expect("empty year range"));
666    let end = rules.next().map(|pair| match pair.as_rule() {
667        Rule::year => build_year(pair),
668        Rule::year_range_plus => 9999,
669        other => unexpected_token(other, Rule::year_range),
670    });
671
672    let step = rules.next().map(build_positive_number).transpose()?;
673    let step = step.unwrap_or(1).try_into().map_err(|_| Error::Overflow {
674        value: format!("{}", step.unwrap()),
675        expected: "an integer in [0, 2**16[".to_string(),
676    })?;
677
678    Ok(ds::YearRange {
679        range: Year(start)..=Year(end.unwrap_or(start)),
680        step,
681    })
682}
683
684// ---
685// --- Basic elements
686// ---
687
688fn build_plus_or_minus(pair: Pair<Rule>) -> PlusOrMinus {
689    assert_eq!(pair.as_rule(), Rule::plus_or_minus);
690    let pair = pair.into_inner().next().expect("empty plus or minus");
691
692    match pair.as_rule() {
693        Rule::plus => PlusOrMinus::Plus,
694        Rule::minus => PlusOrMinus::Minus,
695        other => unexpected_token(other, Rule::plus_or_minus),
696    }
697}
698
699fn build_minute(pair: Pair<Rule>) -> Duration {
700    assert_eq!(pair.as_rule(), Rule::minute);
701    let minutes = pair.as_str().parse().expect("invalid minute");
702    Duration::minutes(minutes)
703}
704
705fn build_hour_minutes(pair: Pair<Rule>) -> Result<ExtendedTime> {
706    assert_eq!(pair.as_rule(), Rule::hour_minutes);
707    let mut pairs = pair.into_inner();
708
709    let Some(hour_rule) = pairs.next() else {
710        return Ok(ExtendedTime::MIDNIGHT_24);
711    };
712
713    let hour = hour_rule.as_str().parse().expect("invalid hour");
714
715    let minutes = pairs
716        .next()
717        .expect("missing minutes")
718        .as_str()
719        .parse()
720        .expect("invalid minutes");
721
722    ExtendedTime::new(hour, minutes).ok_or(Error::InvalidExtendTime { hour, minutes })
723}
724
725fn build_extended_hour_minutes(pair: Pair<Rule>) -> Result<ExtendedTime> {
726    assert_eq!(pair.as_rule(), Rule::extended_hour_minutes);
727    let mut pairs = pair.into_inner();
728
729    let hour = pairs
730        .next()
731        .expect("missing hour")
732        .as_str()
733        .parse()
734        .expect("invalid hour");
735
736    let minutes = pairs
737        .next()
738        .expect("missing minutes")
739        .as_str()
740        .parse()
741        .expect("invalid minutes");
742
743    ExtendedTime::new(hour, minutes).ok_or(Error::InvalidExtendTime { hour, minutes })
744}
745
746fn build_hour_minutes_as_duration(pair: Pair<Rule>) -> Duration {
747    assert_eq!(pair.as_rule(), Rule::hour_minutes);
748    let mut pairs = pair.into_inner();
749
750    let hour = pairs
751        .next()
752        .expect("missing hour")
753        .as_str()
754        .parse()
755        .expect("invalid hour");
756
757    let minutes = pairs
758        .next()
759        .expect("missing minutes")
760        .as_str()
761        .parse()
762        .expect("invalid minutes");
763
764    Duration::hours(hour) + Duration::minutes(minutes)
765}
766
767fn build_wday(pair: Pair<Rule>) -> ds::Weekday {
768    assert_eq!(pair.as_rule(), Rule::wday);
769    let pair = pair.into_inner().next().expect("empty week day");
770
771    match pair.as_rule() {
772        Rule::sunday => ds::Weekday::Sun,
773        Rule::monday => ds::Weekday::Mon,
774        Rule::tuesday => ds::Weekday::Tue,
775        Rule::wednesday => ds::Weekday::Wed,
776        Rule::thursday => ds::Weekday::Thu,
777        Rule::friday => ds::Weekday::Fri,
778        Rule::saturday => ds::Weekday::Sat,
779        other => unexpected_token(other, Rule::wday),
780    }
781}
782
783fn build_daynum(pair: Pair<Rule>) -> u8 {
784    assert_eq!(pair.as_rule(), Rule::daynum);
785    let daynum = pair.as_str().parse().expect("invalid month format");
786
787    if daynum == 0 {
788        #[cfg(feature = "log")]
789        log::warn!("Found day number 0 in opening hours: specify the 1st or 31st instead.");
790        return 1;
791    }
792
793    if daynum > 31 {
794        #[cfg(feature = "log")]
795        log::warn!("Found day number {daynum} in opening hours");
796        return 31;
797    }
798
799    daynum
800}
801
802fn build_weeknum(pair: Pair<Rule>) -> u8 {
803    assert_eq!(pair.as_rule(), Rule::weeknum);
804    pair.as_str().parse().expect("invalid weeknum format")
805}
806
807fn build_month(pair: Pair<Rule>) -> ds::Month {
808    assert_eq!(pair.as_rule(), Rule::month);
809    let pair = pair.into_inner().next().expect("empty month");
810
811    match pair.as_rule() {
812        Rule::january => ds::Month::January,
813        Rule::february => ds::Month::February,
814        Rule::march => ds::Month::March,
815        Rule::april => ds::Month::April,
816        Rule::may => ds::Month::May,
817        Rule::june => ds::Month::June,
818        Rule::july => ds::Month::July,
819        Rule::august => ds::Month::August,
820        Rule::september => ds::Month::September,
821        Rule::october => ds::Month::October,
822        Rule::november => ds::Month::November,
823        Rule::december => ds::Month::December,
824        other => unexpected_token(other, Rule::month),
825    }
826}
827
828fn build_year(pair: Pair<Rule>) -> u16 {
829    assert_eq!(pair.as_rule(), Rule::year);
830    pair.as_str().parse().expect("invalid year format")
831}
832
833fn build_positive_number(pair: Pair<Rule>) -> Result<u64> {
834    assert_eq!(pair.as_rule(), Rule::positive_number);
835    pair.as_str().parse().map_err(|_| Error::Overflow {
836        value: pair.as_str().to_string(),
837        expected: "a number between 0 and 2**64".to_string(),
838    })
839}
840
841fn build_comment(pair: Pair<Rule>) -> String {
842    assert_eq!(pair.as_rule(), Rule::comment);
843    build_comment_inner(pair.into_inner().next().expect("empty comment"))
844}
845
846fn build_comment_inner(pair: Pair<Rule>) -> String {
847    assert_eq!(pair.as_rule(), Rule::comment_inner);
848    pair.as_str().to_string()
849}
850
851// Mics
852
853enum PlusOrMinus {
854    Plus,
855    Minus,
856}