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(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 }