Warning, file /libraries/kopeninghours/src/lib/evaluator.cpp was not indexed or was modified since last indexation (in which case cross-reference links may be missing, inaccurate or erroneous).
0001 /* 0002 SPDX-FileCopyrightText: 2020 Volker Krause <vkrause@kde.org> 0003 0004 SPDX-License-Identifier: LGPL-2.0-or-later 0005 */ 0006 0007 #include <KHolidays/SunRiseSet> 0008 #include "selectors_p.h" 0009 #include "logging.h" 0010 #include "openinghours_p.h" 0011 0012 #include "easter_p.h" 0013 #include "holidaycache_p.h" 0014 0015 #include <QCalendar> 0016 #include <QDateTime> 0017 0018 using namespace KOpeningHours; 0019 0020 static int daysInMonth(int month) 0021 { 0022 return QCalendar(QCalendar::System::Gregorian).daysInMonth(month); 0023 } 0024 0025 static QDateTime resolveTime(Time t, QDate date, OpeningHoursPrivate *context) 0026 { 0027 QDateTime dt; 0028 switch (t.event) { 0029 case Time::NoEvent: 0030 return QDateTime(date, {t.hour % 24, t.minute}); 0031 case Time::Dawn: 0032 dt = QDateTime(date, KHolidays::SunRiseSet::utcDawn(date, context->m_latitude, context->m_longitude), Qt::UTC).toTimeZone(context->m_timezone); break; 0033 case Time::Sunrise: 0034 dt = QDateTime(date, KHolidays::SunRiseSet::utcSunrise(date, context->m_latitude, context->m_longitude), Qt::UTC).toTimeZone(context->m_timezone); 0035 break; 0036 case Time::Dusk: 0037 dt = QDateTime(date, KHolidays::SunRiseSet::utcDusk(date, context->m_latitude, context->m_longitude), Qt::UTC).toTimeZone(context->m_timezone); 0038 break; 0039 case Time::Sunset: 0040 dt = QDateTime(date, KHolidays::SunRiseSet::utcSunset(date, context->m_latitude, context->m_longitude), Qt::UTC).toTimeZone(context->m_timezone); 0041 break; 0042 } 0043 0044 dt.setTimeSpec(Qt::LocalTime); 0045 dt = dt.addSecs(t.hour * 3600 + t.minute * 60); 0046 return dt; 0047 } 0048 0049 bool Timespan::isMultiDay(QDate date, OpeningHoursPrivate *context) const 0050 { 0051 const auto beginDt = resolveTime(begin, date, context); 0052 const auto realEnd = adjustedEnd(); 0053 auto endDt = resolveTime(realEnd, date, context); 0054 if (endDt < beginDt || (realEnd.hour >= 24 && begin.hour < 24)) { 0055 return true; 0056 } 0057 0058 return next ? next->isMultiDay(date, context) : false; 0059 } 0060 0061 SelectorResult Timespan::nextInterval(const Interval &interval, const QDateTime &dt, OpeningHoursPrivate *context) const 0062 { 0063 const auto beginDt = resolveTime(begin, dt.date(), context); 0064 const auto realEnd = adjustedEnd(); 0065 auto endDt = resolveTime(realEnd, dt.date(), context); 0066 if (endDt < beginDt || (realEnd.hour >= 24 && begin.hour < 24)) { 0067 endDt = endDt.addDays(1); 0068 } 0069 0070 if ((dt >= beginDt && dt < endDt) || (beginDt == endDt && beginDt == dt)) { 0071 auto i = interval; 0072 i.setBegin(beginDt); 0073 i.setEnd(endDt); 0074 i.setOpenEndTime(openEnd); 0075 return i; 0076 } 0077 0078 return dt < beginDt ? dt.secsTo(beginDt) : dt.secsTo(beginDt.addDays(1)); 0079 } 0080 0081 static QDate nthWeekdayFromDate(QDate start, int weekDay, int n) 0082 { 0083 if (n > 0) { 0084 const auto delta = (7 + weekDay - start.dayOfWeek()) % 7; 0085 return start.addDays(7 * (n - 1) + (delta == 0 ? 7 : delta)); 0086 } else { 0087 const auto delta = (7 + start.dayOfWeek() - weekDay) % 7; 0088 return start.addDays(7 * (n + 1) - (delta == 0 ? 7 : delta)); 0089 } 0090 } 0091 0092 static QDate nthWeekdayInMonth(QDate month, int weekDay, int n) 0093 { 0094 if (n > 0) { 0095 const auto firstOfMonth = QDate{month.year(), month.month(), 1}; 0096 const auto delta = (7 + weekDay - firstOfMonth.dayOfWeek()) % 7; 0097 const auto day = firstOfMonth.addDays(7 * (n - 1) + delta); 0098 return day.month() == month.month() ? day : QDate(); 0099 } else { 0100 const auto lastOfMonth = QDate{month.year(), month.month(), daysInMonth(month.month())}; 0101 const auto delta = (7 + lastOfMonth.dayOfWeek() - weekDay) % 7; 0102 const auto day = lastOfMonth.addDays(7 * (n + 1) - delta); 0103 return day.month() == month.month() ? day : QDate(); 0104 } 0105 } 0106 0107 SelectorResult WeekdayRange::nextInterval(const Interval &interval, const QDateTime &dt, OpeningHoursPrivate *context) const 0108 { 0109 SelectorResult r; 0110 for (auto s = this; s; s = s->next.get()) { 0111 r = std::min(r, s->nextIntervalLocal(interval, dt, context)); 0112 } 0113 return r; 0114 } 0115 0116 SelectorResult WeekdayRange::nextIntervalLocal(const Interval &interval, const QDateTime &dt, OpeningHoursPrivate *context) const 0117 { 0118 if (lhsAndSelector && rhsAndSelector) { 0119 const auto r1 = lhsAndSelector->nextInterval(interval, dt, context); 0120 if (r1.matchOffset() > 0 || !r1.canMatch()) { 0121 return r1; 0122 } 0123 0124 const auto r2 = rhsAndSelector->nextInterval(interval, dt, context); 0125 if (r2.matchOffset() > 0 || !r2.canMatch()) { 0126 return r2; 0127 } 0128 0129 auto i = r1.interval(); 0130 i.setBegin(std::max(i.begin(), r2.interval().begin())); 0131 i.setEnd(std::min(i.end(), r2.interval().end())); 0132 return i; 0133 } 0134 0135 switch (holiday) { 0136 case NoHoliday: 0137 { 0138 if (nthSequence) { 0139 qint64 smallestOffset = INT_MAX; 0140 for (const NthEntry &entry : nthSequence->sequence) { 0141 Q_ASSERT(entry.begin <= entry.end); 0142 for (int n = entry.begin; n <= entry.end; ++n) { 0143 const auto d = nthWeekdayInMonth(dt.date().addDays(-offset), beginDay, n); 0144 if (!d.isValid() || d.addDays(offset) < dt.date()) { 0145 continue; 0146 } 0147 if (d.addDays(offset) == dt.date()) { 0148 auto i = interval; 0149 i.setBegin(QDateTime(d.addDays(offset), {0, 0})); 0150 i.setEnd(QDateTime(d.addDays(offset + 1), {0, 0})); 0151 return i; 0152 } 0153 // d > dt.date() 0154 smallestOffset = qMin(smallestOffset, dt.secsTo(QDateTime(d.addDays(offset), {0, 0}))); 0155 } 0156 } 0157 if (smallestOffset < INT_MAX) { 0158 return smallestOffset; 0159 } 0160 0161 // skip to next month 0162 return dt.secsTo(QDateTime(dt.date().addDays(dt.date().daysTo({dt.date().year(), dt.date().month(), daysInMonth(dt.date().month())}) + 1 + offset) , {0, 0})); 0163 } 0164 0165 if (beginDay <= endDay) { 0166 if (dt.date().dayOfWeek() < beginDay) { 0167 const auto d = beginDay - dt.date().dayOfWeek(); 0168 return dt.secsTo(QDateTime(dt.date().addDays(d), {0, 0})); 0169 } 0170 if (dt.date().dayOfWeek() > endDay) { 0171 const auto d = 7 + beginDay - dt.date().dayOfWeek(); 0172 return dt.secsTo(QDateTime(dt.date().addDays(d), {0, 0})); 0173 } 0174 } else { 0175 if (dt.date().dayOfWeek() < beginDay && dt.date().dayOfWeek() > endDay) { 0176 const auto d = beginDay - dt.date().dayOfWeek(); 0177 return dt.secsTo(QDateTime(dt.date().addDays(d), {0, 0})); 0178 } 0179 } 0180 0181 auto i = interval; 0182 const auto d = beginDay - dt.date().dayOfWeek(); 0183 i.setBegin(QDateTime(dt.date().addDays(d), {0, 0})); 0184 i.setEnd(QDateTime(i.begin().date().addDays(1 + (beginDay <= endDay ? endDay - beginDay : 7 - (beginDay - endDay))), {0, 0})); 0185 return i; 0186 } 0187 case PublicHoliday: 0188 { 0189 const auto h = HolidayCache::nextHoliday(context->m_region, dt.date().addDays(-offset)); 0190 if (h.name().isEmpty()) { 0191 return false; 0192 } 0193 if (dt.date() < h.observedStartDate().addDays(offset)) { 0194 return dt.secsTo(QDateTime(h.observedStartDate().addDays(offset), {0, 0})); 0195 } 0196 0197 auto i = interval; 0198 i.setBegin(QDateTime(h.observedStartDate().addDays(offset), {0, 0})); 0199 i.setEnd(QDateTime(h.observedEndDate().addDays(1).addDays(offset), {0, 0})); 0200 if (i.comment().isEmpty() && offset == 0) { 0201 i.setComment(h.name()); 0202 } 0203 return i; 0204 } 0205 case SchoolHoliday: 0206 Q_UNREACHABLE(); 0207 } 0208 return {}; 0209 } 0210 0211 SelectorResult Week::nextInterval(const Interval &interval, const QDateTime &dt, OpeningHoursPrivate *context) const 0212 { 0213 Q_UNUSED(context); 0214 if (dt.date().weekNumber() < beginWeek) { 0215 const auto days = (7 - dt.date().dayOfWeek()) + 7 * (beginWeek - dt.date().weekNumber() - 1) + 1; 0216 return dt.secsTo(QDateTime(dt.date().addDays(days), {0, 0})); 0217 } 0218 if (dt.date().weekNumber() > endWeek) { 0219 // "In accordance with ISO 8601, weeks start on Monday and the first Thursday of a year is always in week 1 of that year." 0220 auto d = QDateTime({dt.date().year() + 1, 1, 1}, {0, 0}); 0221 while (d.date().weekNumber() != 1) { 0222 d = d.addDays(1); 0223 } 0224 return dt.secsTo(d); 0225 } 0226 0227 if (this->interval > 1) { 0228 const int wd = (dt.date().weekNumber() - beginWeek) % this->interval; 0229 if (wd) { 0230 const auto days = (7 - dt.date().dayOfWeek()) + 7 * (this->interval - wd - 1) + 1; 0231 return dt.secsTo(QDateTime(dt.date().addDays(days), {0, 0})); 0232 } 0233 } 0234 0235 auto i = interval; 0236 if (this->interval > 1) { 0237 i.setBegin(QDateTime(dt.date().addDays(1 - dt.date().dayOfWeek()), {0, 0})); 0238 i.setEnd(QDateTime(i.begin().date().addDays(7), {0, 0})); 0239 } else { 0240 i.setBegin(QDateTime(dt.date().addDays(1 - dt.date().dayOfWeek() - 7 * (dt.date().weekNumber() - beginWeek)), {0, 0})); 0241 i.setEnd(QDateTime(i.begin().date().addDays((1 + endWeek - beginWeek) * 7), {0, 0})); 0242 } 0243 return i; 0244 } 0245 0246 static QDate resolveDate(Date d, int year) 0247 { 0248 QDate date; 0249 switch (d.variableDate) { 0250 case Date::FixedDate: 0251 date = {d.year ? d.year : year, d.month ? d.month : 1, d.day ? d.day : 1}; 0252 break; 0253 case Date::Easter: 0254 date = Easter::easterDate(d.year ? d.year : year); 0255 break; 0256 } 0257 0258 if (d.offset.weekday && d.offset.nthWeekday) { 0259 if (d.variableDate == Date::Easter) { 0260 date = nthWeekdayFromDate(date, d.offset.weekday, d.offset.nthWeekday); 0261 } else { 0262 date = nthWeekdayInMonth(date, d.offset.weekday, d.offset.nthWeekday); 0263 } 0264 } 0265 date = date.addDays(d.offset.dayOffset); 0266 0267 return date; 0268 } 0269 0270 static QDate resolveDateEnd(Date d, int year) 0271 { 0272 auto date = resolveDate(d, year); 0273 if (d.variableDate == Date::FixedDate) { 0274 if (!d.day && !d.month) { 0275 return date.addYears(1); 0276 } else if (!d.day) { 0277 return date.addDays(daysInMonth(d.month)); 0278 } 0279 } 0280 return date.addDays(1); 0281 } 0282 0283 SelectorResult MonthdayRange::nextInterval(const Interval &interval, const QDateTime &dt, OpeningHoursPrivate *context) const 0284 { 0285 Q_UNUSED(context); 0286 auto beginDt = resolveDate(begin, dt.date().year()); 0287 auto endDt = resolveDateEnd(end, dt.date().year()); 0288 0289 // note that for any of the following we cannot just do addYears(1), as that will break 0290 // for leap years. instead we have to recompute the date again for each year 0291 if (endDt < beginDt || (endDt <= beginDt && begin != end)) { 0292 // month range wraps over the year boundary 0293 endDt = resolveDateEnd(end, dt.date().year() + 1); 0294 } 0295 0296 if (end.year && dt.date() >= endDt) { 0297 return false; 0298 } 0299 0300 // if the current range is in the future, check if we are still in the previous one 0301 if (dt.date() < beginDt && end.month < begin.month) { 0302 auto lookbackBeginDt = resolveDate(begin, dt.date().year() - 1); 0303 auto lookbackEndDt = resolveDateEnd(end, dt.date().year() - 1); 0304 if (lookbackEndDt < lookbackEndDt || (lookbackEndDt <= lookbackBeginDt && begin != end)) { 0305 lookbackEndDt = resolveDateEnd(end, dt.date().year()); 0306 } 0307 if (lookbackEndDt >= dt.date()) { 0308 beginDt = lookbackBeginDt; 0309 endDt = lookbackEndDt; 0310 } 0311 } 0312 0313 if (dt.date() >= endDt) { 0314 beginDt = resolveDate(begin, dt.date().year() + 1); 0315 endDt = resolveDateEnd(end, dt.date().year() + 1); 0316 } 0317 0318 if (dt.date() < beginDt) { 0319 return dt.secsTo(QDateTime(beginDt, {0, 0})); 0320 } 0321 0322 auto i = interval; 0323 i.setBegin(QDateTime(beginDt, {0, 0})); 0324 i.setEnd(QDateTime(endDt, {0, 0})); 0325 return i; 0326 } 0327 0328 SelectorResult YearRange::nextInterval(const Interval &interval, const QDateTime &dt, OpeningHoursPrivate *context) const 0329 { 0330 Q_UNUSED(context); 0331 const auto y = dt.date().year(); 0332 if (begin > y) { 0333 return dt.secsTo(QDateTime({begin, 1, 1}, {0, 0})); 0334 } 0335 if (end > 0 && end < y) { 0336 return false; 0337 } 0338 0339 if (this->interval > 1) { 0340 const int yd = (y - begin) % this->interval; 0341 if (yd) { 0342 return dt.secsTo(QDateTime({y + (this->interval - yd), 1, 1}, {0, 0})); 0343 } 0344 } 0345 0346 auto i = interval; 0347 if (this->interval > 1) { 0348 i.setBegin(QDateTime({y, 1, 1}, {0, 0})); 0349 i.setEnd(QDateTime({y + 1, 1, 1}, {0, 0})); 0350 } else { 0351 i.setBegin(QDateTime({begin, 1, 1}, {0, 0})); 0352 i.setEnd(end > 0 ? QDateTime({end + 1, 1, 1}, {0, 0}) : QDateTime()); 0353 } 0354 return i; 0355 } 0356 0357 RuleResult Rule::nextInterval(const QDateTime &dt, OpeningHoursPrivate *context) const 0358 { 0359 // handle time selectors spanning midnight 0360 // consider e.g. "Tu 12:00-12:00" being evaluated with dt being Wednesday 08:00 0361 // we need to look one day back to find a matching day selector and the correct start 0362 // of the interval here 0363 if (m_timeSelector && m_timeSelector->isMultiDay(dt.date(), context)) { 0364 const auto res = nextInterval(dt.addDays(-1), context, RecursionLimit); 0365 if (res.interval.contains(dt)) { 0366 return res; 0367 } 0368 } 0369 0370 return nextInterval(dt, context, RecursionLimit); 0371 } 0372 0373 RuleResult Rule::nextInterval(const QDateTime &dt, OpeningHoursPrivate *context, int recursionBudget) const 0374 { 0375 const auto resultMode = (recursionBudget == Rule::RecursionLimit && m_ruleType == NormalRule && state() != Interval::Closed) ? RuleResult::Override : RuleResult::Merge; 0376 0377 if (recursionBudget == 0) { 0378 context->m_error = OpeningHours::EvaluationError; 0379 qCWarning(Log) << "Recursion limited reached!"; 0380 return {{}, resultMode}; 0381 } 0382 0383 Interval i; 0384 i.setState(state()); 0385 i.setComment(m_comment); 0386 if (!m_timeSelector && !m_weekdaySelector && !m_monthdaySelector && !m_weekSelector && !m_yearSelector) { 0387 // 24/7 has no selectors 0388 return {i, resultMode}; 0389 } 0390 0391 if (m_yearSelector) { 0392 SelectorResult r; 0393 for (auto s = m_yearSelector.get(); s; s = s->next.get()) { 0394 r = std::min(r, s->nextInterval(i, dt, context)); 0395 } 0396 if (!r.canMatch()) { 0397 return {{}, resultMode}; 0398 } 0399 if (r.matchOffset() > 0) { 0400 return nextInterval(dt.addSecs(r.matchOffset()), context, --recursionBudget); 0401 } 0402 i = r.interval(); 0403 } 0404 0405 if (m_monthdaySelector) { 0406 SelectorResult r; 0407 for (auto s = m_monthdaySelector.get(); s; s = s->next.get()) { 0408 r = std::min(r, s->nextInterval(i, dt, context)); 0409 } 0410 if (!r.canMatch()) { 0411 return {{}, resultMode}; 0412 } 0413 if (r.matchOffset() > 0) { 0414 return nextInterval(dt.addSecs(r.matchOffset()), context, --recursionBudget); 0415 } 0416 i = r.interval(); 0417 } 0418 0419 if (m_weekSelector) { 0420 SelectorResult r; 0421 for (auto s = m_weekSelector.get(); s; s = s->next.get()) { 0422 r = std::min(r, s->nextInterval(i, dt, context)); 0423 } 0424 if (!r.canMatch()) { 0425 return {{}, resultMode}; 0426 } 0427 if (r.matchOffset() > 0) { 0428 return nextInterval(dt.addSecs(r.matchOffset()), context, --recursionBudget); 0429 } 0430 i = r.interval(); 0431 } 0432 0433 if (m_weekdaySelector) { 0434 SelectorResult r = m_weekdaySelector->nextInterval(i, dt, context); 0435 if (!r.canMatch()) { 0436 return {{}, resultMode}; 0437 } 0438 if (r.matchOffset() > 0) { 0439 return nextInterval(dt.addSecs(r.matchOffset()), context, --recursionBudget); 0440 } 0441 i = r.interval(); 0442 } 0443 0444 if (m_timeSelector) { 0445 SelectorResult r; 0446 for (auto s = m_timeSelector.get(); s; s = s->next.get()) { 0447 r = std::min(r, s->nextInterval(i, dt, context)); 0448 } 0449 if (!r.canMatch()) { 0450 return {{}, resultMode}; 0451 } 0452 if (r.matchOffset() > 0) { 0453 return {nextInterval(dt.addSecs(r.matchOffset()), context, --recursionBudget).interval, resultMode}; 0454 } 0455 i = r.interval(); 0456 } 0457 0458 return {i, resultMode}; 0459 }