Warning, file /libraries/kopeninghours/src/lib/selectors.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 "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 && year != prev.begin.year && year != prev.end.year; 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 }