Skip to main content

opening_hours_syntax/rules/
mod.rs

1pub mod day;
2pub mod time;
3
4use std::fmt::Display;
5use std::sync::Arc;
6
7use crate::normalize::frame::Bounded;
8use crate::normalize::paving::{Paving, Paving5D};
9use crate::normalize::{canonical_to_seq, ruleseq_to_selector};
10use crate::sorted_vec::UniqueSortedVec;
11
12// OpeningHoursExpression
13
14#[derive(Clone, Debug, Hash, PartialEq, Eq)]
15pub struct OpeningHoursExpression {
16    pub rules: Vec<RuleSequence>,
17}
18
19impl OpeningHoursExpression {
20    /// Check if this expression is *trivially* constant (ie. always evaluated at the exact same
21    /// status). Note that this may return `false` for an expression that is constant but should
22    /// cover most common cases.
23    ///
24    /// ```
25    /// use opening_hours_syntax::parse;
26    ///
27    /// assert!(parse("24/7").unwrap().is_constant());
28    /// assert!(parse("24/7 closed").unwrap().is_constant());
29    /// assert!(parse("00:00-24:00 open").unwrap().is_constant());
30    /// assert!(!parse("00:00-18:00 open").unwrap().is_constant());
31    /// assert!(!parse("24/7 ; PH off").unwrap().is_constant());
32    /// ```
33    pub fn is_constant(&self) -> bool {
34        let Some(kind) = self.rules.last().map(|rs| rs.kind) else {
35            return true;
36        };
37
38        // Ignores rules from the end as long as they are all evaluated to the same kind.
39        let search_tail_full = self.rules.iter().rev().find(|rs| {
40            rs.day_selector.is_empty() || !rs.time_selector.is_00_24() || rs.kind != kind
41        });
42
43        let Some(tail) = search_tail_full else {
44            return kind == RuleKind::Closed;
45        };
46
47        tail.kind == kind && tail.is_constant()
48    }
49
50    /// Convert the expression into a normalized form. It will not affect the meaning of the
51    /// expression and might impact the performance of evaluations.
52    ///
53    /// ```
54    /// let oh = opening_hours_syntax::parse("24/7 ; Su closed").unwrap();
55    /// assert_eq!(oh.normalize().to_string(), "Mo-Sa");
56    /// ```
57    pub fn normalize(self) -> Self {
58        let mut rules_queue = self.rules.into_iter().peekable();
59        let mut paving = Paving5D::default();
60
61        while let Some(rule) = rules_queue.peek() {
62            if rule.operator == RuleOperator::Fallback {
63                break;
64            }
65
66            let Some(selector) = ruleseq_to_selector(rule) else {
67                break;
68            };
69
70            let rule = rules_queue.next().unwrap();
71
72            // If the rule is not explicitly targeting a closed kind, then it overrides
73            // previous rules for the whole day.
74            if rule.operator == RuleOperator::Normal && rule.kind != RuleKind::Closed {
75                let (_, day_selector) = selector.clone().into_unpack_front();
76                let full_day_selector = day_selector.dim_front([Bounded::bounds()]);
77                paving.set(&full_day_selector, &Default::default());
78            }
79
80            paving.set(&selector, &(rule.kind, rule.comments));
81        }
82
83        Self {
84            rules: canonical_to_seq(paving).chain(rules_queue).collect(),
85        }
86    }
87}
88
89impl Display for OpeningHoursExpression {
90    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
91        let Some(first) = self.rules.first() else {
92            return write!(f, "closed");
93        };
94
95        write!(f, "{first}")?;
96
97        for rule in &self.rules[1..] {
98            let separator = match rule.operator {
99                RuleOperator::Normal => " ; ",
100                RuleOperator::Additional => ", ",
101                RuleOperator::Fallback => " || ",
102            };
103
104            write!(f, "{separator}")?;
105
106            // If the rule operatior is an addition, we need to make sure that the time selector is
107            // prefixed with a day selector to avoid ambiguous syntax. For eg. "Mo 10:00-12:00,
108            // 13:00-14:00" is parsed as a single rule while "Mo 10:00-12:00, Mo-Su 13:00-14:00"
109            // isn't.
110            rule.display(f, rule.operator == RuleOperator::Additional)?;
111        }
112
113        Ok(())
114    }
115}
116
117// RuleSequence
118
119#[derive(Clone, Debug, Hash, PartialEq, Eq)]
120pub struct RuleSequence {
121    pub day_selector: day::DaySelector,
122    pub time_selector: time::TimeSelector,
123    pub kind: RuleKind,
124    pub operator: RuleOperator,
125    pub comments: UniqueSortedVec<Arc<str>>,
126}
127
128impl RuleSequence {
129    /// If this returns `true`, then this expression is always open, but it
130    /// can't detect all cases.
131    pub fn is_constant(&self) -> bool {
132        self.day_selector.is_empty() && self.time_selector.is_00_24()
133    }
134
135    pub(crate) fn display(
136        &self,
137        f: &mut std::fmt::Formatter<'_>,
138        force_day_selector: bool,
139    ) -> std::fmt::Result {
140        let mut is_empty = true;
141
142        if self.is_constant() {
143            is_empty = false;
144            write!(f, "24/7")?;
145        } else {
146            is_empty = is_empty && self.day_selector.is_empty();
147            self.day_selector.display(f, force_day_selector)?;
148
149            if !self.time_selector.is_00_24() {
150                if !is_empty {
151                    write!(f, " ")?;
152                }
153
154                is_empty = is_empty && self.time_selector.is_00_24();
155                write!(f, "{}", self.time_selector)?;
156            }
157        }
158
159        if self.kind != RuleKind::Open {
160            if !is_empty {
161                write!(f, " ")?;
162            }
163
164            is_empty = false;
165            write!(f, "{}", self.kind)?;
166        }
167
168        if !self.comments.is_empty() {
169            if !is_empty {
170                write!(f, " ")?;
171            }
172
173            write!(f, "\"{}\"", self.comments.join(", "))?;
174        }
175
176        Ok(())
177    }
178}
179
180impl Display for RuleSequence {
181    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
182        self.display(f, false)
183    }
184}
185
186// RuleKind
187
188#[derive(Copy, Clone, Debug, Default, Hash, Eq, Ord, PartialEq, PartialOrd)]
189pub enum RuleKind {
190    Open,
191    #[default]
192    Closed,
193    Unknown,
194}
195
196impl RuleKind {
197    pub const fn as_str(self) -> &'static str {
198        match self {
199            Self::Open => "open",
200            Self::Closed => "closed",
201            Self::Unknown => "unknown",
202        }
203    }
204}
205
206impl Display for RuleKind {
207    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
208        write!(f, "{}", self.as_str())
209    }
210}
211
212// RuleOperator
213
214#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)]
215pub enum RuleOperator {
216    Normal,
217    Additional,
218    Fallback,
219}