File indexing completed on 2024-04-14 04:35:55

0001 /*
0002     SPDX-FileCopyrightText: 2020 Volker Krause <vkrause@kde.org>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "selectors_p.h"
0008 #include "logging.h"
0009 #include "openinghours_p.h"
0010 
0011 #include <cstdlib>
0012 #include <cassert>
0013 
0014 using namespace KOpeningHours;
0015 
0016 static QByteArray twoDigits(int n)
0017 {
0018     QByteArray ret = QByteArray::number(n);
0019     if (ret.size() < 2) {
0020         ret.prepend('0');
0021     }
0022     return ret;
0023 }
0024 
0025 bool Time::isValid(Time t)
0026 {
0027     return t.hour >= 0 && t.hour <= 48 && t.minute >= 0 && t.minute < 60;
0028 }
0029 
0030 void Time::convertFromAm(Time &t)
0031 {
0032     if (t.hour == 12) {
0033         t.hour = 0;
0034     }
0035 }
0036 
0037 void Time::convertFromPm(Time &t)
0038 {
0039     if (t.hour < 12) {
0040         t.hour += 12;
0041     }
0042 }
0043 
0044 Time Time::parse(const char *begin, const char *end)
0045 {
0046     Time t{ Time::NoEvent, 0, 0 };
0047 
0048     char *it = nullptr;
0049     t.hour = std::strtol(begin, &it, 10);
0050 
0051     for (const auto sep : {':', 'h', 'H'}) {
0052         if (*it == sep) {
0053             ++it;
0054             break;
0055         }
0056     }
0057     if (it != end) {
0058         t.minute = std::strtol(it, nullptr, 10);
0059     }
0060     return t;
0061 }
0062 
0063 QByteArray Time::toExpression(bool end) const
0064 {
0065     QByteArray expr;
0066     switch (event) {
0067     case Time::NoEvent:
0068         if (hour % 24 == 0 && minute == 0 && end)
0069             return "24:00";
0070         return twoDigits(hour) + ':' + twoDigits(minute);
0071     case Time::Dawn:
0072         expr = "dawn";
0073         break;
0074     case Time::Sunrise:
0075         expr = "sunrise";
0076         break;
0077     case Time::Dusk:
0078         expr = "dusk";
0079         break;
0080     case Time::Sunset:
0081         expr = "sunset";
0082         break;
0083     }
0084     const int minutes = hour * 60 + minute;
0085     if (minutes != 0) {
0086         const QByteArray hhmm = twoDigits(qAbs(hour)) + ':' + twoDigits(qAbs(minute));
0087         expr = '(' + expr + (minutes > 0 ? '+' : '-') + hhmm + ')';
0088     }
0089     return expr;
0090 }
0091 
0092 int Timespan::requiredCapabilities() const
0093 {
0094     int c = Capability::None;
0095     if ((interval > 0 || pointInTime) && !openEnd) {
0096         c |= Capability::PointInTime;
0097     } else {
0098         c |= Capability::Interval;
0099     }
0100     if (begin.event != Time::NoEvent || end.event != Time::NoEvent) {
0101         c |= Capability::Location;
0102     }
0103     return next ? (next->requiredCapabilities() | c) : c;
0104 }
0105 
0106 static QByteArray intervalToExpression(int minutes)
0107 {
0108     if (minutes < 60) {
0109         return twoDigits(minutes);
0110     } else {
0111         const int hours = minutes / 60;
0112         minutes -= hours * 60;
0113         return twoDigits(hours) + ':' + twoDigits(minutes);
0114     }
0115 }
0116 
0117 QByteArray Timespan::toExpression() const
0118 {
0119     QByteArray expr = begin.toExpression(false);
0120     if (!pointInTime) {
0121         expr += '-' + end.toExpression(true);
0122     }
0123     if (openEnd) {
0124         expr += '+';
0125     }
0126     if (interval) {
0127         expr += '/' + intervalToExpression(interval);
0128     }
0129     if (next) {
0130         expr += ',' + next->toExpression();
0131     }
0132     return expr;
0133 }
0134 
0135 Time Timespan::adjustedEnd() const
0136 {
0137     if (begin == end && !pointInTime) {
0138         return { end.event, end.hour + 24, end.minute };
0139     }
0140     return end;
0141 }
0142 
0143 bool Timespan::operator==(Timespan &other) const
0144 {
0145     return begin == other.begin &&
0146             end == other.end &&
0147             openEnd == other.openEnd &&
0148             interval == other.interval &&
0149             bool(next) == (bool)other.next &&
0150             (!next || *next == *other.next);
0151 }
0152 
0153 int WeekdayRange::requiredCapabilities() const
0154 {
0155     // only ranges or nthSequence are allowed, not both at the same time, enforced by parser
0156     assert(beginDay == endDay || !nthSequence);
0157 
0158     int c = Capability::None;
0159     switch (holiday) {
0160         case NoHoliday:
0161             if ((offset > 0 && !nthSequence)) {
0162                 c |= Capability::NotImplemented;
0163             }
0164             break;
0165         case PublicHoliday:
0166             c |= Capability::PublicHoliday;
0167             break;
0168         case SchoolHoliday:
0169             c |= Capability::SchoolHoliday;
0170             break;
0171     }
0172 
0173     c |= lhsAndSelector ? lhsAndSelector->requiredCapabilities() : Capability::None;
0174     c |= rhsAndSelector ? rhsAndSelector->requiredCapabilities() : Capability::None;
0175     c |= next ? next->requiredCapabilities() : Capability::None;
0176     return c;
0177 }
0178 
0179 static constexpr const char* s_weekDays[] = { "ERROR", "Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"};
0180 
0181 QByteArray WeekdayRange::toExpression() const
0182 {
0183     QByteArray expr;
0184     if (lhsAndSelector && rhsAndSelector) {
0185         expr = lhsAndSelector->toExpression() + ' ' + rhsAndSelector->toExpression();
0186     } else {
0187         switch (holiday) {
0188         case NoHoliday: {
0189             expr = s_weekDays[beginDay];
0190             if (endDay != beginDay) {
0191                 expr += '-';
0192                 expr += s_weekDays[endDay];
0193             }
0194             break;
0195         }
0196         case PublicHoliday:
0197             expr = "PH";
0198             break;
0199         case SchoolHoliday:
0200             expr = "SH";
0201             break;
0202         }
0203         if (nthSequence) {
0204             expr += '[' + nthSequence->toExpression() + ']';
0205         }
0206         if (offset > 0) {
0207             expr += " +" + QByteArray::number(offset) + ' ' + (offset > 1 ? "days" : "day");
0208         } else if (offset < 0) {
0209             expr += " -" + QByteArray::number(-offset) + ' ' + (offset < -1 ? "days" : "day");
0210         }
0211     }
0212     if (next) {
0213         expr += ',' + next->toExpression();
0214     }
0215     return expr;
0216 }
0217 
0218 void WeekdayRange::simplify()
0219 {
0220     QMap<int, WeekdayRange *> endToSelectorMap;
0221     bool seenDays[8];
0222     const int endIdx = sizeof(seenDays);
0223     std::fill(std::begin(seenDays), std::end(seenDays), false);
0224     for (WeekdayRange *selector = this; selector; selector = selector->next.get()) {
0225         // Ensure it's all just week days, no other features
0226         if (selector->nthSequence || selector->lhsAndSelector || selector->holiday != NoHoliday || selector->offset) {
0227             return;
0228         }
0229         const bool wrap = selector->beginDay > selector->endDay;
0230         for (int day = selector->beginDay; day <= selector->endDay + (wrap ? 7 : 0); ++day) {
0231             seenDays[(day - 1) % 7 + 1] = true;
0232         }
0233         endToSelectorMap.insert(selector->endDay, selector);
0234     }
0235 
0236     QString str;
0237     for (int idx = 1; idx < endIdx; ++idx) {
0238         str += QLatin1Char(seenDays[idx] ? '1' : '0');
0239     }
0240 
0241     // Clear everything and refill
0242     next.reset(nullptr);
0243 
0244     int startIdx = 1;
0245 
0246     // -1 and +1 in a wrapping world
0247     auto prevIdx = [&](int idx) {
0248         Q_ASSERT(idx > 0 && idx < 8);
0249         return idx == 1 ? 7 : (idx - 1);
0250     };
0251     auto nextIdx = [&](int idx) {
0252         Q_ASSERT(idx > 0 && idx < 8);
0253         return idx % 7 + 1;
0254     };
0255 
0256     // like std::find, but let's use indexes - and wrap at 8
0257     auto find = [&](int idx, bool value) {
0258         do {
0259             if (seenDays[idx] == value)
0260                 return idx;
0261             idx = nextIdx(idx);
0262         } while(idx != startIdx);
0263         return idx;
0264     };
0265     auto findPrev = [&](int idx, bool value) {
0266         for (; idx > 0; --idx) {
0267             if (seenDays[idx] == value)
0268                 return idx;
0269         }
0270         return 0;
0271     };
0272 
0273     WeekdayRange *prev = nullptr;
0274     WeekdayRange *selector = this;
0275 
0276     auto addRange = [&](int from, int to) {
0277         if (prev) {
0278             selector = new WeekdayRange;
0279             prev->next.reset(selector);
0280         }
0281         selector->beginDay = from;
0282         selector->endDay = to;
0283         prev = selector;
0284 
0285     };
0286 
0287     int idx = 0;
0288     if (seenDays[1]) {
0289         // monday is set, try going further back
0290         idx = findPrev(7, false);
0291         if (idx) {
0292             idx = nextIdx(idx);
0293         }
0294     }
0295     if (idx == 0) {
0296         // start at first day being set (Tu or more)
0297         idx = find(1, true);
0298     }
0299     startIdx = idx;
0300     Q_ASSERT(startIdx > 0);
0301     do {
0302         // find end of 'true' range
0303         const int finishIdx = find(idx, false);
0304         // if the range is only 2 items, prefer Mo,Tu over Mo-Tu
0305         if (finishIdx == nextIdx(nextIdx(idx))) {
0306             addRange(idx, idx);
0307             const int n = nextIdx(idx);
0308             addRange(n, n);
0309         } else {
0310             addRange(idx, prevIdx(finishIdx));
0311         }
0312         idx = find(finishIdx, true);
0313     } while (idx != startIdx);
0314 }
0315 
0316 int Week::requiredCapabilities() const
0317 {
0318     if (endWeek < beginWeek) { // is this even officially allowed?
0319         return Capability::NotImplemented;
0320     }
0321     return next ? next->requiredCapabilities() : Capability::None;
0322 }
0323 
0324 QByteArray Week::toExpression() const
0325 {
0326     QByteArray expr = twoDigits(beginWeek);
0327     if (endWeek != beginWeek) {
0328         expr += '-';
0329         expr += twoDigits(endWeek);
0330     }
0331     if (interval > 1) {
0332         expr += '/';
0333         expr += QByteArray::number(interval);
0334     }
0335     if (next) {
0336         expr += ',' + next->toExpression();
0337     }
0338     return expr;
0339 }
0340 
0341 QByteArray Date::toExpression(const Date &refDate, const MonthdayRange &prev) const
0342 {
0343     QByteArray expr;
0344     auto maybeSpace = [&]() {
0345         if (!expr.isEmpty()) {
0346             expr += ' ';
0347         }
0348     };
0349     switch (variableDate) {
0350     case FixedDate: {
0351         const bool needYear = year && (year != refDate.year || day && month && month != refDate.month);
0352         if (needYear) {
0353             expr += QByteArray::number(year);
0354         }
0355         if (month) {
0356             const bool combineWithPrev = prev.begin.month == prev.end.month && month == prev.begin.month;
0357             const bool implicitMonth = month == refDate.month || (refDate.month == 0 && combineWithPrev);
0358             if (needYear || !implicitMonth || hasOffset()) {
0359                 static const char* s_monthName[] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };
0360                 maybeSpace();
0361                 expr += s_monthName[month-1];
0362             }
0363         }
0364         if (day && *this != refDate) {
0365             maybeSpace();
0366             expr += twoDigits(day);
0367         }
0368         break;
0369     }
0370     case Date::Easter:
0371         if (year) {
0372             expr += QByteArray::number(year) + ' ';
0373         }
0374         expr += "easter";
0375         break;
0376     }
0377 
0378     if (offset.nthWeekday) {
0379         expr += ' ';
0380         expr += s_weekDays[offset.weekday];
0381         expr += '[' + QByteArray::number(offset.nthWeekday) + ']';
0382     }
0383 
0384     if (offset.dayOffset > 0) {
0385         expr += " +" + QByteArray::number(offset.dayOffset) + ' ' + (offset.dayOffset > 1 ? "days" : "day");
0386     } else if (offset.dayOffset < 0) {
0387         expr += " -" + QByteArray::number(-offset.dayOffset) + ' ' + (offset.dayOffset < -1 ? "days" : "day");
0388     }
0389     return expr;
0390 }
0391 
0392 bool DateOffset::operator==(DateOffset other) const
0393 {
0394     return weekday == other.weekday && nthWeekday == other.nthWeekday && dayOffset == other.dayOffset;
0395 }
0396 
0397 DateOffset &DateOffset::operator+=(DateOffset other)
0398 {
0399     // Only dayOffset really supports += (this is for whitsun)
0400     dayOffset += other.dayOffset;
0401     // The others can't possibly be set already
0402     Q_ASSERT(weekday == 0);
0403     Q_ASSERT(nthWeekday == 0);
0404     weekday = other.weekday;
0405     nthWeekday = other.nthWeekday;
0406     return *this;
0407 }
0408 
0409 bool Date::operator==(Date other) const
0410 {
0411     if (variableDate != other.variableDate)
0412         return false;
0413     if (variableDate == FixedDate && other.variableDate == FixedDate) {
0414         if (!(year == other.year && month == other.month && day == other.day)) {
0415             return false;
0416         }
0417     }
0418     return offset == other.offset;
0419 }
0420 
0421 bool Date::hasOffset() const
0422 {
0423     return offset.dayOffset || offset.weekday;
0424 }
0425 
0426 int MonthdayRange::requiredCapabilities() const
0427 {
0428     return Capability::None;
0429 }
0430 
0431 QByteArray MonthdayRange::toExpression(const MonthdayRange &prev) const
0432 {
0433     QByteArray expr = begin.toExpression({}, prev);
0434     if (end != begin) {
0435         expr += '-' + end.toExpression(begin, prev);
0436     }
0437     if (next) {
0438         expr += ',' + next->toExpression(*this);
0439     }
0440     return expr;
0441 }
0442 
0443 void MonthdayRange::simplify()
0444 {
0445     // "Feb 1-29" => "Feb" (#446252)
0446     if (begin.variableDate == Date::FixedDate &&
0447             end.variableDate == Date::FixedDate &&
0448             begin.year == end.year &&
0449             begin.month && end.month  &&
0450             begin.month == end.month  &&
0451             begin.day && end.day) {
0452         // The year doesn't matter, but take one with a leap day, for Feb 1-29
0453          const int lastDay = QDate{2004, end.month, end.day}.daysInMonth();
0454          if (begin.day == 1 && end.day == lastDay) {
0455              begin.day = 0;
0456              end.day = 0;
0457          }
0458     }
0459 }
0460 
0461 int YearRange::requiredCapabilities() const
0462 {
0463     return Capability::None;
0464 }
0465 
0466 QByteArray YearRange::toExpression() const
0467 {
0468     QByteArray expr = QByteArray::number(begin);
0469     if (end == 0 && interval == 1) {
0470         expr += '+';
0471     } else if (end != begin && end != 0) {
0472         expr += '-';
0473         expr += QByteArray::number(end);
0474     }
0475     if (interval > 1) {
0476         expr += '/';
0477         expr += QByteArray::number(interval);
0478     }
0479     if (next) {
0480         expr += ',' + next->toExpression();
0481     }
0482     return expr;
0483 }
0484 
0485 void NthSequence::add(NthEntry range)
0486 {
0487     sequence.push_back(std::move(range));
0488 }
0489 
0490 QByteArray NthSequence::toExpression() const
0491 {
0492     QByteArray ret;
0493     for (const NthEntry &entry : sequence) {
0494         if (!ret.isEmpty())
0495             ret += ',';
0496         ret += entry.toExpression();
0497     }
0498     return ret;
0499 }
0500 
0501 QByteArray NthEntry::toExpression() const
0502 {
0503     if (begin == end)
0504         return QByteArray::number(begin);
0505     return QByteArray::number(begin) + '-' + QByteArray::number(end);
0506 }