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>(
343 rule_sequence: &RuleSequence,
344 date: NaiveDate,
345 ctx: &Context<L>,
346) -> Option<Schedule> {
347 let from_today = Some(date)
348 .filter(|date| rule_sequence.day_selector.filter(*date, ctx))
349 .map(|date| time_selector_intervals_at(ctx, &rule_sequence.time_selector, date))
350 .map(|rgs| Schedule::from_ranges(rgs, rule_sequence.kind, &rule_sequence.comments));
351
352 let from_yesterday = (date.pred_opt())
353 .filter(|prev| rule_sequence.day_selector.filter(*prev, ctx))
354 .map(|prev| time_selector_intervals_at_next_day(ctx, &rule_sequence.time_selector, prev))
355 .map(|rgs| Schedule::from_ranges(rgs, rule_sequence.kind, &rule_sequence.comments));
356
357 match (from_today, from_yesterday) {
358 (Some(sched_1), Some(sched_2)) => Some(sched_1.addition(sched_2)),
359 (today, yesterday) => today.or(yesterday),
360 }
361}
362
363pub struct TimeDomainIterator<L: Clone + Localize> {
366 opening_hours: OpeningHours<L>,
367 curr_date: NaiveDate,
368 curr_schedule: Peekable<crate::schedule::IntoIter>,
369 end_datetime: NaiveDateTime,
370}
371
372impl<L: Localize> TimeDomainIterator<L> {
373 fn new(
374 opening_hours: &OpeningHours<L>,
375 start_datetime: NaiveDateTime,
376 end_datetime: NaiveDateTime,
377 ) -> Self {
378 let opening_hours = opening_hours.clone();
379 let start_date = start_datetime.date();
380 let start_time = start_datetime.time().into();
381 let mut curr_schedule = opening_hours.schedule_at(start_date).into_iter().peekable();
382
383 if start_datetime >= end_datetime {
384 (&mut curr_schedule).for_each(|_| {});
385 }
386
387 while curr_schedule
388 .peek()
389 .map(|tr| !tr.range.contains(&start_time))
390 .unwrap_or(false)
391 {
392 curr_schedule.next();
393 }
394
395 Self {
396 opening_hours,
397 curr_date: start_date,
398 curr_schedule,
399 end_datetime,
400 }
401 }
402
403 fn consume_until_next_kind(&mut self, curr_kind: RuleKind) {
404 let start_date = self.curr_date;
405
406 while self.curr_schedule.peek().map(|tr| tr.kind) == Some(curr_kind) {
407 if let Some(max_interval_size) = self.opening_hours.ctx.approx_bound_interval_size {
408 if self.curr_date - start_date > max_interval_size + chrono::TimeDelta::days(1) {
409 return;
410 }
411 }
412
413 self.curr_schedule.next();
414
415 if self.curr_schedule.peek().is_none() {
416 let next_change_hint = self
417 .opening_hours
418 .next_change_hint(self.curr_date)
419 .unwrap_or_else(|| self.curr_date.succ_opt().expect("reached invalid date"));
420
421 assert!(next_change_hint > self.curr_date, "infinite loop detected");
422 self.curr_date = next_change_hint;
423
424 if self.curr_date <= self.end_datetime.date() && self.curr_date < DATE_END.date() {
425 self.curr_schedule = self
426 .opening_hours
427 .schedule_at(self.curr_date)
428 .into_iter()
429 .peekable();
430 }
431 }
432 }
433 }
434}
435
436impl<L: Localize> Iterator for TimeDomainIterator<L> {
437 type Item = DateTimeRange;
438
439 fn next(&mut self) -> Option<Self::Item> {
440 if let Some(curr_tr) = self.curr_schedule.peek().cloned() {
441 let start = NaiveDateTime::new(
442 self.curr_date,
443 curr_tr
444 .range
445 .start
446 .try_into()
447 .expect("got invalid time from schedule"),
448 );
449
450 self.consume_until_next_kind(curr_tr.kind);
451 let end_date = self.curr_date;
452
453 let end_time = self
454 .curr_schedule
455 .peek()
456 .map(|tr| tr.range.start)
457 .unwrap_or(ExtendedTime::MIDNIGHT_00);
458
459 let end = std::cmp::min(
460 self.end_datetime,
461 NaiveDateTime::new(
462 end_date,
463 end_time.try_into().expect("got invalid time from schedule"),
464 ),
465 );
466
467 if let Some(max_interval_size) = self.opening_hours.ctx.approx_bound_interval_size {
468 if end - start > max_interval_size {
469 return Some(DateTimeRange::new_with_sorted_comments(
470 start..DATE_END,
471 curr_tr.kind,
472 curr_tr.comments,
473 ));
474 }
475 }
476
477 Some(DateTimeRange::new_with_sorted_comments(
478 start..end,
479 curr_tr.kind,
480 curr_tr.comments,
481 ))
482 } else {
483 None
484 }
485 }
486}