Warning, file /libraries/kopeninghours/src/lib/openinghours.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 "openinghours.h" 0008 #include "openinghours_p.h" 0009 #include "openinghoursparser_p.h" 0010 #include "openinghoursscanner_p.h" 0011 #include "holidaycache_p.h" 0012 #include "interval.h" 0013 #include "rule_p.h" 0014 #include "logging.h" 0015 0016 #include <QDateTime> 0017 #include <QJsonArray> 0018 #include <QJsonObject> 0019 #include <QTimeZone> 0020 0021 #include <memory> 0022 0023 using namespace KOpeningHours; 0024 0025 static bool isWiderThan(Rule *lhs, Rule *rhs) 0026 { 0027 if ((lhs->m_yearSelector && !rhs->m_yearSelector)) { 0028 return true; 0029 } 0030 if (lhs->m_monthdaySelector && rhs->m_monthdaySelector) { 0031 if (lhs->m_monthdaySelector->begin.year > 0 && rhs->m_monthdaySelector->end.year == 0) { 0032 return true; 0033 } 0034 } 0035 0036 // this is far from handling all cases, expand as needed 0037 return false; 0038 } 0039 0040 void OpeningHoursPrivate::autocorrect() 0041 { 0042 if (m_rules.size() <= 1 || m_error == OpeningHours::SyntaxError) { 0043 return; 0044 } 0045 0046 // find incomplete additional rules, and merge them with the preceding rule 0047 // example: "Mo, We, Fr 06:30-21:30" becomes "Mo,We,Fr 06:30-21:30" 0048 // this matters as those two variants have widely varying semantics, and often occur technically wrong in the wild 0049 // the other case is "Mo-Fr 06:30-12:00, 13:00-18:00", which should become "Mo-Fr 06:30-12:00,13:00-18:00" 0050 0051 for (auto it = std::next(m_rules.begin()); it != m_rules.end(); ++it) { 0052 auto rule = (*it).get(); 0053 auto prevRule = (*(std::prev(it))).get(); 0054 0055 if (rule->hasComment() || prevRule->hasComment() || !prevRule->hasImplicitState()) { 0056 continue; 0057 } 0058 const auto prevRuleSingleSelector = prevRule->selectorCount() == 1; 0059 const auto curRuleSingleSelector = rule->selectorCount() == 1; 0060 0061 if (rule->m_ruleType == Rule::AdditionalRule) { 0062 // the previous rule has no time selector, the current rule only has a weekday selector 0063 // so we fold the two rules together 0064 if (!prevRule->m_timeSelector && prevRule->m_weekdaySelector && rule->m_weekdaySelector && !rule->hasWideRangeSelector()) { 0065 auto tmp = std::move(rule->m_weekdaySelector); 0066 rule->m_weekdaySelector = std::move(prevRule->m_weekdaySelector); 0067 rule->m_weekSelector = std::move(prevRule->m_weekSelector); 0068 rule->m_monthdaySelector = std::move(prevRule->m_monthdaySelector); 0069 rule->m_yearSelector = std::move(prevRule->m_yearSelector); 0070 rule->m_colonAfterWideRangeSelector = prevRule->m_colonAfterWideRangeSelector; 0071 auto *selector = rule->m_weekdaySelector.get(); 0072 while (selector->rhsAndSelector) 0073 selector = selector->rhsAndSelector.get(); 0074 appendSelector(selector, std::move(tmp)); 0075 rule->m_ruleType = prevRule->m_ruleType; 0076 std::swap(*it, *std::prev(it)); 0077 it = std::prev(m_rules.erase(it)); 0078 } 0079 0080 // the current rule only has a time selector, so we append that to the previous rule 0081 else if (curRuleSingleSelector && rule->m_timeSelector && prevRule->m_timeSelector) { 0082 appendSelector(prevRule->m_timeSelector.get(), std::move(rule->m_timeSelector)); 0083 prevRule->copyStateFrom(*rule); 0084 it = std::prev(m_rules.erase(it)); 0085 } 0086 0087 // previous is a single weekday selector and current is a single time selector 0088 else if (curRuleSingleSelector && prevRuleSingleSelector && rule->m_timeSelector && prevRule->m_weekdaySelector) { 0089 prevRule->m_timeSelector = std::move(rule->m_timeSelector); 0090 it = std::prev(m_rules.erase(it)); 0091 } 0092 0093 // previous is a single monthday selector 0094 else if (rule->m_monthdaySelector && prevRuleSingleSelector && prevRule->m_monthdaySelector && !isWiderThan(prevRule, rule)) { 0095 auto tmp = std::move(rule->m_monthdaySelector); 0096 rule->m_monthdaySelector = std::move(prevRule->m_monthdaySelector); 0097 appendSelector(rule->m_monthdaySelector.get(), std::move(tmp)); 0098 rule->m_ruleType = prevRule->m_ruleType; 0099 std::swap(*it, *std::prev(it)); 0100 it = std::prev(m_rules.erase(it)); 0101 } 0102 0103 // previous has no time selector and the current one is a misplaced 24/7 rule: 0104 // convert the 24/7 to a 00:00-24:00 time selector 0105 else if (rule->selectorCount() == 0 && rule->m_seen_24_7 && !prevRule->m_timeSelector) { 0106 prevRule->m_timeSelector.reset(new Timespan); 0107 prevRule->m_timeSelector->begin = { Time::NoEvent, 0, 0 }; 0108 prevRule->m_timeSelector->end = { Time::NoEvent, 24, 0 }; 0109 it = std::prev(m_rules.erase(it)); 0110 } 0111 } else if (rule->m_ruleType == Rule::NormalRule) { 0112 // Previous rule has time and other selectors 0113 // Current rule is only a time selector 0114 // "Mo-Sa 12:00-15:00; 18:00-24:00" => "Mo-Sa 12:00-15:00,18:00-24:00" 0115 if (curRuleSingleSelector && rule->m_timeSelector 0116 && prevRule->selectorCount() > 1 && prevRule->m_timeSelector 0117 && rule->state() == prevRule->state()) { 0118 appendSelector(prevRule->m_timeSelector.get(), std::move(rule->m_timeSelector)); 0119 it = std::prev(m_rules.erase(it)); 0120 } 0121 0122 // Both rules have exactly the same selector apart from time 0123 // Ex: "Mo-Sa 12:00-15:00; Mo-Sa 18:00-24:00" => "Mo-Sa 12:00-15:00,18:00-24:00" 0124 // Obviously a bug, it was overwriting the 12:00-15:00 range. 0125 // For now this only supports weekday selectors, could be extended 0126 else if (rule->selectorCount() == prevRule->selectorCount() 0127 && rule->m_timeSelector && prevRule->m_timeSelector 0128 && !rule->hasComment() && !prevRule->hasComment() 0129 && rule->selectorCount() == 2 && rule->m_weekdaySelector && prevRule->m_weekdaySelector 0130 // slower than writing an operator==, but so much easier to write :) 0131 && rule->m_weekdaySelector->toExpression() == prevRule->m_weekdaySelector->toExpression() 0132 && rule->state() == prevRule->state() 0133 ) { 0134 appendSelector(prevRule->m_timeSelector.get(), std::move(rule->m_timeSelector)); 0135 it = std::prev(m_rules.erase(it)); 0136 } 0137 } 0138 } 0139 } 0140 0141 void OpeningHoursPrivate::simplify() 0142 { 0143 if (m_error == OpeningHours::SyntaxError || m_rules.empty()) { 0144 return; 0145 } 0146 0147 for (auto it = std::next(m_rules.begin()); it != m_rules.end(); ++it) { 0148 auto rule = (*it).get(); 0149 auto prevRule = (*(std::prev(it))).get(); 0150 0151 if (rule->m_ruleType == Rule::AdditionalRule || rule->m_ruleType == Rule::NormalRule) { 0152 0153 auto hasNoHoliday = [](WeekdayRange *selector) { 0154 return selector->holiday == WeekdayRange::NoHoliday 0155 && !selector->lhsAndSelector; 0156 }; 0157 // Both rules have the same time and a different weekday selector 0158 // Mo 08:00-13:00; Tu 08:00-13:00 => Mo,Tu 08:00-13:00 0159 if (rule->selectorCount() == prevRule->selectorCount() 0160 && rule->m_timeSelector && prevRule->m_timeSelector 0161 && rule->selectorCount() == 2 && rule->m_weekdaySelector && prevRule->m_weekdaySelector 0162 && hasNoHoliday(rule->m_weekdaySelector.get()) 0163 && hasNoHoliday(prevRule->m_weekdaySelector.get()) 0164 && *rule->m_timeSelector == *prevRule->m_timeSelector 0165 ) { 0166 // We could of course also turn Mo,Tu,We,Th into Mo-Th... 0167 appendSelector(prevRule->m_weekdaySelector.get(), std::move(rule->m_weekdaySelector)); 0168 it = std::prev(m_rules.erase(it)); 0169 continue; 0170 } 0171 } 0172 0173 if (rule->m_ruleType == Rule::AdditionalRule) { 0174 // Both rules have exactly the same selector apart from time 0175 // Ex: "Mo 12:00-15:00, Mo 18:00-24:00" => "Mo 12:00-15:00,18:00-24:00" 0176 // For now this only supports weekday selectors, could be extended 0177 if (rule->selectorCount() == prevRule->selectorCount() 0178 && rule->m_timeSelector && prevRule->m_timeSelector 0179 && !rule->hasComment() && !prevRule->hasComment() 0180 && rule->selectorCount() == 2 && rule->m_weekdaySelector && prevRule->m_weekdaySelector 0181 // slower than writing an operator==, but so much easier to write :) 0182 && rule->m_weekdaySelector->toExpression() == prevRule->m_weekdaySelector->toExpression() 0183 ) { 0184 appendSelector(prevRule->m_timeSelector.get(), std::move(rule->m_timeSelector)); 0185 it = std::prev(m_rules.erase(it)); 0186 } 0187 } 0188 } 0189 0190 // Now try collapsing adjacent week days: Mo,Tu,We => Mo-We 0191 for (auto it = m_rules.begin(); it != m_rules.end(); ++it) { 0192 auto rule = (*it).get(); 0193 if (rule->m_weekdaySelector) { 0194 rule->m_weekdaySelector->simplify(); 0195 } 0196 if (rule->m_monthdaySelector) { 0197 rule->m_monthdaySelector->simplify(); 0198 } 0199 } 0200 } 0201 0202 void OpeningHoursPrivate::validate() 0203 { 0204 if (m_error == OpeningHours::SyntaxError) { 0205 return; 0206 } 0207 if (m_rules.empty()) { 0208 m_error = OpeningHours::Null; 0209 return; 0210 } 0211 0212 int c = Capability::None; 0213 for (const auto &rule : m_rules) { 0214 c |= rule->requiredCapabilities(); 0215 } 0216 0217 if ((c & Capability::Location) && (std::isnan(m_latitude) || std::isnan(m_longitude))) { 0218 m_error = OpeningHours::MissingLocation; 0219 return; 0220 } 0221 #ifndef KOPENINGHOURS_VALIDATOR_ONLY 0222 if (c & Capability::PublicHoliday && !m_region.isValid()) { 0223 m_error = OpeningHours::MissingRegion; 0224 return; 0225 } 0226 #endif 0227 if (((c & Capability::PointInTime) && (m_modes & OpeningHours::PointInTimeMode) == 0) 0228 || ((c & Capability::Interval) && (m_modes & OpeningHours::IntervalMode) == 0)) { 0229 m_error = OpeningHours::IncompatibleMode; 0230 return; 0231 } 0232 if (c & (Capability::SchoolHoliday | Capability::NotImplemented | Capability::PointInTime)) { 0233 m_error = OpeningHours::UnsupportedFeature; 0234 return; 0235 } 0236 0237 m_error = OpeningHours::NoError; 0238 } 0239 0240 void OpeningHoursPrivate::addRule(Rule *parsedRule) 0241 { 0242 std::unique_ptr<Rule> rule(parsedRule); 0243 0244 // discard empty rules 0245 if (rule->isEmpty()) { 0246 return; 0247 } 0248 0249 if (m_initialRuleType != Rule::NormalRule && rule->m_ruleType == Rule::NormalRule) { 0250 rule->m_ruleType = m_initialRuleType; 0251 m_initialRuleType = Rule::NormalRule; 0252 } 0253 0254 // error recovery after a missing rule separator 0255 // only continue here if whatever we got is somewhat plausible 0256 if (m_ruleSeparatorRecovery && !m_rules.empty()) { 0257 if (rule->selectorCount() <= 1) { 0258 // missing separator was actually between time selectors, not rules 0259 if (m_rules.back()->m_timeSelector && rule->m_timeSelector && m_rules.back()->state() == rule->state()) { 0260 appendSelector(m_rules.back()->m_timeSelector.get(), std::move(rule->m_timeSelector)); 0261 rule.reset(); 0262 return; 0263 } else { 0264 m_error = OpeningHours::SyntaxError; 0265 } 0266 } 0267 0268 // error recovery in the middle of a wide-range selector 0269 // the likely meaning is that the wide-range selectors should be merged, which we can only do if the first 0270 // part is "wider" than the right hand side 0271 if (m_rules.back()->hasWideRangeSelector() && rule->hasWideRangeSelector() 0272 && !m_rules.back()->hasSmallRangeSelector() && rule->hasSmallRangeSelector() 0273 && isWiderThan(rule.get(), m_rules.back().get())) 0274 { 0275 m_error = OpeningHours::SyntaxError; 0276 } 0277 0278 // error recovery in case of a wide range selector followed by two wrongly separated small range selectors 0279 // the likely meaning here is that the wide range selector should apply to both small range selectors, 0280 // but that cannot be modeled without duplicating the wide range selector 0281 // therefore we consider such a case invalid, to be on the safe side 0282 if (m_rules.back()->hasWideRangeSelector() && !rule->hasWideRangeSelector()) { 0283 m_error = OpeningHours::SyntaxError; 0284 } 0285 } 0286 0287 m_ruleSeparatorRecovery = false; 0288 m_rules.push_back(std::move(rule)); 0289 } 0290 0291 void OpeningHoursPrivate::restartFrom(int pos, Rule::Type nextRuleType) 0292 { 0293 m_restartPosition = pos; 0294 if (nextRuleType == Rule::GuessRuleType) { 0295 if (m_rules.empty()) { 0296 m_recoveryRuleType = Rule::NormalRule; 0297 } else { 0298 // if autocorrect() could merge the previous rule, we assume that's the intended meaning 0299 const auto &prev = m_rules.back(); 0300 const auto couldBeMerged = prev->selectorCount() == 1 && !prev->hasComment() && prev->hasImplicitState(); 0301 m_recoveryRuleType = couldBeMerged ? Rule::AdditionalRule : Rule::NormalRule; 0302 } 0303 } else { 0304 m_recoveryRuleType = nextRuleType; 0305 } 0306 } 0307 0308 bool OpeningHoursPrivate::isRecovering() const 0309 { 0310 return m_restartPosition > 0; 0311 } 0312 0313 0314 OpeningHours::OpeningHours() 0315 : d(new OpeningHoursPrivate) 0316 { 0317 d->m_error = OpeningHours::Null; 0318 } 0319 0320 OpeningHours::OpeningHours(const QByteArray &openingHours, Modes modes) 0321 : d(new OpeningHoursPrivate) 0322 { 0323 setExpression(openingHours.constData(), openingHours.size(), modes); 0324 } 0325 0326 OpeningHours::OpeningHours(const char *openingHours, std::size_t size, Modes modes) 0327 : d(new OpeningHoursPrivate) 0328 { 0329 setExpression(openingHours, size, modes); 0330 } 0331 0332 0333 OpeningHours::OpeningHours(const OpeningHours&) = default; 0334 OpeningHours::OpeningHours(OpeningHours&&) = default; 0335 OpeningHours::~OpeningHours() = default; 0336 0337 OpeningHours& OpeningHours::operator=(const OpeningHours&) = default; 0338 OpeningHours& OpeningHours::operator=(OpeningHours&&) = default; 0339 0340 void OpeningHours::setExpression(const QByteArray &openingHours, OpeningHours::Modes modes) 0341 { 0342 setExpression(openingHours.constData(), openingHours.size(), modes); 0343 } 0344 0345 void OpeningHours::setExpression(const char *openingHours, std::size_t size, Modes modes) 0346 { 0347 d->m_modes = modes; 0348 0349 d->m_error = OpeningHours::Null; 0350 d->m_rules.clear(); 0351 d->m_initialRuleType = Rule::NormalRule; 0352 d->m_recoveryRuleType = Rule::NormalRule; 0353 d->m_ruleSeparatorRecovery = false; 0354 0355 // trim trailing spaces 0356 // the parser would handle most of this by itself, but fails if a trailing space would produce a trailing rule separator 0357 // so it's easier to just clean this here 0358 while (size > 0 && std::isspace(static_cast<unsigned char>(openingHours[size - 1]))) { 0359 --size; 0360 } 0361 if (size == 0) { 0362 return; 0363 } 0364 0365 d->m_restartPosition = 0; 0366 int offset = 0; 0367 do { 0368 yyscan_t scanner; 0369 if (yylex_init(&scanner)) { 0370 qCWarning(Log) << "Failed to initialize scanner?!"; 0371 d->m_error = SyntaxError; 0372 return; 0373 } 0374 const std::unique_ptr<void, decltype(&yylex_destroy)> lexerCleanup(scanner, &yylex_destroy); 0375 0376 YY_BUFFER_STATE state; 0377 state = yy_scan_bytes(openingHours + offset, size - offset, scanner); 0378 if (yyparse(d.data(), scanner)) { 0379 if (d->m_restartPosition > 1 && d->m_restartPosition + offset < (int)size) { 0380 offset += d->m_restartPosition - 1; 0381 d->m_initialRuleType = d->m_recoveryRuleType; 0382 d->m_recoveryRuleType = Rule::NormalRule; 0383 d->m_restartPosition = 0; 0384 } else { 0385 d->m_error = SyntaxError; 0386 return; 0387 } 0388 d->m_error = NoError; 0389 } else { 0390 if (d->m_error != SyntaxError) { 0391 d->m_error = NoError; 0392 } 0393 offset = -1; 0394 } 0395 0396 yy_delete_buffer(state, scanner); 0397 } while (offset > 0); 0398 0399 d->autocorrect(); 0400 d->validate(); 0401 } 0402 0403 QByteArray OpeningHours::normalizedExpression() const 0404 { 0405 if (d->m_error == SyntaxError) { 0406 return {}; 0407 } 0408 0409 QByteArray ret; 0410 for (const auto &rule : d->m_rules) { 0411 if (!ret.isEmpty()) { 0412 switch (rule->m_ruleType) { 0413 case Rule::NormalRule: 0414 ret += "; "; 0415 break; 0416 case Rule::AdditionalRule: 0417 ret += ", "; 0418 break; 0419 case Rule::FallbackRule: 0420 ret += " || "; 0421 break; 0422 case Rule::GuessRuleType: 0423 Q_UNREACHABLE(); 0424 break; 0425 } 0426 } 0427 ret += rule->toExpression(); 0428 } 0429 return ret; 0430 } 0431 0432 QByteArray OpeningHours::simplifiedExpression() const 0433 { 0434 OpeningHours copy(normalizedExpression()); 0435 copy.d->simplify(); 0436 return copy.normalizedExpression(); 0437 } 0438 0439 QString OpeningHours::normalizedExpressionString() const 0440 { 0441 return QString::fromUtf8(normalizedExpression()); 0442 } 0443 0444 void OpeningHours::setLocation(float latitude, float longitude) 0445 { 0446 d->m_latitude = latitude; 0447 d->m_longitude = longitude; 0448 d->validate(); 0449 } 0450 0451 float OpeningHours::latitude() const 0452 { 0453 return d->m_latitude; 0454 } 0455 0456 void OpeningHours::setLatitude(float latitude) 0457 { 0458 d->m_latitude = latitude; 0459 d->validate(); 0460 } 0461 0462 float OpeningHours::longitude() const 0463 { 0464 return d->m_longitude; 0465 } 0466 0467 void OpeningHours::setLongitude(float longitude) 0468 { 0469 d->m_longitude = longitude; 0470 d->validate(); 0471 } 0472 0473 #ifndef KOPENINGHOURS_VALIDATOR_ONLY 0474 QString OpeningHours::region() const 0475 { 0476 return d->m_region.regionCode(); 0477 } 0478 0479 void OpeningHours::setRegion(QStringView region) 0480 { 0481 d->m_region = HolidayCache::resolveRegion(region); 0482 d->validate(); 0483 } 0484 #endif 0485 0486 QTimeZone OpeningHours::timeZone() const 0487 { 0488 return d->m_timezone; 0489 } 0490 0491 void OpeningHours::setTimeZone(const QTimeZone &tz) 0492 { 0493 d->m_timezone = tz; 0494 } 0495 0496 QString OpeningHours::timeZoneId() const 0497 { 0498 return QString::fromUtf8(d->m_timezone.id()); 0499 } 0500 0501 void OpeningHours::setTimeZoneId(const QString &tzId) 0502 { 0503 d->m_timezone = QTimeZone(tzId.toUtf8()); 0504 } 0505 0506 OpeningHours::Error OpeningHours::error() const 0507 { 0508 return d->m_error; 0509 } 0510 0511 #ifndef KOPENINGHOURS_VALIDATOR_ONLY 0512 Interval OpeningHours::interval(const QDateTime &dt) const 0513 { 0514 if (d->m_error != NoError) { 0515 return {}; 0516 } 0517 0518 const auto alignedTime = QDateTime(dt.date(), {dt.time().hour(), dt.time().minute()}); 0519 Interval i; 0520 // first try to find the nearest open interval, and afterwards check closed rules 0521 for (const auto &rule : d->m_rules) { 0522 if (rule->state() == Interval::Closed) { 0523 continue; 0524 } 0525 if (i.isValid() && i.contains(dt) && rule->m_ruleType == Rule::FallbackRule) { 0526 continue; 0527 } 0528 auto res = rule->nextInterval(alignedTime, d.data()); 0529 if (!res.interval.isValid()) { 0530 continue; 0531 } 0532 if (i.isValid() && res.mode == RuleResult::Override) { 0533 if (res.interval.begin().isValid() && res.interval.begin().date() > alignedTime.date()) { 0534 i = Interval(); 0535 i.setBegin(alignedTime); 0536 i.setEnd({alignedTime.date().addDays(1), {0, 0}}); 0537 i.setState(Interval::Closed), 0538 i.setComment({}); 0539 } else { 0540 i = res.interval; 0541 } 0542 } else { 0543 if (!i.isValid()) { 0544 i = res.interval; 0545 } else { 0546 // fallback rule intervals needs to be capped to the next occurrence of one of its preceding rules 0547 if (rule->m_ruleType == Rule::FallbackRule) { 0548 res.interval.setEnd(res.interval.hasOpenEnd() ? i.begin() : std::min(res.interval.end(), i.begin())); 0549 } 0550 i = i.isValid() ? std::min(i, res.interval) : res.interval; 0551 } 0552 } 0553 } 0554 0555 QDateTime closeEnd = i.begin(), closeBegin = i.end(); 0556 Interval closedInterval; 0557 for (const auto &rule : d->m_rules) { 0558 if (rule->state() != Interval::Closed) { 0559 continue; 0560 } 0561 const auto j = rule->nextInterval(i.begin().isValid() ? i.begin() : alignedTime, d.data()).interval; 0562 if (!j.isValid() || !i.intersects(j)) { 0563 continue; 0564 } 0565 0566 if (j.contains(alignedTime)) { 0567 if (closedInterval.isValid()) { 0568 // TODO we lose comment information here 0569 closedInterval.setBegin(std::min(closedInterval.begin(), j.begin())); 0570 closedInterval.setEnd(std::max(closedInterval.end(), j.end())); 0571 } else { 0572 closedInterval = j; 0573 } 0574 } else if (alignedTime < j.begin()) { 0575 closeBegin = std::min(j.begin(), closeBegin); 0576 } else if (j.end() <= alignedTime) { 0577 closeEnd = std::max(closeEnd, j.end()); 0578 } 0579 } 0580 if (closedInterval.isValid()) { 0581 i = closedInterval; 0582 } else { 0583 i.setBegin(closeEnd); 0584 i.setEnd(closeBegin); 0585 } 0586 0587 // check if the resulting interval contains dt, otherwise create a synthetic fallback interval 0588 if (!i.isValid() || i.contains(dt)) { 0589 return i; 0590 } 0591 0592 Interval i2; 0593 i2.setState(Interval::Closed); 0594 i2.setBegin(dt); 0595 i2.setEnd(i.begin()); 0596 // TODO do we need to intersect this with closed rules as well? 0597 return i2; 0598 } 0599 0600 Interval OpeningHours::nextInterval(const Interval &interval) const 0601 { 0602 if (!interval.hasOpenEnd()) { 0603 auto endDt = interval.end(); 0604 // ensure we move forward even on zero-length open-end intervals, otherwise we get stuck in a loop 0605 if (interval.hasOpenEndTime() && interval.begin() == interval.end()) { 0606 endDt = endDt.addSecs(3600); 0607 } 0608 auto i = this->interval(endDt); 0609 if (i.begin() < interval.end() && i.end() > interval.end()) { 0610 i.setBegin(interval.end()); 0611 } 0612 return i; 0613 } 0614 return {}; 0615 } 0616 #endif 0617 0618 static Rule* openingHoursSpecToRule(const QJsonObject &obj) 0619 { 0620 if (obj.value(QLatin1String("@type")).toString() != QLatin1String("OpeningHoursSpecification")) { 0621 return nullptr; 0622 } 0623 0624 const auto opens = QTime::fromString(obj.value(QLatin1String("opens")).toString()); 0625 const auto closes = QTime::fromString(obj.value(QLatin1String("closes")).toString()); 0626 0627 if (!opens.isValid() || !closes.isValid()) { 0628 return nullptr; 0629 } 0630 0631 auto r = new Rule; 0632 r->setState(State::Open); 0633 // ### is name or description used for comments? 0634 0635 r->m_timeSelector.reset(new Timespan); 0636 r->m_timeSelector->begin = { Time::NoEvent, opens.hour(), opens.minute() }; 0637 r->m_timeSelector->end = { Time::NoEvent, closes.hour(), closes.minute() }; 0638 0639 const auto validFrom = QDate::fromString(obj.value(QLatin1String("validFrom")).toString(), Qt::ISODate); 0640 const auto validTo = QDate::fromString(obj.value(QLatin1String("validThrough")).toString(), Qt::ISODate); 0641 if (validFrom.isValid() || validTo.isValid()) { 0642 r->m_monthdaySelector.reset(new MonthdayRange); 0643 r->m_monthdaySelector->begin = { validFrom.year(), validFrom.month(), validFrom.day(), Date::FixedDate, { 0, 0, 0 } }; 0644 r->m_monthdaySelector->end = { validTo.year(), validTo.month(), validTo.day(), Date::FixedDate, { 0, 0, 0 } }; 0645 } 0646 0647 const auto weekday = obj.value(QLatin1String("dayOfWeek")).toString(); 0648 if (!weekday.isEmpty()) { 0649 r->m_weekdaySelector.reset(new WeekdayRange); 0650 int i = 1; 0651 for (const auto &d : { "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"}) { 0652 if (weekday.endsWith(QLatin1String(d))) { 0653 r->m_weekdaySelector->beginDay = r->m_weekdaySelector->endDay = i; 0654 break; 0655 } 0656 ++i; 0657 } 0658 } 0659 0660 return r; 0661 } 0662 0663 OpeningHours OpeningHours::fromJsonLd(const QJsonObject &obj) 0664 { 0665 OpeningHours result; 0666 0667 const auto oh = obj.value(QLatin1String("openingHours")); 0668 if (oh.isString()) { 0669 result = OpeningHours(oh.toString().toUtf8()); 0670 } else if (oh.isArray()) { 0671 const auto ohA = oh.toArray(); 0672 QByteArray expr; 0673 for (const auto &exprV : ohA) { 0674 const auto exprS = exprV.toString(); 0675 if (exprS.isEmpty()) { 0676 continue; 0677 } 0678 expr += (expr.isEmpty() ? "" : "; ") + exprS.toUtf8(); 0679 } 0680 result = OpeningHours(expr); 0681 } 0682 0683 std::vector<std::unique_ptr<Rule>> rules; 0684 const auto ohs = obj.value(QLatin1String("openingHoursSpecification")).toArray(); 0685 for (const auto &ohsV : ohs) { 0686 const auto r = openingHoursSpecToRule(ohsV.toObject()); 0687 if (r) { 0688 rules.push_back(std::unique_ptr<Rule>(r)); 0689 } 0690 } 0691 const auto sohs = obj.value(QLatin1String("specialOpeningHoursSpecification")).toArray(); 0692 for (const auto &ohsV : sohs) { 0693 const auto r = openingHoursSpecToRule(ohsV.toObject()); 0694 if (r) { 0695 rules.push_back(std::unique_ptr<Rule>(r)); 0696 } 0697 } 0698 for (auto &r : rules) { 0699 result.d->m_rules.push_back(std::move(r)); 0700 } 0701 0702 result.d->validate(); 0703 return result; 0704 }