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;
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 pub(crate) ctx: Context<L>,
47}
48
49impl OpeningHours<NoLocation> {
50 pub fn parse(raw_oh: &str) -> Result<Self, ParserError> {
59 let expr = Arc::new(opening_hours_syntax::parse(raw_oh)?);
60 Ok(Self { expr, ctx: Context::default() })
61 }
62}
63
64impl<L: Localize> OpeningHours<L> {
65 pub fn with_context<L2: Localize>(self, ctx: Context<L2>) -> OpeningHours<L2> {
79 OpeningHours { expr: self.expr, ctx }
80 }
81
82 pub fn normalize(&self) -> Self {
92 Self {
93 expr: Arc::new(self.expr.as_ref().clone().normalize()),
94 ctx: self.ctx.clone(),
95 }
96 }
97
98 fn next_change_hint(&self, date: NaiveDate) -> Option<NaiveDate> {
111 if date < DATE_START.date() {
112 return Some(DATE_START.date());
113 }
114
115 if self.expr.is_constant() {
116 return Some(DATE_END.date());
117 }
118
119 (self.expr.rules)
120 .iter()
121 .map(|rule| {
122 if rule.time_selector.is_immutable_full_day()
123 || !rule.day_selector.filter(date, &self.ctx)
124 {
125 rule.day_selector.next_change_hint(date, &self.ctx)
126 } else {
127 date.succ_opt()
128 }
129 })
130 .min()
131 .flatten()
132 }
133
134 pub fn schedule_at(&self, date: NaiveDate) -> Schedule {
136 #[cfg(test)]
137 crate::tests::stats::notify::generated_schedule();
138
139 if !(DATE_START.date()..DATE_END.date()).contains(&date) {
140 return Schedule::default();
141 }
142
143 let mut prev_match = false;
144 let mut prev_eval = None;
145
146 for rules_seq in &self.expr.rules {
147 let curr_match = rules_seq.day_selector.filter(date, &self.ctx);
148 let curr_eval = rule_sequence_schedule_at(rules_seq, date, &self.ctx);
149
150 let (new_match, new_eval) = match (rules_seq.operator, rules_seq.kind) {
151 (RuleOperator::Normal, RuleKind::Open | RuleKind::Unknown) => (
153 curr_match || prev_match,
154 if curr_match {
155 curr_eval
156 } else {
157 prev_eval.or(curr_eval)
158 },
159 ),
160 (RuleOperator::Additional, _) | (RuleOperator::Normal, RuleKind::Closed) => (
161 prev_match || curr_match,
162 match (prev_eval, curr_eval) {
163 (Some(prev), Some(curr)) => Some(prev.addition(curr)),
164 (prev, curr) => prev.or(curr),
165 },
166 ),
167 (RuleOperator::Fallback, _) => {
168 if prev_match
169 && !(prev_eval.as_ref())
170 .map(Schedule::is_always_closed)
171 .unwrap_or(false)
172 {
173 (prev_match, prev_eval)
174 } else {
175 (curr_match, curr_eval)
176 }
177 }
178 };
179
180 prev_match = new_match;
181 prev_eval = new_eval;
182 }
183
184 prev_eval.unwrap_or_else(Schedule::new)
185 }
186
187 fn iter_range_naive(
189 &self,
190 from: NaiveDateTime,
191 to: NaiveDateTime,
192 ) -> impl Iterator<Item = DateTimeRange> + Send + Sync + use<L> {
193 let from = std::cmp::min(DATE_END, from);
194 let to = std::cmp::min(DATE_END, to);
195
196 TimeDomainIterator::new(self, from, to)
197 .take_while(move |dtr| dtr.range.start < to)
198 .map(move |dtr| {
199 let start = std::cmp::max(dtr.range.start, from);
200 let end = std::cmp::min(dtr.range.end, to);
201 DateTimeRange::new_with_sorted_comments(start..end, dtr.kind, dtr.comments)
202 })
203 }
204
205 pub fn iter_range(
212 &self,
213 from: L::DateTime,
214 to: L::DateTime,
215 ) -> impl Iterator<Item = DateTimeRange<L::DateTime>> + Send + Sync + use<L> {
216 let locale = self.ctx.locale.clone();
217 let naive_from = std::cmp::min(DATE_END, locale.naive(from));
218 let naive_to = std::cmp::min(DATE_END, locale.naive(to));
219
220 self.iter_range_naive(naive_from, naive_to).map(move |dtr| {
221 DateTimeRange::new_with_sorted_comments(
222 locale.datetime(dtr.range.start)..locale.datetime(dtr.range.end),
223 dtr.kind,
224 dtr.comments,
225 )
226 })
227 }
228
229 pub fn iter_from(
231 &self,
232 from: L::DateTime,
233 ) -> impl Iterator<Item = DateTimeRange<L::DateTime>> + Send + Sync + use<L> {
234 self.iter_range(from, self.ctx.locale.datetime(DATE_END))
235 }
236
237 pub fn next_change(&self, current_time: L::DateTime) -> Option<L::DateTime> {
250 let interval = self.iter_from(current_time).next()?;
251
252 if self.ctx.locale.naive(interval.range.end.clone()) >= DATE_END {
253 None
254 } else {
255 Some(interval.range.end)
256 }
257 }
258
259 pub fn state(&self, current_time: L::DateTime) -> RuleKind {
273 self.iter_range(current_time.clone(), current_time + Duration::minutes(1))
274 .next()
275 .map(|dtr| dtr.kind)
276 .unwrap_or(RuleKind::Closed)
277 }
278
279 pub fn is_open(&self, current_time: L::DateTime) -> bool {
292 self.state(current_time) == RuleKind::Open
293 }
294
295 pub fn is_closed(&self, current_time: L::DateTime) -> bool {
308 self.state(current_time) == RuleKind::Closed
309 }
310
311 pub fn is_unknown(&self, current_time: L::DateTime) -> bool {
324 self.state(current_time) == RuleKind::Unknown
325 }
326}
327
328impl FromStr for OpeningHours {
329 type Err = ParserError;
330
331 fn from_str(s: &str) -> Result<Self, Self::Err> {
332 Self::parse(s)
333 }
334}
335
336impl<L: Localize> Display for OpeningHours<L> {
337 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
338 write!(f, "{}", self.expr)
339 }
340}
341
342fn rule_sequence_schedule_at<L: Localize>(
346 rule_sequence: &RuleSequence,
347 date: NaiveDate,
348 ctx: &Context<L>,
349) -> Option<Schedule> {
350 fn build_from_rules_at_date<L: Localize>(
352 rule_sequence: &RuleSequence,
353 date: NaiveDate,
354 ctx: &Context<L>,
355 intervals: impl Iterator<Item = std::ops::Range<ExtendedTime>>,
356 ) -> Option<Schedule> {
357 if !rule_sequence.day_selector.filter(date, ctx) {
358 return None;
359 }
360
361 let overriden_kind = {
362 if rule_sequence
363 .day_selector
364 .overrides_kind_to_unknown(date, ctx)
365 {
366 RuleKind::Unknown
367 } else {
368 rule_sequence.kind
369 }
370 };
371
372 Some(Schedule::from_ranges(
373 intervals,
374 overriden_kind,
375 &rule_sequence.comments,
376 ))
377 }
378
379 let schedule_from_today = build_from_rules_at_date(
380 rule_sequence,
381 date,
382 ctx,
383 time_selector_intervals_at(ctx, &rule_sequence.time_selector, date),
384 );
385
386 let schedule_from_yesterday = date.pred_opt().and_then(|yesterday| {
389 build_from_rules_at_date(
390 rule_sequence,
391 yesterday,
392 ctx,
393 time_selector_intervals_at_next_day(ctx, &rule_sequence.time_selector, yesterday),
394 )
395 });
396
397 match (schedule_from_today, schedule_from_yesterday) {
398 (Some(sched_1), Some(sched_2)) => Some(sched_1.addition(sched_2)),
399 (opt_1, opt_2) => opt_1.or(opt_2),
400 }
401}
402
403pub struct TimeDomainIterator<L: Clone + Localize> {
406 opening_hours: OpeningHours<L>,
407 curr_date: NaiveDate,
408 curr_schedule: Peekable<crate::schedule::IntoIter>,
409 end_datetime: NaiveDateTime,
410}
411
412impl<L: Localize> TimeDomainIterator<L> {
413 fn new(
414 opening_hours: &OpeningHours<L>,
415 start_datetime: NaiveDateTime,
416 end_datetime: NaiveDateTime,
417 ) -> Self {
418 let opening_hours = opening_hours.clone();
419 let start_date = start_datetime.date();
420 let start_time = start_datetime.time().into();
421 let mut curr_schedule = opening_hours.schedule_at(start_date).into_iter().peekable();
422
423 if start_datetime >= end_datetime {
424 (&mut curr_schedule).for_each(|_| {});
425 }
426
427 while curr_schedule
428 .peek()
429 .map(|tr| !tr.range.contains(&start_time))
430 .unwrap_or(false)
431 {
432 curr_schedule.next();
433 }
434
435 Self {
436 opening_hours,
437 curr_date: start_date,
438 curr_schedule,
439 end_datetime,
440 }
441 }
442
443 fn consume_until_next_kind(&mut self, curr_kind: RuleKind) {
444 let start_date = self.curr_date;
445
446 while self.curr_schedule.peek().map(|tr| tr.kind) == Some(curr_kind) {
447 if let Some(max_interval_size) = self.opening_hours.ctx.approx_bound_interval_size {
448 if self.curr_date - start_date > max_interval_size + chrono::TimeDelta::days(1) {
449 return;
450 }
451 }
452
453 self.curr_schedule.next();
454
455 if self.curr_schedule.peek().is_none() {
456 let next_change_hint = self
457 .opening_hours
458 .next_change_hint(self.curr_date)
459 .unwrap_or_else(|| self.curr_date.succ_opt().expect("reached invalid date"));
460
461 assert!(next_change_hint > self.curr_date, "infinite loop detected");
462 self.curr_date = next_change_hint;
463
464 if self.curr_date <= self.end_datetime.date() && self.curr_date < DATE_END.date() {
465 self.curr_schedule = self
466 .opening_hours
467 .schedule_at(self.curr_date)
468 .into_iter()
469 .peekable();
470 }
471 }
472 }
473 }
474}
475
476impl<L: Localize> Iterator for TimeDomainIterator<L> {
477 type Item = DateTimeRange;
478
479 fn next(&mut self) -> Option<Self::Item> {
480 if let Some(curr_tr) = self.curr_schedule.peek().cloned() {
481 let start = NaiveDateTime::new(
482 self.curr_date,
483 curr_tr
484 .range
485 .start
486 .try_into()
487 .expect("got invalid time from schedule"),
488 );
489
490 self.consume_until_next_kind(curr_tr.kind);
491 let end_date = self.curr_date;
492
493 let end_time = self
494 .curr_schedule
495 .peek()
496 .map(|tr| tr.range.start)
497 .unwrap_or(ExtendedTime::MIDNIGHT_00);
498
499 let end = std::cmp::min(
500 self.end_datetime,
501 NaiveDateTime::new(
502 end_date,
503 end_time.try_into().expect("got invalid time from schedule"),
504 ),
505 );
506
507 if let Some(max_interval_size) = self.opening_hours.ctx.approx_bound_interval_size {
508 if end - start > max_interval_size {
509 return Some(DateTimeRange::new_with_sorted_comments(
510 start..DATE_END,
511 curr_tr.kind,
512 curr_tr.comments,
513 ));
514 }
515 }
516
517 Some(DateTimeRange::new_with_sorted_comments(
518 start..end,
519 curr_tr.kind,
520 curr_tr.comments,
521 ))
522 } else {
523 None
524 }
525 }
526}