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::filter::date_filter::DateFilter;
13use crate::filter::time_filter::{
14 time_selector_intervals_at, time_selector_intervals_at_next_day, TimeFilter,
15};
16use crate::localization::{Localize, NoLocation};
17use crate::schedule::Schedule;
18use crate::Context;
19use crate::DateTimeRange;
20
21pub 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
28pub 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#[derive(Clone, Debug, Hash, PartialEq, Eq)]
42pub struct OpeningHours<L: Localize = NoLocation> {
43 expr: Arc<OpeningHoursExpression>,
45 ctx: Context<L>,
47}
48
49impl OpeningHours<NoLocation> {
50 #[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 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 pub fn get_context(&self) -> &Context<L> {
83 &self.ctx
84 }
85
86 pub fn get_expression(&self) -> Arc<OpeningHoursExpression> {
88 self.expr.clone()
89 }
90
91 pub fn with_context<L2: Localize>(self, ctx: Context<L2>) -> OpeningHours<L2> {
101 OpeningHours { expr: self.expr, ctx }
102 }
103
104 pub fn normalize(&self) -> Self {
114 Self {
115 expr: Arc::new(self.expr.as_ref().clone().normalize()),
116 ctx: self.ctx.clone(),
117 }
118 }
119
120 fn next_change_hint(&self, date: NaiveDate) -> Option<NaiveDate> {
133 if date < DATE_START.date() {
134 return Some(DATE_START.date());
135 }
136
137 if self.expr.is_constant() {
138 return Some(DATE_END.date());
139 }
140
141 (self.expr.rules)
142 .iter()
143 .map(|rule| {
144 if rule.time_selector.is_immutable_full_day()
145 || !rule.day_selector.filter(date, &self.ctx)
146 {
147 rule.day_selector.next_change_hint(date, &self.ctx)
148 } else {
149 date.succ_opt()
150 }
151 })
152 .min()
153 .flatten()
154 }
155
156 pub fn schedule_at(&self, date: NaiveDate) -> Schedule {
158 if !(DATE_START.date()..DATE_END.date()).contains(&date) {
159 return Schedule::default();
160 }
161
162 let mut prev_match = false;
163 let mut prev_eval = None;
164
165 for rules_seq in &self.expr.rules {
166 let curr_match = rules_seq.day_selector.filter(date, &self.ctx);
167 let curr_eval = rule_sequence_schedule_at(rules_seq, date, &self.ctx);
168
169 let (new_match, new_eval) = match (rules_seq.operator, rules_seq.kind) {
170 (RuleOperator::Normal, RuleKind::Open | RuleKind::Unknown) => (
172 curr_match || prev_match,
173 if curr_match {
174 curr_eval
175 } else {
176 prev_eval.or(curr_eval)
177 },
178 ),
179 (RuleOperator::Additional, _) | (RuleOperator::Normal, RuleKind::Closed) => (
180 prev_match || curr_match,
181 match (prev_eval, curr_eval) {
182 (Some(prev), Some(curr)) => Some(prev.addition(curr)),
183 (prev, curr) => prev.or(curr),
184 },
185 ),
186 (RuleOperator::Fallback, _) => {
187 if prev_match
188 && !(prev_eval.as_ref())
189 .map(Schedule::is_always_closed_with_no_comments)
190 .unwrap_or(false)
191 {
192 (prev_match, prev_eval)
193 } else {
194 (curr_match, curr_eval)
195 }
196 }
197 };
198
199 prev_match = new_match;
200 prev_eval = new_eval;
201 }
202
203 prev_eval
204 .map(Schedule::filter_closed_ranges)
205 .unwrap_or_else(Schedule::new)
206 }
207
208 fn iter_range_naive(
210 &self,
211 from: NaiveDateTime,
212 to: NaiveDateTime,
213 ) -> impl Iterator<Item = DateTimeRange> + Send + Sync + use<L> {
214 let from = std::cmp::min(DATE_END, from);
215 let to = std::cmp::min(DATE_END, to);
216
217 TimeDomainIterator::new(self, from, to)
218 .take_while(move |dtr| dtr.range.start < to)
219 .map(move |dtr| {
220 let start = std::cmp::max(dtr.range.start, from);
221 let end = std::cmp::min(dtr.range.end, to);
222
223 DateTimeRange {
224 range: start..end,
225 kind: dtr.kind,
226 comment: dtr.comment.clone(),
227 }
228 })
229 }
230
231 pub fn iter_range(
238 &self,
239 from: L::DateTime,
240 to: L::DateTime,
241 ) -> impl Iterator<Item = DateTimeRange<L::DateTime>> + Send + Sync + use<L> {
242 let locale = self.ctx.locale.clone();
243 let naive_from = std::cmp::min(DATE_END, locale.naive(from));
244 let naive_to = std::cmp::min(DATE_END, locale.naive(to));
245
246 self.iter_range_naive(naive_from, naive_to)
247 .map(move |dtr| DateTimeRange {
248 range: locale.datetime(dtr.range.start)..locale.datetime(dtr.range.end),
249 kind: dtr.kind,
250 comment: dtr.comment.clone(),
251 })
252 }
253
254 pub fn iter_from(
256 &self,
257 from: L::DateTime,
258 ) -> impl Iterator<Item = DateTimeRange<L::DateTime>> + Send + Sync + use<L> {
259 self.iter_range(from, self.ctx.locale.datetime(DATE_END))
260 }
261
262 pub fn next_change(&self, current_time: L::DateTime) -> Option<L::DateTime> {
275 let interval = self.iter_from(current_time).next()?;
276
277 if self.ctx.locale.naive(interval.range.end.clone()) >= DATE_END {
278 None
279 } else {
280 Some(interval.range.end)
281 }
282 }
283
284 pub fn state(&self, current_time: L::DateTime) -> (RuleKind, Arc<str>) {
298 self.iter_range(current_time.clone(), current_time + Duration::minutes(1))
299 .next()
300 .map(|dtr| dtr.into_state())
301 .unwrap_or_default()
302 }
303
304 pub fn is_open(&self, current_time: L::DateTime) -> bool {
317 self.state(current_time).0 == RuleKind::Open
318 }
319
320 pub fn is_closed(&self, current_time: L::DateTime) -> bool {
333 self.state(current_time).0 == RuleKind::Closed
334 }
335
336 pub fn is_unknown(&self, current_time: L::DateTime) -> bool {
349 self.state(current_time).0 == RuleKind::Unknown
350 }
351}
352
353impl FromStr for OpeningHours {
354 type Err = ParserError;
355
356 fn from_str(s: &str) -> Result<Self, Self::Err> {
357 OpeningHours::parse_with(&mut Parser::default(), s)
358 }
359}
360
361impl<L: Localize> Display for OpeningHours<L> {
362 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
363 write!(f, "{}", self.expr)
364 }
365}
366
367fn rule_sequence_schedule_at<L: Localize>(
371 rule_sequence: &RuleSequence,
372 date: NaiveDate,
373 ctx: &Context<L>,
374) -> Option<Schedule> {
375 #[cfg(test)]
376 crate::tests::utils::stats::notify::generated_schedule();
377
378 fn build_from_rules_at_date<L: Localize>(
380 rule_sequence: &RuleSequence,
381 date: NaiveDate,
382 ctx: &Context<L>,
383 intervals: impl Iterator<Item = std::ops::Range<ExtendedTime>>,
384 ) -> Option<Schedule> {
385 if !rule_sequence.day_selector.filter(date, ctx) {
386 return None;
387 }
388
389 let overriden_kind = {
390 if rule_sequence
391 .day_selector
392 .overrides_kind_to_unknown(date, ctx)
393 {
394 RuleKind::Unknown
395 } else {
396 rule_sequence.kind
397 }
398 };
399
400 Some(Schedule::from_ranges(
401 intervals,
402 overriden_kind,
403 rule_sequence.comment.clone(),
404 ))
405 }
406
407 let schedule_from_today = build_from_rules_at_date(
408 rule_sequence,
409 date,
410 ctx,
411 time_selector_intervals_at(ctx, &rule_sequence.time_selector, date),
412 );
413
414 let schedule_from_yesterday = date.pred_opt().and_then(|yesterday| {
417 build_from_rules_at_date(
418 rule_sequence,
419 yesterday,
420 ctx,
421 time_selector_intervals_at_next_day(ctx, &rule_sequence.time_selector, yesterday),
422 )
423 });
424
425 match (schedule_from_today, schedule_from_yesterday) {
426 (Some(sched_1), Some(sched_2)) => Some(sched_1.addition(sched_2)),
427 (opt_1, opt_2) => opt_1.or(opt_2),
428 }
429}
430
431pub struct TimeDomainIterator<L: Clone + Localize> {
434 opening_hours: OpeningHours<L>,
435 curr_date: NaiveDate,
436 curr_schedule: Peekable<crate::schedule::IntoIter>,
437 end_datetime: NaiveDateTime,
438}
439
440impl<L: Localize> TimeDomainIterator<L> {
441 fn new(
442 opening_hours: &OpeningHours<L>,
443 start_datetime: NaiveDateTime,
444 end_datetime: NaiveDateTime,
445 ) -> Self {
446 let opening_hours = opening_hours.clone();
447 let start_date = start_datetime.date();
448 let start_time = start_datetime.time().into();
449 let mut curr_schedule = opening_hours.schedule_at(start_date).into_iter().peekable();
450
451 if start_datetime >= end_datetime {
452 (&mut curr_schedule).for_each(|_| {});
453 }
454
455 while curr_schedule
456 .peek()
457 .map(|tr| !tr.range.contains(&start_time))
458 .unwrap_or(false)
459 {
460 curr_schedule.next();
461 }
462
463 Self {
464 opening_hours,
465 curr_date: start_date,
466 curr_schedule,
467 end_datetime,
468 }
469 }
470
471 fn consume_until_next_state(&mut self, curr_state: (RuleKind, &str)) {
472 let start_date = self.curr_date;
473
474 while self
475 .curr_schedule
476 .peek()
477 .map(|tr| tr.as_state() == curr_state)
478 .unwrap_or(false)
479 {
480 if let Some(max_interval_size) = self.opening_hours.ctx.approx_bound_interval_size {
482 if self.curr_date - start_date > max_interval_size + chrono::TimeDelta::days(1) {
483 return;
484 }
485 }
486
487 self.curr_schedule.next();
488
489 if self.curr_schedule.peek().is_none() {
490 let next_change_hint = self
491 .opening_hours
492 .next_change_hint(self.curr_date)
493 .unwrap_or_else(|| self.curr_date.succ_opt().expect("reached invalid date"));
494
495 assert!(next_change_hint > self.curr_date, "infinite loop detected");
496 self.curr_date = next_change_hint;
497
498 if self.curr_date > self.end_datetime.date() || self.curr_date >= DATE_END.date() {
499 break;
500 }
501
502 self.curr_schedule = (self.opening_hours)
503 .schedule_at(self.curr_date)
504 .into_iter()
505 .peekable();
506 }
507 }
508 }
509}
510
511impl<L: Localize> Iterator for TimeDomainIterator<L> {
512 type Item = DateTimeRange;
513
514 fn next(&mut self) -> Option<Self::Item> {
515 if let Some(curr_tr) = self.curr_schedule.peek().cloned() {
516 let start = NaiveDateTime::new(
517 self.curr_date,
518 curr_tr
519 .range
520 .start
521 .try_into()
522 .expect("got invalid time from schedule"),
523 );
524
525 self.consume_until_next_state(curr_tr.as_state());
526 let end_date = self.curr_date;
527
528 let end_time = self
529 .curr_schedule
530 .peek()
531 .map(|tr| tr.range.start)
532 .unwrap_or(ExtendedTime::MIDNIGHT_00);
533
534 let end = std::cmp::min(
535 self.end_datetime,
536 NaiveDateTime::new(
537 end_date,
538 end_time.try_into().expect("got invalid time from schedule"),
539 ),
540 );
541
542 if let Some(max_interval_size) = self.opening_hours.ctx.approx_bound_interval_size {
544 if end - start > max_interval_size {
545 return Some(DateTimeRange {
546 range: start..DATE_END,
547 kind: curr_tr.kind,
548 comment: curr_tr.comment.clone(),
549 });
550 }
551 }
552
553 Some(DateTimeRange {
554 range: start..end,
555 kind: curr_tr.kind,
556 comment: curr_tr.comment.clone(),
557 })
558 } else {
559 None
560 }
561 }
562}