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
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 #[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 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 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 (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 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 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 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 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 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 pub fn is_open(&self, current_time: L::DateTime) -> bool {
321 self.state(current_time).0 == RuleKind::Open
322 }
323
324 pub fn is_closed(&self, current_time: L::DateTime) -> bool {
337 self.state(current_time).0 == RuleKind::Closed
338 }
339
340 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
371fn 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 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 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
435pub 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 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 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}