Skip to main content

opening_hours_syntax/rules/
mod.rs

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