File indexing completed on 2024-04-21 04:40:43

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 }