KOpeningHours

openinghours.cpp
1 /*
2  SPDX-FileCopyrightText: 2020 Volker Krause <[email protected]>
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 
23 using namespace KOpeningHours;
24 
25 static 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 
40 void 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 
141 void 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 
202 void 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()) {
223  m_error = OpeningHours::MissingRegion;
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 
240 void 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 {
264  m_error = OpeningHours::SyntaxError;
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  {
275  m_error = OpeningHours::SyntaxError;
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()) {
283  m_error = OpeningHours::SyntaxError;
284  }
285  }
286 
287  m_ruleSeparatorRecovery = false;
288  m_rules.push_back(std::move(rule));
289 }
290 
291 void 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 
308 bool 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 
320 OpeningHours::OpeningHours(const QByteArray &openingHours, Modes modes)
321  : d(new OpeningHoursPrivate)
322 {
323  setExpression(openingHours.constData(), openingHours.size(), modes);
324 }
325 
326 OpeningHours::OpeningHours(const char *openingHours, std::size_t size, Modes modes)
327  : d(new OpeningHoursPrivate)
328 {
329  setExpression(openingHours, size, modes);
330 }
331 
332 
333 OpeningHours::OpeningHours(const OpeningHours&) = default;
335 OpeningHours::~OpeningHours() = default;
336 
337 OpeningHours& OpeningHours::operator=(const OpeningHours&) = default;
338 OpeningHours& OpeningHours::operator=(OpeningHours&&) = default;
339 
341 {
342  setExpression(openingHours.constData(), openingHours.size(), modes);
343 }
344 
345 void 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 
403 QByteArray 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 
439 QString OpeningHours::normalizedExpressionString() const
440 {
441  return QString::fromUtf8(normalizedExpression());
442 }
443 
444 void OpeningHours::setLocation(float latitude, float longitude)
445 {
446  d->m_latitude = latitude;
447  d->m_longitude = longitude;
448  d->validate();
449 }
450 
451 float OpeningHours::latitude() const
452 {
453  return d->m_latitude;
454 }
455 
456 void OpeningHours::setLatitude(float latitude)
457 {
458  d->m_latitude = latitude;
459  d->validate();
460 }
461 
462 float OpeningHours::longitude() const
463 {
464  return d->m_longitude;
465 }
466 
467 void OpeningHours::setLongitude(float longitude)
468 {
469  d->m_longitude = longitude;
470  d->validate();
471 }
472 
473 #ifndef KOPENINGHOURS_VALIDATOR_ONLY
474 QString 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 
486 QTimeZone OpeningHours::timeZone() const
487 {
488  return d->m_timezone;
489 }
490 
492 {
493  d->m_timezone = tz;
494 }
495 
496 QString OpeningHours::timeZoneId() const
497 {
498  return QString::fromUtf8(d->m_timezone.id());
499 }
500 
501 void OpeningHours::setTimeZoneId(const QString &tzId)
502 {
503  d->m_timezone = QTimeZone(tzId.toUtf8());
504 }
505 
506 OpeningHours::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 
618 static 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 }
QDateTime addSecs(qint64 s) const const
Q_INVOKABLE void setLocation(float latitude, float longitude)
Geographic coordinate at which this expression should be evaluated.
QString toString(const T &enumerator)
void setTimeZone(const QTimeZone &tz)
Timezone in which this expression should be evaluated.
QString fromUtf8(const char *str, int size)
QTime fromString(const QString &string, Qt::DateFormat format)
OpeningHours()
Create an empty/invalid instance.
@ MissingRegion
expression refers to public or school holidays, but that information is not available
Definition: openinghours.h:134
QString toString() const const
@ NoError
there is no error, the expression is valid and can be used
Definition: openinghours.h:132
void setRegion(QStringView region)
ISO 3166-2 region or ISO 316-1 country in which this expression should be evaluated.
@ SyntaxError
syntax error in the opening hours expression
Definition: openinghours.h:133
@ MissingLocation
evaluation requires location information and those aren't set
Definition: openinghours.h:135
QByteArray toUtf8() const const
bool contains(const QDateTime &dt) const
Check if this interval contains dt.
Definition: interval.cpp:108
@ IntervalMode
Expressions that describe time ranges.
Definition: openinghours.h:46
@ UnsupportedFeature
expression uses a feature that isn't implemented/supported (yet)
Definition: openinghours.h:137
QJsonValue value(const QString &key) const const
Q_INVOKABLE KOpeningHours::Interval nextInterval(const KOpeningHours::Interval &interval) const
Returns the interval immediately following interval.
bool isValid() const
Default constructed empty/invalid interval.
Definition: interval.cpp:60
QJsonArray toArray() const const
An OSM opening hours specification.
Definition: openinghours.h:32
QDate fromString(const QString &string, Qt::DateFormat format)
Q_INVOKABLE KOpeningHours::Interval interval(const QDateTime &dt) const
Returns the interval containing dt.
bool isEmpty() const const
const char * constData() const const
QDate date() const const
bool isValid() const const
QByteArray simplifiedExpression() const
Returns a simplified OSM opening hours expression reconstructed from this object.
@ Null
no expression is set
Definition: openinghours.h:131
@ IncompatibleMode
expression mode doesn't match the expected mode
Definition: openinghours.h:136
int size() const const
OSM opening hours parsing and evaluation.
Definition: display.h:14
@ PointInTimeMode
Expressions that describe points in time.
Definition: openinghours.h:47
static OpeningHours fromJsonLd(const QJsonObject &obj)
Convert opening hours in schema.org JSON-LD format.
bool intersects(const Interval &other) const
Checks whether this interval overlaps with other.
Definition: interval.cpp:49
A time interval for which an opening hours expression has been evaluated.
Definition: interval.h:24
void setExpression(const QByteArray &openingHours, Modes modes=IntervalMode)
Parse OSM opening hours expression openingHours.
This file is part of the KDE documentation.
Documentation copyright © 1996-2023 The KDE developers.
Generated on Mon May 8 2023 03:54:27 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.