7#include "openinghours.h"
8#include "openinghours_p.h"
9#include "openinghoursparser_p.h"
10#include "openinghoursscanner_p.h"
11#include "holidaycache_p.h"
25static bool isWiderThan(Rule *lhs, Rule *rhs)
27 if ((lhs->m_yearSelector && !rhs->m_yearSelector)) {
30 if (lhs->m_monthdaySelector && rhs->m_monthdaySelector) {
31 if (lhs->m_monthdaySelector->begin.year > 0 && rhs->m_monthdaySelector->end.year == 0) {
40void OpeningHoursPrivate::autocorrect()
51 for (
auto it = std::next(m_rules.begin()); it != m_rules.end(); ++it) {
52 auto rule = (*it).get();
53 auto prevRule = (*(std::prev(it))).
get();
55 if (rule->hasComment() || prevRule->hasComment() || !prevRule->hasImplicitState()) {
58 const auto prevRuleSingleSelector = prevRule->selectorCount() == 1;
59 const auto curRuleSingleSelector = rule->selectorCount() == 1;
61 if (rule->m_ruleType == Rule::AdditionalRule) {
64 if (!prevRule->m_timeSelector && prevRule->m_weekdaySelector && rule->m_weekdaySelector && !rule->hasWideRangeSelector()) {
65 auto tmp = std::move(rule->m_weekdaySelector);
66 rule->m_weekdaySelector = std::move(prevRule->m_weekdaySelector);
67 rule->m_weekSelector = std::move(prevRule->m_weekSelector);
68 rule->m_monthdaySelector = std::move(prevRule->m_monthdaySelector);
69 rule->m_yearSelector = std::move(prevRule->m_yearSelector);
70 rule->m_colonAfterWideRangeSelector = prevRule->m_colonAfterWideRangeSelector;
71 auto *selector = rule->m_weekdaySelector.get();
72 while (selector->rhsAndSelector)
73 selector = selector->rhsAndSelector.get();
74 appendSelector(selector, std::move(tmp));
75 rule->m_ruleType = prevRule->m_ruleType;
76 std::swap(*it, *std::prev(it));
77 it = std::prev(m_rules.erase(it));
81 else if (curRuleSingleSelector && rule->m_timeSelector && prevRule->m_timeSelector) {
82 appendSelector(prevRule->m_timeSelector.get(), std::move(rule->m_timeSelector));
83 prevRule->copyStateFrom(*rule);
84 it = std::prev(m_rules.erase(it));
88 else if (curRuleSingleSelector && prevRuleSingleSelector && rule->m_timeSelector && prevRule->m_weekdaySelector) {
89 prevRule->m_timeSelector = std::move(rule->m_timeSelector);
90 it = std::prev(m_rules.erase(it));
94 else if (rule->m_monthdaySelector && prevRuleSingleSelector && prevRule->m_monthdaySelector && !isWiderThan(prevRule, rule)) {
95 auto tmp = std::move(rule->m_monthdaySelector);
96 rule->m_monthdaySelector = std::move(prevRule->m_monthdaySelector);
97 appendSelector(rule->m_monthdaySelector.get(), std::move(tmp));
98 rule->m_ruleType = prevRule->m_ruleType;
99 std::swap(*it, *std::prev(it));
100 it = std::prev(m_rules.erase(it));
105 else if (rule->selectorCount() == 0 && rule->m_seen_24_7 && !prevRule->m_timeSelector) {
106 prevRule->m_timeSelector.reset(
new Timespan);
107 prevRule->m_timeSelector->begin = { Time::NoEvent, 0, 0 };
108 prevRule->m_timeSelector->end = { Time::NoEvent, 24, 0 };
109 it = std::prev(m_rules.erase(it));
111 }
else if (rule->m_ruleType == Rule::NormalRule) {
115 if (curRuleSingleSelector && rule->m_timeSelector
116 && prevRule->selectorCount() > 1 && prevRule->m_timeSelector
117 && rule->state() == prevRule->state()) {
118 appendSelector(prevRule->m_timeSelector.get(), std::move(rule->m_timeSelector));
119 it = std::prev(m_rules.erase(it));
126 else if (rule->selectorCount() == prevRule->selectorCount()
127 && rule->m_timeSelector && prevRule->m_timeSelector
128 && !rule->hasComment() && !prevRule->hasComment()
129 && rule->selectorCount() == 2 && rule->m_weekdaySelector && prevRule->m_weekdaySelector
131 && rule->m_weekdaySelector->toExpression() == prevRule->m_weekdaySelector->toExpression()
132 && rule->state() == prevRule->state()
134 appendSelector(prevRule->m_timeSelector.get(), std::move(rule->m_timeSelector));
135 it = std::prev(m_rules.erase(it));
141void OpeningHoursPrivate::simplify()
147 for (
auto it = std::next(m_rules.begin()); it != m_rules.end(); ++it) {
148 auto rule = (*it).get();
149 auto prevRule = (*(std::prev(it))).
get();
151 if (rule->m_ruleType == Rule::AdditionalRule || rule->m_ruleType == Rule::NormalRule) {
153 auto hasNoHoliday = [](WeekdayRange *selector) {
154 return selector->holiday == WeekdayRange::NoHoliday
155 && !selector->lhsAndSelector;
159 if (rule->selectorCount() == prevRule->selectorCount()
160 && rule->m_timeSelector && prevRule->m_timeSelector
161 && rule->selectorCount() == 2 && rule->m_weekdaySelector && prevRule->m_weekdaySelector
162 && hasNoHoliday(rule->m_weekdaySelector.get())
163 && hasNoHoliday(prevRule->m_weekdaySelector.get())
164 && *rule->m_timeSelector == *prevRule->m_timeSelector
167 appendSelector(prevRule->m_weekdaySelector.get(), std::move(rule->m_weekdaySelector));
168 it = std::prev(m_rules.erase(it));
173 if (rule->m_ruleType == Rule::AdditionalRule) {
177 if (rule->selectorCount() == prevRule->selectorCount()
178 && rule->m_timeSelector && prevRule->m_timeSelector
179 && !rule->hasComment() && !prevRule->hasComment()
180 && rule->selectorCount() == 2 && rule->m_weekdaySelector && prevRule->m_weekdaySelector
182 && rule->m_weekdaySelector->toExpression() == prevRule->m_weekdaySelector->toExpression()
184 appendSelector(prevRule->m_timeSelector.get(), std::move(rule->m_timeSelector));
185 it = std::prev(m_rules.erase(it));
191 for (
auto it = m_rules.begin(); it != m_rules.end(); ++it) {
192 auto rule = (*it).get();
193 if (rule->m_weekdaySelector) {
194 rule->m_weekdaySelector->simplify();
196 if (rule->m_monthdaySelector) {
197 rule->m_monthdaySelector->simplify();
202void OpeningHoursPrivate::validate()
207 if (m_rules.empty()) {
212 int c = Capability::None;
213 for (
const auto &rule : m_rules) {
214 c |= rule->requiredCapabilities();
217 if ((c & Capability::Location) && (std::isnan(m_latitude) || std::isnan(m_longitude))) {
221#ifndef KOPENINGHOURS_VALIDATOR_ONLY
222 if (c & Capability::PublicHoliday && !m_region.isValid()) {
232 if (c & (Capability::SchoolHoliday | Capability::NotImplemented | Capability::PointInTime)) {
240void OpeningHoursPrivate::addRule(Rule *parsedRule)
242 std::unique_ptr<Rule> rule(parsedRule);
245 if (rule->isEmpty()) {
249 if (m_initialRuleType != Rule::NormalRule && rule->m_ruleType == Rule::NormalRule) {
250 rule->m_ruleType = m_initialRuleType;
251 m_initialRuleType = Rule::NormalRule;
256 if (m_ruleSeparatorRecovery && !m_rules.empty()) {
257 if (rule->selectorCount() <= 1) {
259 if (m_rules.back()->m_timeSelector && rule->m_timeSelector && m_rules.back()->state() == rule->state()) {
260 appendSelector(m_rules.back()->m_timeSelector.get(), std::move(rule->m_timeSelector));
271 if (m_rules.back()->hasWideRangeSelector() && rule->hasWideRangeSelector()
272 && !m_rules.back()->hasSmallRangeSelector() && rule->hasSmallRangeSelector()
273 && isWiderThan(rule.get(), m_rules.back().get()))
282 if (m_rules.back()->hasWideRangeSelector() && !rule->hasWideRangeSelector()) {
287 m_ruleSeparatorRecovery =
false;
288 m_rules.push_back(std::move(rule));
291void OpeningHoursPrivate::restartFrom(
int pos, Rule::Type nextRuleType)
293 m_restartPosition = pos;
294 if (nextRuleType == Rule::GuessRuleType) {
295 if (m_rules.empty()) {
296 m_recoveryRuleType = Rule::NormalRule;
299 const auto &prev = m_rules.back();
300 const auto couldBeMerged = prev->selectorCount() == 1 && !prev->hasComment() && prev->hasImplicitState();
301 m_recoveryRuleType = couldBeMerged ? Rule::AdditionalRule : Rule::NormalRule;
304 m_recoveryRuleType = nextRuleType;
308bool OpeningHoursPrivate::isRecovering()
const
310 return m_restartPosition > 0;
315 : d(new OpeningHoursPrivate)
321 : d(new OpeningHoursPrivate)
327 : d(new OpeningHoursPrivate)
335OpeningHours::~OpeningHours() =
default;
351 d->m_initialRuleType = Rule::NormalRule;
352 d->m_recoveryRuleType = Rule::NormalRule;
353 d->m_ruleSeparatorRecovery =
false;
358 while (size > 0 && std::isspace(
static_cast<unsigned char>(openingHours[size - 1]))) {
365 d->m_restartPosition = 0;
369 if (yylex_init(&scanner)) {
370 qCWarning(
Log) <<
"Failed to initialize scanner?!";
374 const std::unique_ptr<void,
decltype(&yylex_destroy)> lexerCleanup(scanner, &yylex_destroy);
376 YY_BUFFER_STATE state;
377 state = yy_scan_bytes(openingHours + offset, size - offset, scanner);
378 if (yyparse(d.
data(), scanner)) {
379 if (d->m_restartPosition > 1 && d->m_restartPosition + offset < (
int)size) {
380 offset += d->m_restartPosition - 1;
381 d->m_initialRuleType = d->m_recoveryRuleType;
382 d->m_recoveryRuleType = Rule::NormalRule;
383 d->m_restartPosition = 0;
396 yy_delete_buffer(state, scanner);
397 }
while (offset > 0);
410 for (
const auto &rule : d->m_rules) {
412 switch (rule->m_ruleType) {
413 case Rule::NormalRule:
416 case Rule::AdditionalRule:
419 case Rule::FallbackRule:
422 case Rule::GuessRuleType:
427 ret += rule->toExpression();
436 return copy.normalizedExpression();
439QString OpeningHours::normalizedExpressionString()
const
446 d->m_latitude = latitude;
447 d->m_longitude = longitude;
451float OpeningHours::latitude()
const
453 return d->m_latitude;
456void OpeningHours::setLatitude(
float latitude)
458 d->m_latitude = latitude;
462float OpeningHours::longitude()
const
464 return d->m_longitude;
467void OpeningHours::setLongitude(
float longitude)
469 d->m_longitude = longitude;
473#ifndef KOPENINGHOURS_VALIDATOR_ONLY
474QString OpeningHours::region()
const
476 return d->m_region.regionCode();
481 d->m_region = HolidayCache::resolveRegion(region);
488 return d->m_timezone;
496QString OpeningHours::timeZoneId()
const
501void OpeningHours::setTimeZoneId(
const QString &tzId)
511#ifndef KOPENINGHOURS_VALIDATOR_ONLY
518 const auto alignedTime =
QDateTime(dt.
date(), {dt.time().hour(), dt.time().minute()});
521 for (
const auto &rule : d->m_rules) {
522 if (rule->state() == Interval::Closed) {
525 if (i.
isValid() && i.
contains(dt) && rule->m_ruleType == Rule::FallbackRule) {
528 auto res = rule->nextInterval(alignedTime, d.
data());
529 if (!res.interval.isValid()) {
532 if (i.
isValid() && res.mode == RuleResult::Override) {
533 if (res.interval.begin().isValid() && res.interval.begin().date() > alignedTime.date()) {
535 i.setBegin(alignedTime);
536 i.setEnd({alignedTime.date().addDays(1), {0, 0}});
537 i.setState(Interval::Closed),
547 if (rule->m_ruleType == Rule::FallbackRule) {
548 res.interval.setEnd(res.interval.hasOpenEnd() ? i.begin() : std::min(res.interval.end(), i.begin()));
550 i = i.
isValid() ? std::min(i, res.interval) : res.interval;
555 QDateTime closeEnd = i.begin(), closeBegin = i.end();
557 for (
const auto &rule : d->m_rules) {
558 if (rule->state() != Interval::Closed) {
561 const auto j = rule->nextInterval(i.begin().
isValid() ? i.begin() : alignedTime, d.
data()).interval;
566 if (j.contains(alignedTime)) {
567 if (closedInterval.
isValid()) {
569 closedInterval.setBegin(std::min(closedInterval.begin(), j.begin()));
570 closedInterval.setEnd(std::max(closedInterval.end(), j.end()));
574 }
else if (alignedTime < j.begin()) {
575 closeBegin = std::min(j.begin(), closeBegin);
576 }
else if (j.end() <= alignedTime) {
577 closeEnd = std::max(closeEnd, j.end());
580 if (closedInterval.
isValid()) {
583 i.setBegin(closeEnd);
584 i.setEnd(closeBegin);
593 i2.setState(Interval::Closed);
595 i2.setEnd(i.begin());
606 endDt = endDt.addSecs(3600);
618static Rule* openingHoursSpecToRule(
const QJsonObject &obj)
627 if (!opens.isValid() || !closes.isValid()) {
632 r->setState(State::Open);
635 r->m_timeSelector.reset(
new Timespan);
636 r->m_timeSelector->begin = { Time::NoEvent, opens.hour(), opens.minute() };
637 r->m_timeSelector->end = { Time::NoEvent, closes.hour(), closes.minute() };
641 if (validFrom.isValid() || validTo.isValid()) {
642 r->m_monthdaySelector.reset(
new MonthdayRange);
643 r->m_monthdaySelector->begin = { validFrom.year(), validFrom.month(), validFrom.day(), Date::FixedDate, { 0, 0, 0 } };
644 r->m_monthdaySelector->end = { validTo.year(), validTo.month(), validTo.day(), Date::FixedDate, { 0, 0, 0 } };
648 if (!weekday.isEmpty()) {
649 r->m_weekdaySelector.reset(
new WeekdayRange);
651 for (
const auto &d : {
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
"Sunday"}) {
653 r->m_weekdaySelector->beginDay = r->m_weekdaySelector->endDay = i;
670 }
else if (oh.isArray()) {
671 const auto ohA = oh.toArray();
673 for (
const auto &exprV : ohA) {
674 const auto exprS = exprV.toString();
675 if (exprS.isEmpty()) {
678 expr += (expr.
isEmpty() ?
"" :
"; ") + exprS.toUtf8();
683 std::vector<std::unique_ptr<Rule>> rules;
685 for (
const auto &ohsV : ohs) {
686 const auto r = openingHoursSpecToRule(ohsV.toObject());
688 rules.push_back(std::unique_ptr<Rule>(r));
692 for (
const auto &ohsV : sohs) {
693 const auto r = openingHoursSpecToRule(ohsV.toObject());
695 rules.push_back(std::unique_ptr<Rule>(r));
698 for (
auto &r : rules) {
699 result.d->m_rules.push_back(std::move(r));
702 result.d->validate();
706#include "moc_openinghours.cpp"
A time interval for which an opening hours expression has been evaluated.
bool isValid() const
Default constructed empty/invalid interval.
bool contains(const QDateTime &dt) const
Check if this interval contains dt.
bool intersects(const Interval &other) const
Checks whether this interval overlaps with other.
An OSM opening hours specification.
QByteArray simplifiedExpression() const
Returns a simplified OSM opening hours expression reconstructed from this object.
void setExpression(const QByteArray &openingHours, Modes modes=IntervalMode)
Parse OSM opening hours expression openingHours.
Q_INVOKABLE KOpeningHours::Interval interval(const QDateTime &dt) const
Returns the interval containing dt.
@ PointInTimeMode
Expressions that describe points in time.
@ IntervalMode
Expressions that describe time ranges.
static OpeningHours fromJsonLd(const QJsonObject &obj)
Convert opening hours in schema.org JSON-LD format.
void setRegion(QStringView region)
ISO 3166-2 region or ISO 316-1 country in which this expression should be evaluated.
void setTimeZone(const QTimeZone &tz)
Timezone in which this expression should be evaluated.
OpeningHours()
Create an empty/invalid instance.
Q_INVOKABLE KOpeningHours::Interval nextInterval(const KOpeningHours::Interval &interval) const
Returns the interval immediately following interval.
Q_INVOKABLE void setLocation(float latitude, float longitude)
Geographic coordinate at which this expression should be evaluated.
@ MissingLocation
evaluation requires location information and those aren't set
@ UnsupportedFeature
expression uses a feature that isn't implemented/supported (yet)
@ IncompatibleMode
expression mode doesn't match the expected mode
@ Null
no expression is set
@ SyntaxError
syntax error in the opening hours expression
@ MissingRegion
expression refers to public or school holidays, but that information is not available
@ NoError
there is no error, the expression is valid and can be used
char * toString(const EngineQuery &query)
KIOCORE_EXPORT TransferJob * get(const QUrl &url, LoadType reload=NoReload, JobFlags flags=DefaultFlags)
OSM opening hours parsing and evaluation.
const char * constData() const const
bool isEmpty() const const
qsizetype size() const const
QDate fromString(QStringView string, QStringView format, QCalendar cal)
bool isValid() const const
QJsonValue value(QLatin1StringView key) const const
QJsonArray toArray() const const
QString toString() const const
QString fromUtf8(QByteArrayView str)
QByteArray toUtf8() const const
QTime fromString(QStringView string, QStringView format)