KOpeningHours

openinghours.cpp
1/*
2 SPDX-FileCopyrightText: 2020 Volker Krause <vkrause@kde.org>
3
4 SPDX-License-Identifier: LGPL-2.0-or-later
5*/
6
7#include "openinghours.h"
8#include "openinghours_p.h"
9#include "openinghoursparser_p.h"
10#include "openinghoursscanner_p.h"
11#include "holidaycache_p.h"
12#include "interval.h"
13#include "rule_p.h"
14#include "logging.h"
15
16#include <QDateTime>
17#include <QJsonArray>
18#include <QJsonObject>
19#include <QTimeZone>
20
21#include <memory>
22
23using namespace KOpeningHours;
24
25static bool isWiderThan(Rule *lhs, Rule *rhs)
26{
27 if ((lhs->m_yearSelector && !rhs->m_yearSelector)) {
28 return true;
29 }
30 if (lhs->m_monthdaySelector && rhs->m_monthdaySelector) {
31 if (lhs->m_monthdaySelector->begin.year > 0 && rhs->m_monthdaySelector->end.year == 0) {
32 return true;
33 }
34 }
35
36 // this is far from handling all cases, expand as needed
37 return false;
38}
39
40void OpeningHoursPrivate::autocorrect()
41{
42 if (m_rules.size() <= 1 || m_error == OpeningHours::SyntaxError) {
43 return;
44 }
45
46 // find incomplete additional rules, and merge them with the preceding rule
47 // example: "Mo, We, Fr 06:30-21:30" becomes "Mo,We,Fr 06:30-21:30"
48 // this matters as those two variants have widely varying semantics, and often occur technically wrong in the wild
49 // 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"
50
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();
54
55 if (rule->hasComment() || prevRule->hasComment() || !prevRule->hasImplicitState()) {
56 continue;
57 }
58 const auto prevRuleSingleSelector = prevRule->selectorCount() == 1;
59 const auto curRuleSingleSelector = rule->selectorCount() == 1;
60
61 if (rule->m_ruleType == Rule::AdditionalRule) {
62 // the previous rule has no time selector, the current rule only has a weekday selector
63 // so we fold the two rules together
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));
78 }
79
80 // the current rule only has a time selector, so we append that to the previous rule
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));
85 }
86
87 // previous is a single weekday selector and current is a single time selector
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));
91 }
92
93 // previous is a single monthday selector
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));
101 }
102
103 // previous has no time selector and the current one is a misplaced 24/7 rule:
104 // convert the 24/7 to a 00:00-24:00 time selector
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));
110 }
111 } else if (rule->m_ruleType == Rule::NormalRule) {
112 // Previous rule has time and other selectors
113 // Current rule is only a time selector
114 // "Mo-Sa 12:00-15:00; 18:00-24:00" => "Mo-Sa 12:00-15:00,18:00-24:00"
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));
120 }
121
122 // Both rules have exactly the same selector apart from time
123 // Ex: "Mo-Sa 12:00-15:00; Mo-Sa 18:00-24:00" => "Mo-Sa 12:00-15:00,18:00-24:00"
124 // Obviously a bug, it was overwriting the 12:00-15:00 range.
125 // For now this only supports weekday selectors, could be extended
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
130 // slower than writing an operator==, but so much easier to write :)
131 && rule->m_weekdaySelector->toExpression() == prevRule->m_weekdaySelector->toExpression()
132 && rule->state() == prevRule->state()
133 ) {
134 appendSelector(prevRule->m_timeSelector.get(), std::move(rule->m_timeSelector));
135 it = std::prev(m_rules.erase(it));
136 }
137 }
138 }
139}
140
141void OpeningHoursPrivate::simplify()
142{
143 if (m_error == OpeningHours::SyntaxError || m_rules.empty()) {
144 return;
145 }
146
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();
150
151 if (rule->m_ruleType == Rule::AdditionalRule || rule->m_ruleType == Rule::NormalRule) {
152
153 auto hasNoHoliday = [](WeekdayRange *selector) {
154 return selector->holiday == WeekdayRange::NoHoliday
155 && !selector->lhsAndSelector;
156 };
157 // Both rules have the same time and a different weekday selector
158 // Mo 08:00-13:00; Tu 08:00-13:00 => Mo,Tu 08:00-13:00
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
165 ) {
166 // We could of course also turn Mo,Tu,We,Th into Mo-Th...
167 appendSelector(prevRule->m_weekdaySelector.get(), std::move(rule->m_weekdaySelector));
168 it = std::prev(m_rules.erase(it));
169 continue;
170 }
171 }
172
173 if (rule->m_ruleType == Rule::AdditionalRule) {
174 // Both rules have exactly the same selector apart from time
175 // Ex: "Mo 12:00-15:00, Mo 18:00-24:00" => "Mo 12:00-15:00,18:00-24:00"
176 // For now this only supports weekday selectors, could be extended
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
181 // slower than writing an operator==, but so much easier to write :)
182 && rule->m_weekdaySelector->toExpression() == prevRule->m_weekdaySelector->toExpression()
183 ) {
184 appendSelector(prevRule->m_timeSelector.get(), std::move(rule->m_timeSelector));
185 it = std::prev(m_rules.erase(it));
186 }
187 }
188 }
189
190 // Now try collapsing adjacent week days: Mo,Tu,We => Mo-We
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();
195 }
196 if (rule->m_monthdaySelector) {
197 rule->m_monthdaySelector->simplify();
198 }
199 }
200}
201
202void OpeningHoursPrivate::validate()
203{
204 if (m_error == OpeningHours::SyntaxError) {
205 return;
206 }
207 if (m_rules.empty()) {
208 m_error = OpeningHours::Null;
209 return;
210 }
211
212 int c = Capability::None;
213 for (const auto &rule : m_rules) {
214 c |= rule->requiredCapabilities();
215 }
216
217 if ((c & Capability::Location) && (std::isnan(m_latitude) || std::isnan(m_longitude))) {
219 return;
220 }
221#ifndef KOPENINGHOURS_VALIDATOR_ONLY
222 if (c & Capability::PublicHoliday && !m_region.isValid()) {
224 return;
225 }
226#endif
227 if (((c & Capability::PointInTime) && (m_modes & OpeningHours::PointInTimeMode) == 0)
228 || ((c & Capability::Interval) && (m_modes & OpeningHours::IntervalMode) == 0)) {
230 return;
231 }
232 if (c & (Capability::SchoolHoliday | Capability::NotImplemented | Capability::PointInTime)) {
234 return;
235 }
236
237 m_error = OpeningHours::NoError;
238}
239
240void OpeningHoursPrivate::addRule(Rule *parsedRule)
241{
242 std::unique_ptr<Rule> rule(parsedRule);
243
244 // discard empty rules
245 if (rule->isEmpty()) {
246 return;
247 }
248
249 if (m_initialRuleType != Rule::NormalRule && rule->m_ruleType == Rule::NormalRule) {
250 rule->m_ruleType = m_initialRuleType;
251 m_initialRuleType = Rule::NormalRule;
252 }
253
254 // error recovery after a missing rule separator
255 // only continue here if whatever we got is somewhat plausible
256 if (m_ruleSeparatorRecovery && !m_rules.empty()) {
257 if (rule->selectorCount() <= 1) {
258 // missing separator was actually between time selectors, not rules
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));
261 rule.reset();
262 return;
263 } else {
265 }
266 }
267
268 // error recovery in the middle of a wide-range selector
269 // the likely meaning is that the wide-range selectors should be merged, which we can only do if the first
270 // part is "wider" than the right hand side
271 if (m_rules.back()->hasWideRangeSelector() && rule->hasWideRangeSelector()
272 && !m_rules.back()->hasSmallRangeSelector() && rule->hasSmallRangeSelector()
273 && isWiderThan(rule.get(), m_rules.back().get()))
274 {
276 }
277
278 // error recovery in case of a wide range selector followed by two wrongly separated small range selectors
279 // the likely meaning here is that the wide range selector should apply to both small range selectors,
280 // but that cannot be modeled without duplicating the wide range selector
281 // therefore we consider such a case invalid, to be on the safe side
282 if (m_rules.back()->hasWideRangeSelector() && !rule->hasWideRangeSelector()) {
284 }
285 }
286
287 m_ruleSeparatorRecovery = false;
288 m_rules.push_back(std::move(rule));
289}
290
291void OpeningHoursPrivate::restartFrom(int pos, Rule::Type nextRuleType)
292{
293 m_restartPosition = pos;
294 if (nextRuleType == Rule::GuessRuleType) {
295 if (m_rules.empty()) {
296 m_recoveryRuleType = Rule::NormalRule;
297 } else {
298 // if autocorrect() could merge the previous rule, we assume that's the intended meaning
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;
302 }
303 } else {
304 m_recoveryRuleType = nextRuleType;
305 }
306}
307
308bool OpeningHoursPrivate::isRecovering() const
309{
310 return m_restartPosition > 0;
311}
312
313
315 : d(new OpeningHoursPrivate)
316{
317 d->m_error = OpeningHours::Null;
318}
319
320OpeningHours::OpeningHours(const QByteArray &openingHours, Modes modes)
321 : d(new OpeningHoursPrivate)
322{
323 setExpression(openingHours.constData(), openingHours.size(), modes);
324}
325
326OpeningHours::OpeningHours(const char *openingHours, std::size_t size, Modes modes)
327 : d(new OpeningHoursPrivate)
328{
329 setExpression(openingHours, size, modes);
330}
331
332
335OpeningHours::~OpeningHours() = default;
336
337OpeningHours& OpeningHours::operator=(const OpeningHours&) = default;
338OpeningHours& OpeningHours::operator=(OpeningHours&&) = default;
339
341{
342 setExpression(openingHours.constData(), openingHours.size(), modes);
343}
344
345void OpeningHours::setExpression(const char *openingHours, std::size_t size, Modes modes)
346{
347 d->m_modes = modes;
348
349 d->m_error = OpeningHours::Null;
350 d->m_rules.clear();
351 d->m_initialRuleType = Rule::NormalRule;
352 d->m_recoveryRuleType = Rule::NormalRule;
353 d->m_ruleSeparatorRecovery = false;
354
355 // trim trailing spaces
356 // the parser would handle most of this by itself, but fails if a trailing space would produce a trailing rule separator
357 // so it's easier to just clean this here
358 while (size > 0 && std::isspace(static_cast<unsigned char>(openingHours[size - 1]))) {
359 --size;
360 }
361 if (size == 0) {
362 return;
363 }
364
365 d->m_restartPosition = 0;
366 int offset = 0;
367 do {
368 yyscan_t scanner;
369 if (yylex_init(&scanner)) {
370 qCWarning(Log) << "Failed to initialize scanner?!";
371 d->m_error = SyntaxError;
372 return;
373 }
374 const std::unique_ptr<void, decltype(&yylex_destroy)> lexerCleanup(scanner, &yylex_destroy);
375
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;
384 } else {
385 d->m_error = SyntaxError;
386 return;
387 }
388 d->m_error = NoError;
389 } else {
390 if (d->m_error != SyntaxError) {
391 d->m_error = NoError;
392 }
393 offset = -1;
394 }
395
396 yy_delete_buffer(state, scanner);
397 } while (offset > 0);
398
399 d->autocorrect();
400 d->validate();
401}
402
403QByteArray OpeningHours::normalizedExpression() const
404{
405 if (d->m_error == SyntaxError) {
406 return {};
407 }
408
409 QByteArray ret;
410 for (const auto &rule : d->m_rules) {
411 if (!ret.isEmpty()) {
412 switch (rule->m_ruleType) {
413 case Rule::NormalRule:
414 ret += "; ";
415 break;
416 case Rule::AdditionalRule:
417 ret += ", ";
418 break;
419 case Rule::FallbackRule:
420 ret += " || ";
421 break;
422 case Rule::GuessRuleType:
423 Q_UNREACHABLE();
424 break;
425 }
426 }
427 ret += rule->toExpression();
428 }
429 return ret;
430}
431
433{
434 OpeningHours copy(normalizedExpression());
435 copy.d->simplify();
436 return copy.normalizedExpression();
437}
438
439QString OpeningHours::normalizedExpressionString() const
440{
441 return QString::fromUtf8(normalizedExpression());
442}
443
444void OpeningHours::setLocation(float latitude, float longitude)
445{
446 d->m_latitude = latitude;
447 d->m_longitude = longitude;
448 d->validate();
449}
450
451float OpeningHours::latitude() const
452{
453 return d->m_latitude;
454}
455
456void OpeningHours::setLatitude(float latitude)
457{
458 d->m_latitude = latitude;
459 d->validate();
460}
461
462float OpeningHours::longitude() const
463{
464 return d->m_longitude;
465}
466
467void OpeningHours::setLongitude(float longitude)
468{
469 d->m_longitude = longitude;
470 d->validate();
471}
472
473#ifndef KOPENINGHOURS_VALIDATOR_ONLY
474QString OpeningHours::region() const
475{
476 return d->m_region.regionCode();
477}
478
480{
481 d->m_region = HolidayCache::resolveRegion(region);
482 d->validate();
483}
484#endif
485
486QTimeZone OpeningHours::timeZone() const
487{
488 return d->m_timezone;
489}
490
492{
493 d->m_timezone = tz;
494}
495
496QString OpeningHours::timeZoneId() const
497{
498 return QString::fromUtf8(d->m_timezone.id());
499}
500
501void OpeningHours::setTimeZoneId(const QString &tzId)
502{
503 d->m_timezone = QTimeZone(tzId.toUtf8());
504}
505
506OpeningHours::Error OpeningHours::error() const
507{
508 return d->m_error;
509}
510
511#ifndef KOPENINGHOURS_VALIDATOR_ONLY
513{
514 if (d->m_error != NoError) {
515 return {};
516 }
517
518 const auto alignedTime = QDateTime(dt.date(), {dt.time().hour(), dt.time().minute()});
519 Interval i;
520 // first try to find the nearest open interval, and afterwards check closed rules
521 for (const auto &rule : d->m_rules) {
522 if (rule->state() == Interval::Closed) {
523 continue;
524 }
525 if (i.isValid() && i.contains(dt) && rule->m_ruleType == Rule::FallbackRule) {
526 continue;
527 }
528 auto res = rule->nextInterval(alignedTime, d.data());
529 if (!res.interval.isValid()) {
530 continue;
531 }
532 if (i.isValid() && res.mode == RuleResult::Override) {
533 if (res.interval.begin().isValid() && res.interval.begin().date() > alignedTime.date()) {
534 i = Interval();
535 i.setBegin(alignedTime);
536 i.setEnd({alignedTime.date().addDays(1), {0, 0}});
537 i.setState(Interval::Closed),
538 i.setComment({});
539 } else {
540 i = res.interval;
541 }
542 } else {
543 if (!i.isValid()) {
544 i = res.interval;
545 } else {
546 // fallback rule intervals needs to be capped to the next occurrence of one of its preceding rules
547 if (rule->m_ruleType == Rule::FallbackRule) {
548 res.interval.setEnd(res.interval.hasOpenEnd() ? i.begin() : std::min(res.interval.end(), i.begin()));
549 }
550 i = i.isValid() ? std::min(i, res.interval) : res.interval;
551 }
552 }
553 }
554
555 QDateTime closeEnd = i.begin(), closeBegin = i.end();
556 Interval closedInterval;
557 for (const auto &rule : d->m_rules) {
558 if (rule->state() != Interval::Closed) {
559 continue;
560 }
561 const auto j = rule->nextInterval(i.begin().isValid() ? i.begin() : alignedTime, d.data()).interval;
562 if (!j.isValid() || !i.intersects(j)) {
563 continue;
564 }
565
566 if (j.contains(alignedTime)) {
567 if (closedInterval.isValid()) {
568 // TODO we lose comment information here
569 closedInterval.setBegin(std::min(closedInterval.begin(), j.begin()));
570 closedInterval.setEnd(std::max(closedInterval.end(), j.end()));
571 } else {
572 closedInterval = j;
573 }
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());
578 }
579 }
580 if (closedInterval.isValid()) {
581 i = closedInterval;
582 } else {
583 i.setBegin(closeEnd);
584 i.setEnd(closeBegin);
585 }
586
587 // check if the resulting interval contains dt, otherwise create a synthetic fallback interval
588 if (!i.isValid() || i.contains(dt)) {
589 return i;
590 }
591
592 Interval i2;
593 i2.setState(Interval::Closed);
594 i2.setBegin(dt);
595 i2.setEnd(i.begin());
596 // TODO do we need to intersect this with closed rules as well?
597 return i2;
598}
599
601{
602 if (!interval.hasOpenEnd()) {
603 auto endDt = interval.end();
604 // ensure we move forward even on zero-length open-end intervals, otherwise we get stuck in a loop
605 if (interval.hasOpenEndTime() && interval.begin() == interval.end()) {
606 endDt = endDt.addSecs(3600);
607 }
608 auto i = this->interval(endDt);
609 if (i.begin() < interval.end() && i.end() > interval.end()) {
610 i.setBegin(interval.end());
611 }
612 return i;
613 }
614 return {};
615}
616#endif
617
618static Rule* openingHoursSpecToRule(const QJsonObject &obj)
619{
620 if (obj.value(QLatin1String("@type")).toString() != QLatin1String("OpeningHoursSpecification")) {
621 return nullptr;
622 }
623
624 const auto opens = QTime::fromString(obj.value(QLatin1String("opens")).toString());
625 const auto closes = QTime::fromString(obj.value(QLatin1String("closes")).toString());
626
627 if (!opens.isValid() || !closes.isValid()) {
628 return nullptr;
629 }
630
631 auto r = new Rule;
632 r->setState(State::Open);
633 // ### is name or description used for comments?
634
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() };
638
639 const auto validFrom = QDate::fromString(obj.value(QLatin1String("validFrom")).toString(), Qt::ISODate);
640 const auto validTo = QDate::fromString(obj.value(QLatin1String("validThrough")).toString(), Qt::ISODate);
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 } };
645 }
646
647 const auto weekday = obj.value(QLatin1String("dayOfWeek")).toString();
648 if (!weekday.isEmpty()) {
649 r->m_weekdaySelector.reset(new WeekdayRange);
650 int i = 1;
651 for (const auto &d : { "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"}) {
652 if (weekday.endsWith(QLatin1String(d))) {
653 r->m_weekdaySelector->beginDay = r->m_weekdaySelector->endDay = i;
654 break;
655 }
656 ++i;
657 }
658 }
659
660 return r;
661}
662
664{
665 OpeningHours result;
666
667 const auto oh = obj.value(QLatin1String("openingHours"));
668 if (oh.isString()) {
669 result = OpeningHours(oh.toString().toUtf8());
670 } else if (oh.isArray()) {
671 const auto ohA = oh.toArray();
672 QByteArray expr;
673 for (const auto &exprV : ohA) {
674 const auto exprS = exprV.toString();
675 if (exprS.isEmpty()) {
676 continue;
677 }
678 expr += (expr.isEmpty() ? "" : "; ") + exprS.toUtf8();
679 }
680 result = OpeningHours(expr);
681 }
682
683 std::vector<std::unique_ptr<Rule>> rules;
684 const auto ohs = obj.value(QLatin1String("openingHoursSpecification")).toArray();
685 for (const auto &ohsV : ohs) {
686 const auto r = openingHoursSpecToRule(ohsV.toObject());
687 if (r) {
688 rules.push_back(std::unique_ptr<Rule>(r));
689 }
690 }
691 const auto sohs = obj.value(QLatin1String("specialOpeningHoursSpecification")).toArray();
692 for (const auto &ohsV : sohs) {
693 const auto r = openingHoursSpecToRule(ohsV.toObject());
694 if (r) {
695 rules.push_back(std::unique_ptr<Rule>(r));
696 }
697 }
698 for (auto &r : rules) {
699 result.d->m_rules.push_back(std::move(r));
700 }
701
702 result.d->validate();
703 return result;
704}
705
706#include "moc_openinghours.cpp"
A time interval for which an opening hours expression has been evaluated.
Definition interval.h:25
bool isValid() const
Default constructed empty/invalid interval.
Definition interval.cpp:60
bool contains(const QDateTime &dt) const
Check if this interval contains dt.
Definition interval.cpp:108
bool intersects(const Interval &other) const
Checks whether this interval overlaps with other.
Definition interval.cpp:49
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.
Definition display.h:16
const char * constData() const const
bool isEmpty() const const
qsizetype size() const const
QDate fromString(QStringView string, QStringView format, QCalendar cal)
QDate date() const const
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)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri Nov 8 2024 11:58:07 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.