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(const QDate &date)
21{
22 return date.daysInMonth(QCalendar(QCalendar::System::Gregorian));
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)};
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())}) + 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(date));
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 QString start(QString train="")
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()
QDate addDays(qint64 ndays) const const
QDate addYears(int nyears) const const
int dayOfWeek() const const
int daysInMonth() 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-2025 The KDE developers.
Generated on Fri May 2 2025 11:52:41 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.