KOpeningHours

evaluator.cpp
1/*
2 SPDX-FileCopyrightText: 2020 Volker Krause <vkrause@kde.org>
3
4 SPDX-License-Identifier: LGPL-2.0-or-later
5*/
6
7#include <KHolidays/SunRiseSet>
8#include "selectors_p.h"
9#include "logging.h"
10#include "openinghours_p.h"
11
12#include "easter_p.h"
13#include "holidaycache_p.h"
14
15#include <QCalendar>
16#include <QDateTime>
17
18using namespace KOpeningHours;
19
20static int daysInMonth(int month)
21{
22 return QCalendar(QCalendar::System::Gregorian).daysInMonth(month);
23}
24
25static QDateTime resolveTime(Time t, QDate date, OpeningHoursPrivate *context)
26{
27 QDateTime dt;
28 switch (t.event) {
29 case Time::NoEvent:
30 return QDateTime(date, {t.hour % 24, t.minute});
31 case Time::Dawn:
32 dt = QDateTime(date, KHolidays::SunRiseSet::utcDawn(date, context->m_latitude, context->m_longitude), Qt::UTC).toTimeZone(context->m_timezone); break;
33 case Time::Sunrise:
34 dt = QDateTime(date, KHolidays::SunRiseSet::utcSunrise(date, context->m_latitude, context->m_longitude), Qt::UTC).toTimeZone(context->m_timezone);
35 break;
36 case Time::Dusk:
37 dt = QDateTime(date, KHolidays::SunRiseSet::utcDusk(date, context->m_latitude, context->m_longitude), Qt::UTC).toTimeZone(context->m_timezone);
38 break;
39 case Time::Sunset:
40 dt = QDateTime(date, KHolidays::SunRiseSet::utcSunset(date, context->m_latitude, context->m_longitude), Qt::UTC).toTimeZone(context->m_timezone);
41 break;
42 }
43
45 dt = dt.addSecs(t.hour * 3600 + t.minute * 60);
46 return dt;
47}
48
49bool Timespan::isMultiDay(QDate date, OpeningHoursPrivate *context) const
50{
51 const auto beginDt = resolveTime(begin, date, context);
52 const auto realEnd = adjustedEnd();
53 auto endDt = resolveTime(realEnd, date, context);
54 if (endDt < beginDt || (realEnd.hour >= 24 && begin.hour < 24)) {
55 return true;
56 }
57
58 return next ? next->isMultiDay(date, context) : false;
59}
60
61SelectorResult Timespan::nextInterval(const Interval &interval, const QDateTime &dt, OpeningHoursPrivate *context) const
62{
63 const auto beginDt = resolveTime(begin, dt.date(), context);
64 const auto realEnd = adjustedEnd();
65 auto endDt = resolveTime(realEnd, dt.date(), context);
66 if (endDt < beginDt || (realEnd.hour >= 24 && begin.hour < 24)) {
67 endDt = endDt.addDays(1);
68 }
69
70 if ((dt >= beginDt && dt < endDt) || (beginDt == endDt && beginDt == dt)) {
71 auto i = interval;
72 i.setBegin(beginDt);
73 i.setEnd(endDt);
74 i.setOpenEndTime(openEnd);
75 return i;
76 }
77
78 return dt < beginDt ? dt.secsTo(beginDt) : dt.secsTo(beginDt.addDays(1));
79}
80
81static QDate nthWeekdayFromDate(QDate start, int weekDay, int n)
82{
83 if (n > 0) {
84 const auto delta = (7 + weekDay - start.dayOfWeek()) % 7;
85 return start.addDays(7 * (n - 1) + (delta == 0 ? 7 : delta));
86 } else {
87 const auto delta = (7 + start.dayOfWeek() - weekDay) % 7;
88 return start.addDays(7 * (n + 1) - (delta == 0 ? 7 : delta));
89 }
90}
91
92static QDate nthWeekdayInMonth(QDate month, int weekDay, int n)
93{
94 if (n > 0) {
95 const auto firstOfMonth = QDate{month.year(), month.month(), 1};
96 const auto delta = (7 + weekDay - firstOfMonth.dayOfWeek()) % 7;
97 const auto day = firstOfMonth.addDays(7 * (n - 1) + delta);
98 return day.month() == month.month() ? day : QDate();
99 } else {
100 const auto lastOfMonth = QDate{month.year(), month.month(), daysInMonth(month.month())};
101 const auto delta = (7 + lastOfMonth.dayOfWeek() - weekDay) % 7;
102 const auto day = lastOfMonth.addDays(7 * (n + 1) - delta);
103 return day.month() == month.month() ? day : QDate();
104 }
105}
106
107SelectorResult WeekdayRange::nextInterval(const Interval &interval, const QDateTime &dt, OpeningHoursPrivate *context) const
108{
109 SelectorResult r;
110 for (auto s = this; s; s = s->next.get()) {
111 r = std::min(r, s->nextIntervalLocal(interval, dt, context));
112 }
113 return r;
114}
115
116SelectorResult WeekdayRange::nextIntervalLocal(const Interval &interval, const QDateTime &dt, OpeningHoursPrivate *context) const
117{
118 if (lhsAndSelector && rhsAndSelector) {
119 const auto r1 = lhsAndSelector->nextInterval(interval, dt, context);
120 if (r1.matchOffset() > 0 || !r1.canMatch()) {
121 return r1;
122 }
123
124 const auto r2 = rhsAndSelector->nextInterval(interval, dt, context);
125 if (r2.matchOffset() > 0 || !r2.canMatch()) {
126 return r2;
127 }
128
129 auto i = r1.interval();
130 i.setBegin(std::max(i.begin(), r2.interval().begin()));
131 i.setEnd(std::min(i.end(), r2.interval().end()));
132 return i;
133 }
134
135 switch (holiday) {
136 case NoHoliday:
137 {
138 if (nthSequence) {
139 qint64 smallestOffset = INT_MAX;
140 for (const NthEntry &entry : nthSequence->sequence) {
141 Q_ASSERT(entry.begin <= entry.end);
142 for (int n = entry.begin; n <= entry.end; ++n) {
143 const auto d = nthWeekdayInMonth(dt.date().addDays(-offset), beginDay, n);
144 if (!d.isValid() || d.addDays(offset) < dt.date()) {
145 continue;
146 }
147 if (d.addDays(offset) == dt.date()) {
148 auto i = interval;
149 i.setBegin(QDateTime(d.addDays(offset), {0, 0}));
150 i.setEnd(QDateTime(d.addDays(offset + 1), {0, 0}));
151 return i;
152 }
153 // d > dt.date()
154 smallestOffset = qMin(smallestOffset, dt.secsTo(QDateTime(d.addDays(offset), {0, 0})));
155 }
156 }
157 if (smallestOffset < INT_MAX) {
158 return smallestOffset;
159 }
160
161 // skip to next month
162 return dt.secsTo(QDateTime(dt.date().addDays(dt.date().daysTo({dt.date().year(), dt.date().month(), daysInMonth(dt.date().month())}) + 1 + offset) , {0, 0}));
163 }
164
165 if (beginDay <= endDay) {
166 if (dt.date().dayOfWeek() < beginDay) {
167 const auto d = beginDay - dt.date().dayOfWeek();
168 return dt.secsTo(QDateTime(dt.date().addDays(d), {0, 0}));
169 }
170 if (dt.date().dayOfWeek() > endDay) {
171 const auto d = 7 + beginDay - dt.date().dayOfWeek();
172 return dt.secsTo(QDateTime(dt.date().addDays(d), {0, 0}));
173 }
174 } else {
175 if (dt.date().dayOfWeek() < beginDay && dt.date().dayOfWeek() > endDay) {
176 const auto d = beginDay - dt.date().dayOfWeek();
177 return dt.secsTo(QDateTime(dt.date().addDays(d), {0, 0}));
178 }
179 }
180
181 auto i = interval;
182 const auto d = beginDay - dt.date().dayOfWeek();
183 i.setBegin(QDateTime(dt.date().addDays(d), {0, 0}));
184 i.setEnd(QDateTime(i.begin().date().addDays(1 + (beginDay <= endDay ? endDay - beginDay : 7 - (beginDay - endDay))), {0, 0}));
185 return i;
186 }
187 case PublicHoliday:
188 {
189 const auto h = HolidayCache::nextHoliday(context->m_region, dt.date().addDays(-offset));
190 if (h.name().isEmpty()) {
191 return false;
192 }
193 if (dt.date() < h.observedStartDate().addDays(offset)) {
194 return dt.secsTo(QDateTime(h.observedStartDate().addDays(offset), {0, 0}));
195 }
196
197 auto i = interval;
198 i.setBegin(QDateTime(h.observedStartDate().addDays(offset), {0, 0}));
199 i.setEnd(QDateTime(h.observedEndDate().addDays(1).addDays(offset), {0, 0}));
200 if (i.comment().isEmpty() && offset == 0) {
201 i.setComment(h.name());
202 }
203 return i;
204 }
205 case SchoolHoliday:
206 Q_UNREACHABLE();
207 }
208 return {};
209}
210
211SelectorResult Week::nextInterval(const Interval &interval, const QDateTime &dt, OpeningHoursPrivate *context) const
212{
213 Q_UNUSED(context);
214 if (dt.date().weekNumber() < beginWeek) {
215 const auto days = (7 - dt.date().dayOfWeek()) + 7 * (beginWeek - dt.date().weekNumber() - 1) + 1;
216 return dt.secsTo(QDateTime(dt.date().addDays(days), {0, 0}));
217 }
218 if (dt.date().weekNumber() > endWeek) {
219 // "In accordance with ISO 8601, weeks start on Monday and the first Thursday of a year is always in week 1 of that year."
220 auto d = QDateTime({dt.date().year() + 1, 1, 1}, {0, 0});
221 while (d.date().weekNumber() != 1) {
222 d = d.addDays(1);
223 }
224 return dt.secsTo(d);
225 }
226
227 if (this->interval > 1) {
228 const int wd = (dt.date().weekNumber() - beginWeek) % this->interval;
229 if (wd) {
230 const auto days = (7 - dt.date().dayOfWeek()) + 7 * (this->interval - wd - 1) + 1;
231 return dt.secsTo(QDateTime(dt.date().addDays(days), {0, 0}));
232 }
233 }
234
235 auto i = interval;
236 if (this->interval > 1) {
237 i.setBegin(QDateTime(dt.date().addDays(1 - dt.date().dayOfWeek()), {0, 0}));
238 i.setEnd(QDateTime(i.begin().date().addDays(7), {0, 0}));
239 } else {
240 i.setBegin(QDateTime(dt.date().addDays(1 - dt.date().dayOfWeek() - 7 * (dt.date().weekNumber() - beginWeek)), {0, 0}));
241 i.setEnd(QDateTime(i.begin().date().addDays((1 + endWeek - beginWeek) * 7), {0, 0}));
242 }
243 return i;
244}
245
246static QDate resolveDate(Date d, int year)
247{
248 QDate date;
249 switch (d.variableDate) {
250 case Date::FixedDate:
251 date = {d.year ? d.year : year, d.month ? d.month : 1, d.day ? d.day : 1};
252 break;
253 case Date::Easter:
254 date = Easter::easterDate(d.year ? d.year : year);
255 break;
256 }
257
258 if (d.offset.weekday && d.offset.nthWeekday) {
259 if (d.variableDate == Date::Easter) {
260 date = nthWeekdayFromDate(date, d.offset.weekday, d.offset.nthWeekday);
261 } else {
262 date = nthWeekdayInMonth(date, d.offset.weekday, d.offset.nthWeekday);
263 }
264 }
265 date = date.addDays(d.offset.dayOffset);
266
267 return date;
268}
269
270static QDate resolveDateEnd(Date d, int year)
271{
272 auto date = resolveDate(d, year);
273 if (d.variableDate == Date::FixedDate) {
274 if (!d.day && !d.month) {
275 return date.addYears(1);
276 } else if (!d.day) {
277 return date.addDays(daysInMonth(d.month));
278 }
279 }
280 return date.addDays(1);
281}
282
283SelectorResult MonthdayRange::nextInterval(const Interval &interval, const QDateTime &dt, OpeningHoursPrivate *context) const
284{
285 Q_UNUSED(context);
286 auto beginDt = resolveDate(begin, dt.date().year());
287 auto endDt = resolveDateEnd(end, dt.date().year());
288
289 // note that for any of the following we cannot just do addYears(1), as that will break
290 // for leap years. instead we have to recompute the date again for each year
291 if (endDt < beginDt || (endDt <= beginDt && begin != end)) {
292 // month range wraps over the year boundary
293 endDt = resolveDateEnd(end, dt.date().year() + 1);
294 }
295
296 if (end.year && dt.date() >= endDt) {
297 return false;
298 }
299
300 // if the current range is in the future, check if we are still in the previous one
301 if (dt.date() < beginDt && end.month < begin.month) {
302 auto lookbackBeginDt = resolveDate(begin, dt.date().year() - 1);
303 auto lookbackEndDt = resolveDateEnd(end, dt.date().year() - 1);
304 if (lookbackEndDt < lookbackEndDt || (lookbackEndDt <= lookbackBeginDt && begin != end)) {
305 lookbackEndDt = resolveDateEnd(end, dt.date().year());
306 }
307 if (lookbackEndDt >= dt.date()) {
308 beginDt = lookbackBeginDt;
309 endDt = lookbackEndDt;
310 }
311 }
312
313 if (dt.date() >= endDt) {
314 beginDt = resolveDate(begin, dt.date().year() + 1);
315 endDt = resolveDateEnd(end, dt.date().year() + 1);
316 }
317
318 if (dt.date() < beginDt) {
319 return dt.secsTo(QDateTime(beginDt, {0, 0}));
320 }
321
322 auto i = interval;
323 i.setBegin(QDateTime(beginDt, {0, 0}));
324 i.setEnd(QDateTime(endDt, {0, 0}));
325 return i;
326}
327
328SelectorResult YearRange::nextInterval(const Interval &interval, const QDateTime &dt, OpeningHoursPrivate *context) const
329{
330 Q_UNUSED(context);
331 const auto y = dt.date().year();
332 if (begin > y) {
333 return dt.secsTo(QDateTime({begin, 1, 1}, {0, 0}));
334 }
335 if (end > 0 && end < y) {
336 return false;
337 }
338
339 if (this->interval > 1) {
340 const int yd = (y - begin) % this->interval;
341 if (yd) {
342 return dt.secsTo(QDateTime({y + (this->interval - yd), 1, 1}, {0, 0}));
343 }
344 }
345
346 auto i = interval;
347 if (this->interval > 1) {
348 i.setBegin(QDateTime({y, 1, 1}, {0, 0}));
349 i.setEnd(QDateTime({y + 1, 1, 1}, {0, 0}));
350 } else {
351 i.setBegin(QDateTime({begin, 1, 1}, {0, 0}));
352 i.setEnd(end > 0 ? QDateTime({end + 1, 1, 1}, {0, 0}) : QDateTime());
353 }
354 return i;
355}
356
357RuleResult Rule::nextInterval(const QDateTime &dt, OpeningHoursPrivate *context) const
358{
359 // handle time selectors spanning midnight
360 // consider e.g. "Tu 12:00-12:00" being evaluated with dt being Wednesday 08:00
361 // we need to look one day back to find a matching day selector and the correct start
362 // of the interval here
363 if (m_timeSelector && m_timeSelector->isMultiDay(dt.date(), context)) {
364 const auto res = nextInterval(dt.addDays(-1), context, RecursionLimit);
365 if (res.interval.contains(dt)) {
366 return res;
367 }
368 }
369
370 return nextInterval(dt, context, RecursionLimit);
371}
372
373RuleResult Rule::nextInterval(const QDateTime &dt, OpeningHoursPrivate *context, int recursionBudget) const
374{
375 const auto resultMode = (recursionBudget == Rule::RecursionLimit && m_ruleType == NormalRule && state() != Interval::Closed) ? RuleResult::Override : RuleResult::Merge;
376
377 if (recursionBudget == 0) {
378 context->m_error = OpeningHours::EvaluationError;
379 qCWarning(Log) << "Recursion limited reached!";
380 return {{}, resultMode};
381 }
382
383 Interval i;
384 i.setState(state());
385 i.setComment(m_comment);
386 if (!m_timeSelector && !m_weekdaySelector && !m_monthdaySelector && !m_weekSelector && !m_yearSelector) {
387 // 24/7 has no selectors
388 return {i, resultMode};
389 }
390
391 if (m_yearSelector) {
392 SelectorResult r;
393 for (auto s = m_yearSelector.get(); s; s = s->next.get()) {
394 r = std::min(r, s->nextInterval(i, dt, context));
395 }
396 if (!r.canMatch()) {
397 return {{}, resultMode};
398 }
399 if (r.matchOffset() > 0) {
400 return nextInterval(dt.addSecs(r.matchOffset()), context, --recursionBudget);
401 }
402 i = r.interval();
403 }
404
405 if (m_monthdaySelector) {
406 SelectorResult r;
407 for (auto s = m_monthdaySelector.get(); s; s = s->next.get()) {
408 r = std::min(r, s->nextInterval(i, dt, context));
409 }
410 if (!r.canMatch()) {
411 return {{}, resultMode};
412 }
413 if (r.matchOffset() > 0) {
414 return nextInterval(dt.addSecs(r.matchOffset()), context, --recursionBudget);
415 }
416 i = r.interval();
417 }
418
419 if (m_weekSelector) {
420 SelectorResult r;
421 for (auto s = m_weekSelector.get(); s; s = s->next.get()) {
422 r = std::min(r, s->nextInterval(i, dt, context));
423 }
424 if (!r.canMatch()) {
425 return {{}, resultMode};
426 }
427 if (r.matchOffset() > 0) {
428 return nextInterval(dt.addSecs(r.matchOffset()), context, --recursionBudget);
429 }
430 i = r.interval();
431 }
432
433 if (m_weekdaySelector) {
434 SelectorResult r = m_weekdaySelector->nextInterval(i, dt, context);
435 if (!r.canMatch()) {
436 return {{}, resultMode};
437 }
438 if (r.matchOffset() > 0) {
439 return nextInterval(dt.addSecs(r.matchOffset()), context, --recursionBudget);
440 }
441 i = r.interval();
442 }
443
444 if (m_timeSelector) {
445 SelectorResult r;
446 for (auto s = m_timeSelector.get(); s; s = s->next.get()) {
447 r = std::min(r, s->nextInterval(i, dt, context));
448 }
449 if (!r.canMatch()) {
450 return {{}, resultMode};
451 }
452 if (r.matchOffset() > 0) {
453 return {nextInterval(dt.addSecs(r.matchOffset()), context, --recursionBudget).interval, resultMode};
454 }
455 i = r.interval();
456 }
457
458 return {i, resultMode};
459}
A time interval for which an opening hours expression has been evaluated.
Definition interval.h:25
@ EvaluationError
runtime error during evaluating the expression
Q_SCRIPTABLE Q_NOREPLY void start()
KHOLIDAYS_EXPORT QTime utcSunset(const QDate &date, double latitude, double longitude)
KHOLIDAYS_EXPORT QTime utcSunrise(const QDate &date, double latitude, double longitude)
KHOLIDAYS_EXPORT QTime utcDawn(const QDate &date, double latitude, double longitude)
KHOLIDAYS_EXPORT QTime utcDusk(const QDate &date, double latitude, double longitude)
OSM opening hours parsing and evaluation.
Definition display.h:16
const QList< QKeySequence > & begin()
const QList< QKeySequence > & next()
const QList< QKeySequence > & end()
int daysInMonth(int month, int year) const const
QDate addDays(qint64 ndays) const const
QDate addYears(int nyears) const const
int dayOfWeek() const const
qint64 daysTo(QDate d) const const
int month() const const
int weekNumber(int *yearNumber) const const
int year() const const
void setTimeSpec(Qt::TimeSpec spec)
QDateTime addDays(qint64 ndays) const const
QDateTime addSecs(qint64 s) const const
QDate date() const const
qint64 secsTo(const QDateTime &other) const const
QDateTime toTimeZone(const QTimeZone &timeZone) const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Tue Mar 26 2024 11:13:17 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.