Skip to main content

opening_hours/
opening_hours.rs

1use std::fmt::Display;
2use std::iter::Peekable;
3use std::str::FromStr;
4use std::sync::Arc;
5
6use chrono::{Duration, NaiveDate, NaiveDateTime, NaiveTime};
7
8use opening_hours_syntax::extended_time::ExtendedTime;
9use opening_hours_syntax::rules::{OpeningHoursExpression, RuleKind, RuleOperator, RuleSequence};
10use opening_hours_syntax::{Error as ParserError, Parser, Warning};
11
12use crate::Context;
13use crate::DateTimeRange;
14use crate::filter::date_filter::DateFilter;
15use crate::filter::time_filter::{
16    TimeFilter, time_selector_intervals_at, time_selector_intervals_at_next_day,
17};
18use crate::localization::{Localize, NoLocation};
19use crate::schedule::Schedule;
20
21/// The lower bound of dates handled by specification
22pub const DATE_START: NaiveDateTime = {
23    let date = NaiveDate::from_ymd_opt(1900, 1, 1).unwrap();
24    let time = NaiveTime::from_hms_opt(0, 0, 0).unwrap();
25    NaiveDateTime::new(date, time)
26};
27
28/// The upper bound of dates handled by specification
29pub const DATE_END: NaiveDateTime = {
30    let date = NaiveDate::from_ymd_opt(10_000, 1, 1).unwrap();
31    let time = NaiveTime::from_hms_opt(0, 0, 0).unwrap();
32    NaiveDateTime::new(date, time)
33};
34
35// OpeningHours
36
37/// A parsed opening hours expression and its evaluation context.
38///
39/// Note that all big inner structures are immutable and wrapped by an `Arc`
40/// so this is safe and fast to clone.
41#[derive(Clone, Debug, Hash, PartialEq, Eq)]
42pub struct OpeningHours<L: Localize = NoLocation> {
43    /// Rules describing opening hours
44    expr: Arc<OpeningHoursExpression>,
45    /// Evaluation context
46    ctx: Context<L>,
47}
48
49impl OpeningHours<NoLocation> {
50    /// Parse a raw opening hours expression.
51    ///
52    /// ```
53    /// use opening_hours::{Context, OpeningHours};
54    ///
55    /// assert!(OpeningHours::parse("24/7 open").is_ok());
56    /// assert!(OpeningHours::parse("not a valid expression").is_err());
57    /// ```
58    #[deprecated(
59        since = "2.0.0",
60        note = "Use `OpeningHours::from_str(raw_oh: &str)` or `raw_oh.parse()` via the trait `std::str::FromStr`"
61    )]
62    pub fn parse(raw_oh: &str) -> Result<Self, ParserError> {
63        raw_oh.parse()
64    }
65
66    /// Use a specific parser configuration to parse an expression.
67    pub fn parse_with<F: FnMut(Warning)>(
68        parser: &mut Parser<F>,
69        raw_oh: &str,
70    ) -> Result<Self, ParserError> {
71        let expr = parser.parse(raw_oh)?;
72        Ok(Self { expr: Arc::new(expr), ctx: Context::default() })
73    }
74}
75
76impl<L: Localize> OpeningHours<L> {
77    // --
78    // -- Builder Methods
79    // --
80
81    /// Get the evaluation context for this expression.
82    pub fn get_context(&self) -> &Context<L> {
83        &self.ctx
84    }
85
86    /// Get the inner expression object.
87    pub fn get_expression(&self) -> Arc<OpeningHoursExpression> {
88        self.expr.clone()
89    }
90
91    /// Set a new evaluation context for this expression.
92    ///
93    /// ```
94    /// use opening_hours::{Context, OpeningHours};
95    ///
96    /// let oh = OpeningHours::parse("Mo-Fr open")
97    ///     .unwrap()
98    ///     .with_context(Context::default());
99    /// ```
100    pub fn with_context<L2: Localize>(self, ctx: Context<L2>) -> OpeningHours<L2> {
101        OpeningHours { expr: self.expr, ctx }
102    }
103
104    /// Convert the expression into a normalized form. It will not affect the meaning of the
105    /// expression and might impact the performance of evaluations.
106    ///
107    /// ```
108    /// use opening_hours::OpeningHours;
109    ///
110    /// let oh = OpeningHours::parse("24/7 ; Su closed").unwrap();
111    /// assert_eq!(oh.normalize().to_string(), "Mo-Sa");
112    /// ```
113    ///
114    #[doc = include_str!("../doc/normalize.md")]
115    pub fn normalize(&self) -> Self {
116        Self {
117            expr: Arc::new(self.expr.as_ref().clone().normalize()),
118            ctx: self.ctx.clone(),
119        }
120    }
121
122    // --
123    // -- Low level implementations.
124    // --
125    //
126    // Following functions are used to build the TimeDomainIterator which is
127    // used to implement all other functions.
128    //
129    // This means that performances matters a lot for these functions and it
130    // would be relevant to focus on optimisations to this regard.
131
132    /// Provide a lower bound to the next date when a different set of rules
133    /// could match.
134    fn next_change_hint(&self, date: NaiveDate) -> Option<NaiveDate> {
135        if date < DATE_START.date() {
136            return Some(DATE_START.date());
137        }
138
139        if self.expr.is_constant() {
140            return Some(DATE_END.date());
141        }
142
143        (self.expr.rules)
144            .iter()
145            .map(|rule| {
146                if rule.time_selector.is_immutable_full_day()
147                    || !rule.day_selector.filter(date, &self.ctx)
148                {
149                    rule.day_selector.next_change_hint(date, &self.ctx)
150                } else {
151                    date.succ_opt()
152                }
153            })
154            .min()
155            .flatten()
156    }
157
158    /// Get the schedule at a given day.
159    pub fn schedule_at(&self, date: NaiveDate) -> Schedule {
160        if !(DATE_START.date()..DATE_END.date()).contains(&date) {
161            return Schedule::default();
162        }
163
164        let mut prev_match = false;
165        let mut prev_eval = None;
166
167        for rules_seq in &self.expr.rules {
168            let curr_match = rules_seq.day_selector.filter(date, &self.ctx);
169            let curr_eval = rule_sequence_schedule_at(rules_seq, date, &self.ctx);
170
171            let (new_match, new_eval) = match (rules_seq.operator, rules_seq.kind) {
172                // The normal rule acts like the additional rule when the kind is "closed".
173                (RuleOperator::Normal, RuleKind::Open | RuleKind::Unknown) => (
174                    curr_match || prev_match,
175                    if curr_match {
176                        curr_eval
177                    } else {
178                        prev_eval
179                            .filter(|s: &Schedule| !s.is_always_closed_with_no_comments())
180                            .or(curr_eval)
181                    },
182                ),
183                (RuleOperator::Additional, _) | (RuleOperator::Normal, RuleKind::Closed) => (
184                    prev_match || curr_match,
185                    match (prev_eval, curr_eval) {
186                        (Some(prev), Some(curr)) => Some(prev.addition(curr)),
187                        (prev, curr) => prev.or(curr),
188                    },
189                ),
190                (RuleOperator::Fallback, _) => {
191                    if prev_match
192                        && !(prev_eval.as_ref())
193                            .map(Schedule::is_always_closed_with_no_comments)
194                            .unwrap_or(true)
195                    {
196                        (prev_match, prev_eval)
197                    } else {
198                        (curr_match, curr_eval)
199                    }
200                }
201            };
202
203            prev_match = new_match;
204            prev_eval = new_eval;
205        }
206
207        prev_eval
208            .map(Schedule::filter_closed_ranges)
209            .unwrap_or_else(Schedule::new)
210    }
211
212    /// Same as [`iter_range`], but with naive date input and outputs.
213    fn iter_range_naive(
214        &self,
215        from: NaiveDateTime,
216        to: NaiveDateTime,
217    ) -> impl Iterator<Item = DateTimeRange> + Send + Sync + use<L> {
218        let from = std::cmp::min(DATE_END, from);
219        let to = std::cmp::min(DATE_END, to);
220
221        TimeDomainIterator::new(self, from, to)
222            .take_while(move |dtr| dtr.range.start < to)
223            .map(move |dtr| {
224                let start = std::cmp::max(dtr.range.start, from);
225                let end = std::cmp::min(dtr.range.end, to);
226
227                DateTimeRange {
228                    range: start..end,
229                    kind: dtr.kind,
230                    comment: dtr.comment.clone(),
231                }
232            })
233    }
234
235    // --
236    // -- High level implementations / Syntactic sugar
237    // --
238
239    /// Iterate over disjoint intervals of different state restricted to the
240    /// time interval `from..to`.
241    pub fn iter_range(
242        &self,
243        from: L::DateTime,
244        to: L::DateTime,
245    ) -> impl Iterator<Item = DateTimeRange<L::DateTime>> + Send + Sync + use<L> {
246        let locale = self.ctx.locale.clone();
247        let naive_from = std::cmp::min(DATE_END, locale.naive(from));
248        let naive_to = std::cmp::min(DATE_END, locale.naive(to));
249
250        self.iter_range_naive(naive_from, naive_to)
251            .map(move |dtr| DateTimeRange {
252                range: locale.datetime(dtr.range.start)..locale.datetime(dtr.range.end),
253                kind: dtr.kind,
254                comment: dtr.comment.clone(),
255            })
256    }
257
258    // Same as [`OpeningHours::iter_range`] but with an open end.
259    pub fn iter_from(
260        &self,
261        from: L::DateTime,
262    ) -> impl Iterator<Item = DateTimeRange<L::DateTime>> + Send + Sync + use<L> {
263        self.iter_range(from, self.ctx.locale.datetime(DATE_END))
264    }
265
266    /// Get the next time where the state will change.
267    ///
268    /// ```
269    /// use chrono::NaiveDateTime;
270    /// use opening_hours::OpeningHours;
271    /// use opening_hours_syntax::RuleKind;
272    ///
273    /// let oh = OpeningHours::parse("12:00-18:00 open, 18:00-20:00 unknown").unwrap();
274    /// let date_1 = NaiveDateTime::parse_from_str("2024-11-18 15:00", "%Y-%m-%d %H:%M").unwrap();
275    /// let date_2 = NaiveDateTime::parse_from_str("2024-11-18 18:00", "%Y-%m-%d %H:%M").unwrap();
276    /// assert_eq!(oh.next_change(date_1), Some(date_2));
277    /// ```
278    pub fn next_change(&self, current_time: L::DateTime) -> Option<L::DateTime> {
279        let interval = self.iter_from(current_time).next()?;
280
281        if self.ctx.locale.naive(interval.range.end.clone()) >= DATE_END {
282            None
283        } else {
284            Some(interval.range.end)
285        }
286    }
287
288    /// Get the state at given time.
289    ///
290    /// ```
291    /// use chrono::NaiveDateTime;
292    /// use opening_hours::OpeningHours;
293    /// use opening_hours_syntax::RuleKind;
294    ///
295    /// let oh = OpeningHours::parse("12:00-18:00 open, 18:00-20:00 unknown").unwrap();
296    /// let date_1 = NaiveDateTime::parse_from_str("2024-11-18 15:00", "%Y-%m-%d %H:%M").unwrap();
297    /// let date_2 = NaiveDateTime::parse_from_str("2024-11-18 19:00", "%Y-%m-%d %H:%M").unwrap();
298    /// assert_eq!(oh.state(date_1), (RuleKind::Open, "".into()));
299    /// assert_eq!(oh.state(date_2), (RuleKind::Unknown, "".into()));
300    /// ```
301    pub fn state(&self, current_time: L::DateTime) -> (RuleKind, Arc<str>) {
302        self.iter_range(current_time.clone(), current_time + Duration::minutes(1))
303            .next()
304            .map(|dtr| dtr.into_state())
305            .unwrap_or_default()
306    }
307
308    /// Check if this is open at a given time.
309    ///
310    /// ```
311    /// use chrono::NaiveDateTime;
312    /// use opening_hours::OpeningHours;
313    ///
314    /// let oh = OpeningHours::parse("12:00-18:00 open, 18:00-20:00 unknown").unwrap();
315    /// let date_1 = NaiveDateTime::parse_from_str("2024-11-18 15:00", "%Y-%m-%d %H:%M").unwrap();
316    /// let date_2 = NaiveDateTime::parse_from_str("2024-11-18 19:00", "%Y-%m-%d %H:%M").unwrap();
317    /// assert!(oh.is_open(date_1));
318    /// assert!(!oh.is_open(date_2));
319    /// ```
320    pub fn is_open(&self, current_time: L::DateTime) -> bool {
321        self.state(current_time).0 == RuleKind::Open
322    }
323
324    /// Check if this is closed at a given time.
325    ///
326    /// ```
327    /// use chrono::NaiveDateTime;
328    /// use opening_hours::OpeningHours;
329    ///
330    /// let oh = OpeningHours::parse("12:00-18:00 open, 18:00-20:00 unknown").unwrap();
331    /// let date_1 = NaiveDateTime::parse_from_str("2024-11-18 10:00", "%Y-%m-%d %H:%M").unwrap();
332    /// let date_2 = NaiveDateTime::parse_from_str("2024-11-18 19:00", "%Y-%m-%d %H:%M").unwrap();
333    /// assert!(oh.is_closed(date_1));
334    /// assert!(!oh.is_closed(date_2));
335    /// ```
336    pub fn is_closed(&self, current_time: L::DateTime) -> bool {
337        self.state(current_time).0 == RuleKind::Closed
338    }
339
340    /// Check if this is unknown at a given time.
341    ///
342    /// ```
343    /// use chrono::NaiveDateTime;
344    /// use opening_hours::OpeningHours;
345    ///
346    /// let oh = OpeningHours::parse("12:00-18:00 open, 18:00-20:00 unknown").unwrap();
347    /// let date_1 = NaiveDateTime::parse_from_str("2024-11-18 19:00", "%Y-%m-%d %H:%M").unwrap();
348    /// let date_2 = NaiveDateTime::parse_from_str("2024-11-18 15:00", "%Y-%m-%d %H:%M").unwrap();
349    /// assert!(oh.is_unknown(date_1));
350    /// assert!(!oh.is_unknown(date_2));
351    /// ```
352    pub fn is_unknown(&self, current_time: L::DateTime) -> bool {
353        self.state(current_time).0 == RuleKind::Unknown
354    }
355}
356
357impl FromStr for OpeningHours {
358    type Err = ParserError;
359
360    fn from_str(s: &str) -> Result<Self, Self::Err> {
361        OpeningHours::parse_with(&mut Parser::default(), s)
362    }
363}
364
365impl<L: Localize> Display for OpeningHours<L> {
366    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
367        write!(f, "{}", self.expr)
368    }
369}
370
371/// Build the full schedule at a given date from a rule sequence:
372/// - handles overlap with previous day,
373/// - handles unknown kind overrides.
374fn rule_sequence_schedule_at<L: Localize>(
375    rule_sequence: &RuleSequence,
376    date: NaiveDate,
377    ctx: &Context<L>,
378) -> Option<Schedule> {
379    #[cfg(test)]
380    crate::tests::utils::stats::notify::generated_schedule();
381
382    /// Build a schedule at a given date from a list of intervals.
383    fn build_from_rules_at_date<L: Localize>(
384        rule_sequence: &RuleSequence,
385        date: NaiveDate,
386        ctx: &Context<L>,
387        intervals: impl Iterator<Item = std::ops::Range<ExtendedTime>>,
388    ) -> Option<Schedule> {
389        if !rule_sequence.day_selector.filter(date, ctx) {
390            return None;
391        }
392
393        let overriden_kind = {
394            if rule_sequence
395                .day_selector
396                .overrides_kind_to_unknown(date, ctx)
397            {
398                RuleKind::Unknown
399            } else {
400                rule_sequence.kind
401            }
402        };
403
404        Some(Schedule::from_ranges(
405            intervals,
406            overriden_kind,
407            rule_sequence.comment.clone(),
408        ))
409    }
410
411    let schedule_from_today = build_from_rules_at_date(
412        rule_sequence,
413        date,
414        ctx,
415        time_selector_intervals_at(ctx, &rule_sequence.time_selector, date),
416    );
417
418    // We can't just return the schedule obtained this way for the given date because a rule from
419    // previous day could overlap (extended times can be specified up to 48h).
420    let schedule_from_yesterday = date.pred_opt().and_then(|yesterday| {
421        build_from_rules_at_date(
422            rule_sequence,
423            yesterday,
424            ctx,
425            time_selector_intervals_at_next_day(ctx, &rule_sequence.time_selector, yesterday),
426        )
427    });
428
429    match (schedule_from_today, schedule_from_yesterday) {
430        (Some(sched_1), Some(sched_2)) => Some(sched_1.addition(sched_2)),
431        (opt_1, opt_2) => opt_1.or(opt_2),
432    }
433}
434
435// TimeDomainIterator
436
437pub struct TimeDomainIterator<L: Clone + Localize> {
438    opening_hours: OpeningHours<L>,
439    curr_date: NaiveDate,
440    curr_schedule: Peekable<crate::schedule::IntoIter>,
441    end_datetime: NaiveDateTime,
442}
443
444impl<L: Localize> TimeDomainIterator<L> {
445    fn new(
446        opening_hours: &OpeningHours<L>,
447        start_datetime: NaiveDateTime,
448        end_datetime: NaiveDateTime,
449    ) -> Self {
450        let opening_hours = opening_hours.clone();
451        let start_date = start_datetime.date();
452        let start_time = start_datetime.time().into();
453        let mut curr_schedule = opening_hours.schedule_at(start_date).into_iter().peekable();
454
455        if start_datetime >= end_datetime {
456            (&mut curr_schedule).for_each(|_| {});
457        }
458
459        while curr_schedule
460            .peek()
461            .map(|tr| !tr.range.contains(&start_time))
462            .unwrap_or(false)
463        {
464            curr_schedule.next();
465        }
466
467        Self {
468            opening_hours,
469            curr_date: start_date,
470            curr_schedule,
471            end_datetime,
472        }
473    }
474
475    fn consume_until_next_state(&mut self, curr_state: (RuleKind, &str)) {
476        let start_date = self.curr_date;
477
478        while self
479            .curr_schedule
480            .peek()
481            .map(|tr| tr.as_state() == curr_state)
482            .unwrap_or(false)
483        {
484            // Early return if infinite approximation is enabled
485            if let Some(max_interval_size) = self.opening_hours.ctx.approx_bound_interval_size
486                && self.curr_date - start_date > max_interval_size + chrono::TimeDelta::days(1)
487            {
488                return;
489            }
490
491            self.curr_schedule.next();
492
493            if self.curr_schedule.peek().is_none() {
494                let next_change_hint = self
495                    .opening_hours
496                    .next_change_hint(self.curr_date)
497                    .unwrap_or_else(|| self.curr_date.succ_opt().expect("reached invalid date"));
498
499                assert!(next_change_hint > self.curr_date, "infinite loop detected");
500                self.curr_date = next_change_hint;
501
502                if self.curr_date > self.end_datetime.date() || self.curr_date >= DATE_END.date() {
503                    break;
504                }
505
506                self.curr_schedule = (self.opening_hours)
507                    .schedule_at(self.curr_date)
508                    .into_iter()
509                    .peekable();
510            }
511        }
512    }
513}
514
515impl<L: Localize> Iterator for TimeDomainIterator<L> {
516    type Item = DateTimeRange;
517
518    fn next(&mut self) -> Option<Self::Item> {
519        if let Some(curr_tr) = self.curr_schedule.peek().cloned() {
520            let start = NaiveDateTime::new(
521                self.curr_date,
522                curr_tr
523                    .range
524                    .start
525                    .try_into()
526                    .expect("got invalid time from schedule"),
527            );
528
529            self.consume_until_next_state(curr_tr.as_state());
530            let end_date = self.curr_date;
531
532            let end_time = self
533                .curr_schedule
534                .peek()
535                .map(|tr| tr.range.start)
536                .unwrap_or(ExtendedTime::MIDNIGHT_00);
537
538            let end = std::cmp::min(
539                self.end_datetime,
540                NaiveDateTime::new(
541                    end_date,
542                    end_time.try_into().expect("got invalid time from schedule"),
543                ),
544            );
545
546            // Infinity approximation, if enabled
547            if let Some(max_interval_size) = self.opening_hours.ctx.approx_bound_interval_size
548                && end - start > max_interval_size
549            {
550                return Some(DateTimeRange {
551                    range: start..DATE_END,
552                    kind: curr_tr.kind,
553                    comment: curr_tr.comment.clone(),
554                });
555            }
556
557            Some(DateTimeRange {
558                range: start..end,
559                kind: curr_tr.kind,
560                comment: curr_tr.comment.clone(),
561            })
562        } else {
563            None
564        }
565    }
566}