Skip to main content

opening_hours_syntax/rules/
mod.rs

1pub mod day;
2pub mod time;
3
4use alloc::str::FromStr;
5use alloc::string::String;
6use alloc::sync::Arc;
7use alloc::vec::Vec;
8use core::fmt::Display;
9
10use crate::normalize::{canonical_to_ruleseq, drain_ruleseq_into_canonical};
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(state) = self.rules.last().map(|rs| rs.as_state()) 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.as_state() != state
41        });
42
43        let Some(tail) = search_tail_full else {
44            return state == Default::default();
45        };
46
47        tail.as_state() == state && 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    ///
58    #[doc = include_str!("../../doc/normalize.md")]
59    pub fn normalize(self) -> Self {
60        let mut old_rules = self.rules.into();
61        let canonical = drain_ruleseq_into_canonical(&mut old_rules);
62        let mut new_rules = canonical_to_ruleseq(canonical);
63        new_rules.extend(old_rules);
64        Self { rules: new_rules }
65    }
66}
67
68impl Display for OpeningHoursExpression {
69    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
70        let Some(first) = self.rules.first() else {
71            return write!(f, "closed");
72        };
73
74        write!(f, "{first}")?;
75
76        for rule in &self.rules[1..] {
77            let separator = match rule.operator {
78                RuleOperator::Normal => "; ",
79                RuleOperator::Additional => ", ",
80                RuleOperator::Fallback => " || ",
81            };
82
83            write!(f, "{separator}")?;
84
85            // If the rule operatior is an addition, we need to make sure that the time selector is
86            // prefixed with a day selector to avoid ambiguous syntax. For eg. "Mo 10:00-12:00,
87            // 13:00-14:00" is parsed as a single rule while "Mo 10:00-12:00, Mo-Su 13:00-14:00"
88            // isn't.
89            rule.display(f, rule.operator == RuleOperator::Additional)?;
90        }
91
92        Ok(())
93    }
94}
95
96// RuleSequence
97
98#[derive(Clone, Debug, Hash, PartialEq, Eq)]
99pub struct RuleSequence {
100    pub day_selector: day::DaySelector,
101    pub time_selector: time::TimeSelector,
102    pub kind: RuleKind,
103    pub operator: RuleOperator,
104    pub comment: Arc<str>,
105}
106
107impl RuleSequence {
108    /// If this returns `true`, then this expression is always open, but it
109    /// can't detect all cases.
110    pub fn is_constant(&self) -> bool {
111        self.day_selector.is_empty() && self.time_selector.is_00_24()
112    }
113
114    /// Extract the kind and comment from the range, which are the values that define current state
115    /// of an expression.
116    pub fn as_state(&self) -> (RuleKind, &str) {
117        (self.kind, &self.comment)
118    }
119
120    /// Format rule sequence into given formatter.
121    ///
122    /// If `force_day_selector` is set to true, the day selector part is guaranteed to yield a
123    /// non-empty string by adding "Mo-Su" as fallback.
124    pub(crate) fn display(
125        &self,
126        f: &mut core::fmt::Formatter<'_>,
127        force_day_selector: bool,
128    ) -> core::fmt::Result {
129        let mut is_empty;
130
131        if self.is_constant() {
132            is_empty = false;
133            write!(f, "24/7")?;
134        } else {
135            self.day_selector.display(f, force_day_selector)?;
136            is_empty = !force_day_selector && self.day_selector.is_empty();
137
138            if !self.time_selector.is_00_24() {
139                if !is_empty {
140                    write!(f, " ")?;
141                }
142
143                is_empty = is_empty && self.time_selector.is_00_24();
144                write!(f, "{}", self.time_selector)?;
145            }
146        }
147
148        if self.kind != RuleKind::Open {
149            if !is_empty {
150                write!(f, " ")?;
151            }
152
153            is_empty = false;
154            write!(f, "{}", self.kind)?;
155        }
156
157        if !self.comment.is_empty() {
158            if !is_empty {
159                write!(f, " ")?;
160            }
161
162            write!(f, "\"{}\"", self.comment)?;
163        }
164
165        Ok(())
166    }
167}
168
169impl Display for RuleSequence {
170    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
171        self.display(f, false)
172    }
173}
174
175// RuleKind
176
177#[derive(Copy, Clone, Debug, Default, Hash, Eq, Ord, PartialEq, PartialOrd)]
178pub enum RuleKind {
179    Open,
180    #[default]
181    Closed,
182    Unknown,
183}
184
185impl RuleKind {
186    pub const fn as_str(self) -> &'static str {
187        match self {
188            Self::Open => "open",
189            Self::Closed => "closed",
190            Self::Unknown => "unknown",
191        }
192    }
193}
194
195impl FromStr for RuleKind {
196    type Err = String;
197
198    fn from_str(s: &str) -> Result<Self, Self::Err> {
199        match s.to_lowercase().as_str() {
200            "open" => Ok(Self::Open),
201            "closed" => Ok(Self::Closed),
202            "unknown" => Ok(Self::Unknown),
203            other => Err(format!("Unknown rule kind {other:?}")),
204        }
205    }
206}
207
208impl Display for RuleKind {
209    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
210        write!(f, "{}", self.as_str())
211    }
212}
213
214// RuleOperator
215
216#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)]
217pub enum RuleOperator {
218    Normal,
219    Additional,
220    Fallback,
221}