openinghours.cpp 23.8 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
/*
    SPDX-FileCopyrightText: 2020 Volker Krause <vkrause@kde.org>

    SPDX-License-Identifier: LGPL-2.0-or-later
*/

#include "openinghours.h"
#include "openinghours_p.h"
#include "openinghoursparser_p.h"
#include "openinghoursscanner_p.h"
11
#include "holidaycache_p.h"
12
#include "interval.h"
13
#include "rule_p.h"
14
#include "logging.h"
15
#include "consecutiveaccumulator_p.h"
16

17
#include <QDateTime>
18
19
#include <QJsonArray>
#include <QJsonObject>
20
#include <QTimeZone>
21

22
23
#include <memory>

24
25
using namespace KOpeningHours;

26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
static bool isWiderThan(Rule *lhs, Rule *rhs)
{
    if ((lhs->m_yearSelector && !rhs->m_yearSelector)) {
        return true;
    }
    if (lhs->m_monthdaySelector && rhs->m_monthdaySelector) {
        if (lhs->m_monthdaySelector->begin.year > 0 && rhs->m_monthdaySelector->end.year == 0) {
            return true;
        }
    }

    // this is far from handling all cases, expand as needed
    return false;
}

41
42
void OpeningHoursPrivate::autocorrect()
{
43
    if (m_rules.size() <= 1 || m_error == OpeningHours::SyntaxError) {
44
45
46
47
48
49
        return;
    }

    // find incomplete additional rules, and merge them with the preceding rule
    // example: "Mo, We, Fr 06:30-21:30" becomes "Mo,We,Fr 06:30-21:30"
    // this matters as those two variants have widely varying semantics, and often occur technically wrong in the wild
50
    // 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"
51
52
53
54
55

    for (auto it = std::next(m_rules.begin()); it != m_rules.end(); ++it) {
        auto rule = (*it).get();
        auto prevRule = (*(std::prev(it))).get();

56
        if (rule->hasComment() || prevRule->hasComment() || !prevRule->hasImplicitState()) {
57
58
            continue;
        }
59
60
        const auto prevRuleSingleSelector = prevRule->selectorCount() == 1;
        const auto curRuleSingleSelector = rule->selectorCount() == 1;
61

62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
        if (rule->m_ruleType == Rule::AdditionalRule) {
            // the previous rule has no time selector, the current rule only has a weekday selector
            // so we fold the two rules together
            if (!prevRule->m_timeSelector && prevRule->m_weekdaySelector && rule->m_weekdaySelector && !rule->hasWideRangeSelector()) {
                auto tmp = std::move(rule->m_weekdaySelector);
                rule->m_weekdaySelector = std::move(prevRule->m_weekdaySelector);
                rule->m_weekSelector = std::move(prevRule->m_weekSelector);
                rule->m_monthdaySelector = std::move(prevRule->m_monthdaySelector);
                rule->m_yearSelector = std::move(prevRule->m_yearSelector);
                rule->m_colonAfterWideRangeSelector = prevRule->m_colonAfterWideRangeSelector;
                auto *selector = rule->m_weekdaySelector.get();
                while (selector->rhsAndSelector)
                    selector = selector->rhsAndSelector.get();
                appendSelector(selector, std::move(tmp));
                rule->m_ruleType = prevRule->m_ruleType;
                std::swap(*it, *std::prev(it));
                it = std::prev(m_rules.erase(it));
            }

            // the current rule only has a time selector, so we append that to the previous rule
            else if (curRuleSingleSelector && rule->m_timeSelector && prevRule->m_timeSelector) {
                appendSelector(prevRule->m_timeSelector.get(), std::move(rule->m_timeSelector));
                it = std::prev(m_rules.erase(it));
            }

            // previous is a single monthday selector
            else if (rule->m_monthdaySelector && prevRuleSingleSelector && prevRule->m_monthdaySelector && !isWiderThan(prevRule, rule)) {
                auto tmp = std::move(rule->m_monthdaySelector);
                rule->m_monthdaySelector = std::move(prevRule->m_monthdaySelector);
                appendSelector(rule->m_monthdaySelector.get(), std::move(tmp));
                rule->m_ruleType = prevRule->m_ruleType;
                std::swap(*it, *std::prev(it));
                it = std::prev(m_rules.erase(it));
            }
        } else if (rule->m_ruleType == Rule::NormalRule) {
            // Previous rule has time and other selectors
            // Current rule is only a time selector
            // "Mo-Sa 12:00-15:00; 18:00-24:00" => "Mo-Sa 12:00-15:00,18:00-24:00"
            if (curRuleSingleSelector && rule->m_timeSelector
                    && prevRule->selectorCount() > 1 && prevRule->m_timeSelector) {
                appendSelector(prevRule->m_timeSelector.get(), std::move(rule->m_timeSelector));
                it = std::prev(m_rules.erase(it));
            }

            // Both rules have exactly the same selector apart from time
107
108
            // Ex: "Mo-Sa 12:00-15:00; Mo-Sa 18:00-24:00" => "Mo-Sa 12:00-15:00,18:00-24:00"
            // Obviously a bug, it was overwriting the 12:00-15:00 range.
109
            // For now this only supports weekday selectors, could be extended
110
111
112
            else if (rule->selectorCount() == prevRule->selectorCount()
                     && rule->m_timeSelector && prevRule->m_timeSelector
                     && !rule->hasComment() && !prevRule->hasComment()
113
                     && rule->selectorCount() == 2 && rule->m_weekdaySelector && prevRule->m_weekdaySelector
114
115
116
117
118
119
                     // slower than writing an operator==, but so much easier to write :)
                     && rule->m_weekdaySelector->toExpression() == prevRule->m_weekdaySelector->toExpression()
                     ) {
                appendSelector(prevRule->m_timeSelector.get(), std::move(rule->m_timeSelector));
                it = std::prev(m_rules.erase(it));
            }
120
        }
121
    }
David Faure's avatar
David Faure committed
122
123
124
125
}

void OpeningHoursPrivate::simplify()
{
126
    if (m_error == OpeningHours::SyntaxError || m_rules.empty()) {
David Faure's avatar
David Faure committed
127
128
        return;
    }
129

David Faure's avatar
David Faure committed
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
    for (auto it = std::next(m_rules.begin()); it != m_rules.end(); ++it) {
        auto rule = (*it).get();
        auto prevRule = (*(std::prev(it))).get();

        if (rule->m_ruleType == Rule::AdditionalRule || rule->m_ruleType == Rule::NormalRule) {

            auto hasNoHoliday = [](WeekdayRange *selector) {
                return selector->holiday == WeekdayRange::NoHoliday
                        && !selector->lhsAndSelector;
            };
            // Both rules have the same time and a different weekday selector
            // Mo 08:00-13:00; Tu 08:00-13:00 => Mo,Tu 08:00-13:00
            if (rule->selectorCount() == prevRule->selectorCount()
                    && rule->m_timeSelector && prevRule->m_timeSelector
                    && rule->selectorCount() == 2 && rule->m_weekdaySelector && prevRule->m_weekdaySelector
                    && hasNoHoliday(rule->m_weekdaySelector.get())
                    && hasNoHoliday(prevRule->m_weekdaySelector.get())
                    && *rule->m_timeSelector == *prevRule->m_timeSelector
                    ) {
                // We could of course also turn Mo,Tu,We,Th into Mo-Th...
                appendSelector(prevRule->m_weekdaySelector.get(), std::move(rule->m_weekdaySelector));
                it = std::prev(m_rules.erase(it));
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
                continue;
            }
        }

        if (rule->m_ruleType == Rule::AdditionalRule) {
            // Both rules have exactly the same selector apart from time
            // Ex: "Mo 12:00-15:00, Mo 18:00-24:00" => "Mo 12:00-15:00,18:00-24:00"
            // For now this only supports weekday selectors, could be extended
            if (rule->selectorCount() == prevRule->selectorCount()
                    && rule->m_timeSelector && prevRule->m_timeSelector
                    && !rule->hasComment() && !prevRule->hasComment()
                    && rule->selectorCount() == 2 && rule->m_weekdaySelector && prevRule->m_weekdaySelector
                    // slower than writing an operator==, but so much easier to write :)
                    && rule->m_weekdaySelector->toExpression() == prevRule->m_weekdaySelector->toExpression()
                    ) {
                appendSelector(prevRule->m_timeSelector.get(), std::move(rule->m_timeSelector));
                it = std::prev(m_rules.erase(it));
David Faure's avatar
David Faure committed
169
170
171
            }
        }
    }
172
173
174
175
176
177
178
179

    // Now try collapsing adjacent week days: Mo,Tu,We => Mo-We
    for (auto it = m_rules.begin(); it != m_rules.end(); ++it) {
        auto rule = (*it).get();
        if (rule->m_weekdaySelector) {
            rule->m_weekdaySelector->simplify();
        }
    }
180
181
}

Volker Krause's avatar
Volker Krause committed
182
183
184
185
186
void OpeningHoursPrivate::validate()
{
    if (m_error == OpeningHours::SyntaxError) {
        return;
    }
187
188
189
190
    if (m_rules.empty()) {
        m_error = OpeningHours::Null;
        return;
    }
Volker Krause's avatar
Volker Krause committed
191
192
193
194
195
196
197
198
199
200

    int c = Capability::None;
    for (const auto &rule : m_rules) {
        c |= rule->requiredCapabilities();
    }

    if ((c & Capability::Location) && (std::isnan(m_latitude) || std::isnan(m_longitude))) {
        m_error = OpeningHours::MissingLocation;
        return;
    }
201
#ifndef KOPENINGHOURS_VALIDATOR_ONLY
202
    if (c & Capability::PublicHoliday && !m_region.isValid()) {
Volker Krause's avatar
Volker Krause committed
203
204
205
        m_error = OpeningHours::MissingRegion;
        return;
    }
206
#endif
207
208
209
210
211
212
    if (((c & Capability::PointInTime) && (m_modes & OpeningHours::PointInTimeMode) == 0)
     || ((c & Capability::Interval) && (m_modes & OpeningHours::IntervalMode) == 0)) {
        m_error = OpeningHours::IncompatibleMode;
        return;
    }
    if (c & (Capability::SchoolHoliday | Capability::NotImplemented | Capability::PointInTime)) {
Volker Krause's avatar
Volker Krause committed
213
214
215
216
217
218
219
        m_error = OpeningHours::UnsupportedFeature;
        return;
    }

    m_error = OpeningHours::NoError;
}

220
void OpeningHoursPrivate::addRule(Rule *parsedRule)
Volker Krause's avatar
Volker Krause committed
221
{
222
223
224
225
226
227
228
    std::unique_ptr<Rule> rule(parsedRule);

    // discard empty rules
    if (rule->isEmpty()) {
        return;
    }

229
230
231
232
    if (m_initialRuleType != Rule::NormalRule && rule->m_ruleType == Rule::NormalRule) {
        rule->m_ruleType = m_initialRuleType;
        m_initialRuleType = Rule::NormalRule;
    }
233
234
235

    // error recovery after a missing rule separator
    // only continue here if whatever we got is somewhat plausible
236
237
    if (m_ruleSeparatorRecovery && !m_rules.empty()) {
        if (rule->selectorCount() <= 1) {
238
239
240
241
242
243
244
245
246
247
            // missing separator was actually between time selectors, not rules
            if (m_rules.back()->m_timeSelector && rule->m_timeSelector && m_rules.back()->state() == rule->state()) {
                appendSelector(m_rules.back()->m_timeSelector.get(), std::move(rule->m_timeSelector));
                rule.reset();
                return;
            } else {
                m_error = OpeningHours::SyntaxError;
            }
        }

248
249
250
251
252
253
254
255
256
257
        // error recovery in the middle of a wide-range selector
        // the likely meaning is that the wide-range selectors should be merged, which we can only do if the first
        // part is "wider" than the right hand side
        if (m_rules.back()->hasWideRangeSelector() && rule->hasWideRangeSelector()
            && !m_rules.back()->hasSmallRangeSelector() && rule->hasSmallRangeSelector()
            && isWiderThan(rule.get(), m_rules.back().get()))
        {
            m_error = OpeningHours::SyntaxError;
        }

258
259
260
261
        // error recovery in case of a wide range selector followed by two wrongly separated small range selectors
        // the likely meaning here is that the wide range selector should apply to both small range selectors,
        // but that cannot be modeled without duplicating the wide range selector
        // therefore we consider such a case invalid, to be on the safe side
262
        if (m_rules.back()->hasWideRangeSelector() && !rule->hasWideRangeSelector()) {
263
264
265
266
            m_error = OpeningHours::SyntaxError;
        }
    }

267
    m_ruleSeparatorRecovery = false;
268
    m_rules.push_back(std::move(rule));
Volker Krause's avatar
Volker Krause committed
269
270
}

271
272
273
void OpeningHoursPrivate::restartFrom(int pos, Rule::Type nextRuleType)
{
    m_restartPosition = pos;
274
275
276
277
278
279
280
281
282
283
284
285
    if (nextRuleType == Rule::GuessRuleType) {
        if (m_rules.empty()) {
            m_recoveryRuleType = Rule::NormalRule;
        } else {
            // if autocorrect() could merge the previous rule, we assume that's the intended meaning
            const auto &prev = m_rules.back();
            const auto couldBeMerged = prev->selectorCount() == 1 && !prev->hasComment() && prev->hasImplicitState();
            m_recoveryRuleType = couldBeMerged ? Rule::AdditionalRule : Rule::NormalRule;
        }
    } else {
        m_recoveryRuleType = nextRuleType;
    }
286
287
}

288
289
290
291
292
bool OpeningHoursPrivate::isRecovering() const
{
    return m_restartPosition > 0;
}

Volker Krause's avatar
Volker Krause committed
293

294
295
296
OpeningHours::OpeningHours()
    : d(new OpeningHoursPrivate)
{
297
    d->m_error = OpeningHours::Null;
298
299
}

300
OpeningHours::OpeningHours(const QByteArray &openingHours, Modes modes)
301
    : d(new OpeningHoursPrivate)
302
{
303
    setExpression(openingHours.constData(), openingHours.size(), modes);
304
305
}

306
307
308
309
310
311
312
OpeningHours::OpeningHours(const char *openingHours, std::size_t size, Modes modes)
    : d(new OpeningHoursPrivate)
{
    setExpression(openingHours, size, modes);
}


313
314
315
316
317
318
319
320
OpeningHours::OpeningHours(const OpeningHours&) = default;
OpeningHours::OpeningHours(OpeningHours&&) = default;
OpeningHours::~OpeningHours() = default;

OpeningHours& OpeningHours::operator=(const OpeningHours&) = default;
OpeningHours& OpeningHours::operator=(OpeningHours&&) = default;

void OpeningHours::setExpression(const QByteArray &openingHours, OpeningHours::Modes modes)
321
322
323
324
325
{
    setExpression(openingHours.constData(), openingHours.size(), modes);
}

void OpeningHours::setExpression(const char *openingHours, std::size_t size, Modes modes)
326
{
327
328
    d->m_modes = modes;

329
330
    d->m_error = OpeningHours::Null;
    d->m_rules.clear();
331
332
333
    d->m_initialRuleType = Rule::NormalRule;
    d->m_recoveryRuleType = Rule::NormalRule;
    d->m_ruleSeparatorRecovery = false;
334

335
336
337
338
339
340
    // trim trailing spaces
    // the parser would handle most of this by itself, but fails if a trailing space would produce a trailing rule separator
    // so it's easier to just clean this here
    while (size > 0 && std::isspace(openingHours[size - 1])) {
        --size;
    }
341
342
343
    if (size == 0) {
        return;
    }
344

345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
    d->m_restartPosition = 0;
    int offset = 0;
    do {
        yyscan_t scanner;
        if (yylex_init(&scanner)) {
            qCWarning(Log) << "Failed to initialize scanner?!";
            d->m_error = SyntaxError;
            return;
        }
        const std::unique_ptr<void, decltype(&yylex_destroy)> lexerCleanup(scanner, &yylex_destroy);

        YY_BUFFER_STATE state;
        state = yy_scan_bytes(openingHours + offset, size - offset, scanner);
        if (yyparse(d.data(), scanner)) {
            if (d->m_restartPosition > 1 && d->m_restartPosition + offset < (int)size) {
                offset += d->m_restartPosition - 1;
                d->m_initialRuleType = d->m_recoveryRuleType;
                d->m_recoveryRuleType = Rule::NormalRule;
363
                d->m_restartPosition = 0;
364
365
366
367
368
            } else {
                d->m_error = SyntaxError;
                return;
            }
            d->m_error = NoError;
369
370
371
372
        } else {
            if (d->m_error != SyntaxError) {
                d->m_error = NoError;
            }
373
374
            offset = -1;
        }
375

376
377
        yy_delete_buffer(state, scanner);
    } while (offset > 0);
378

379
    d->autocorrect();
Volker Krause's avatar
Volker Krause committed
380
    d->validate();
381
}
382

383
384
385
386
387
388
389
390
391
QByteArray OpeningHours::normalizedExpression() const
{
    if (d->m_error == SyntaxError) {
        return {};
    }

    QByteArray ret;
    for (const auto &rule : d->m_rules) {
        if (!ret.isEmpty()) {
Volker Krause's avatar
Volker Krause committed
392
393
394
395
396
397
398
399
400
401
            switch (rule->m_ruleType) {
                case Rule::NormalRule:
                    ret += "; ";
                    break;
                case Rule::AdditionalRule:
                    ret += ", ";
                    break;
                case Rule::FallbackRule:
                    ret += " || ";
                    break;
402
403
404
                case Rule::GuessRuleType:
                    Q_UNREACHABLE();
                    break;
Volker Krause's avatar
Volker Krause committed
405
            }
406
        }
407
        ret += rule->toExpression();
408
409
410
411
    }
    return ret;
}

David Faure's avatar
David Faure committed
412
413
414
415
416
417
QByteArray OpeningHours::simplifiedExpression() const
{
    d->simplify();
    return normalizedExpression();
}

418
419
420
421
422
QString OpeningHours::normalizedExpressionString() const
{
    return QString::fromUtf8(normalizedExpression());
}

423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
void OpeningHours::setLocation(float latitude, float longitude)
{
    d->m_latitude = latitude;
    d->m_longitude = longitude;
    d->validate();
}

float OpeningHours::latitude() const
{
    return d->m_latitude;
}

void OpeningHours::setLatitude(float latitude)
{
    d->m_latitude = latitude;
    d->validate();
}

float OpeningHours::longitude() const
{
    return d->m_longitude;
}

void OpeningHours::setLongitude(float longitude)
{
    d->m_longitude = longitude;
    d->validate();
}

452
#ifndef KOPENINGHOURS_VALIDATOR_ONLY
453
454
455
456
457
QString OpeningHours::region() const
{
    return d->m_region.regionCode();
}

458
void OpeningHours::setRegion(QStringView region)
459
{
460
    d->m_region = HolidayCache::resolveRegion(region);
461
462
    d->validate();
}
463
#endif
464

465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
QTimeZone OpeningHours::timeZone() const
{
    return d->m_timezone;
}

void OpeningHours::setTimeZone(const QTimeZone &tz)
{
    d->m_timezone = tz;
}

QString OpeningHours::timeZoneId() const
{
    return QString::fromUtf8(d->m_timezone.id());
}

void OpeningHours::setTimeZoneId(const QString &tzId)
{
    d->m_timezone = QTimeZone(tzId.toUtf8());
}

485
486
487
488
OpeningHours::Error OpeningHours::error() const
{
    return d->m_error;
}
489

490
#ifndef KOPENINGHOURS_VALIDATOR_ONLY
491
492
Interval OpeningHours::interval(const QDateTime &dt) const
{
493
494
495
496
    if (d->m_error != NoError) {
        return {};
    }

497
    const auto alignedTime = QDateTime(dt.date(), {dt.time().hour(), dt.time().minute()});
498
    Interval i;
499
    // first try to find the nearest open interval, and afterwards check closed rules
500
    for (const auto &rule : d->m_rules) {
501
        if (rule->state() == Interval::Closed) {
502
503
            continue;
        }
Volker Krause's avatar
Volker Krause committed
504
505
506
507
        if (i.isValid() && i.contains(dt) && rule->m_ruleType == Rule::FallbackRule) {
            continue;
        }
        auto res = rule->nextInterval(alignedTime, d.data());
508
        if (!res.interval.isValid()) {
509
510
            continue;
        }
511
512
513
514
515
516
517
518
519
520
521
        if (i.isValid() && res.mode == RuleResult::Override) {
            if (res.interval.begin().isValid() && res.interval.begin().date() > alignedTime.date()) {
                i = Interval();
                i.setBegin(alignedTime);
                i.setEnd({alignedTime.date().addDays(1), {0, 0}});
                i.setState(Interval::Closed),
                i.setComment({});
            } else {
                i = res.interval;
            }
        } else {
Volker Krause's avatar
Volker Krause committed
522
523
524
525
526
527
528
529
530
            if (!i.isValid()) {
                i = res.interval;
            } else {
                // fallback rule intervals needs to be capped to the next occurrence of one of its preceding rules
                if (rule->m_ruleType == Rule::FallbackRule) {
                    res.interval.setEnd(res.interval.hasOpenEnd() ? i.begin() : std::min(res.interval.end(), i.begin()));
                }
                i = i.isValid() ? std::min(i, res.interval) : res.interval;
            }
531
        }
532
    }
533

534
535
    QDateTime closeEnd = i.begin(), closeBegin = i.end();
    Interval closedInterval;
536
    for (const auto &rule : d->m_rules) {
537
        if (rule->state() != Interval::Closed) {
538
539
            continue;
        }
540
        const auto j = rule->nextInterval(i.begin(), d.data()).interval;
541
542
543
544
545
        if (!j.isValid() || !i.intersects(j)) {
            continue;
        }

        if (j.contains(alignedTime)) {
546
547
548
549
550
551
552
553
554
555
556
            if (closedInterval.isValid()) {
                // TODO we lose comment information here
                closedInterval.setBegin(std::min(closedInterval.begin(), j.begin()));
                closedInterval.setEnd(std::max(closedInterval.end(), j.end()));
            } else {
                closedInterval = j;
            }
        } else if (alignedTime < j.begin()) {
            closeBegin = std::min(j.begin(), closeBegin);
        } else if (j.end() <= alignedTime) {
            closeEnd = std::max(closeEnd, j.end());
557
558
        }
    }
559
560
561
562
563
564
    if (closedInterval.isValid()) {
        i = closedInterval;
    } else {
        i.setBegin(closeEnd);
        i.setEnd(closeBegin);
    }
565

566
    // check if the resulting interval contains dt, otherwise create a synthetic fallback interval
567
568
569
570
571
    if (!i.isValid() || i.contains(dt)) {
        return i;
    }

    Interval i2;
Volker Krause's avatar
Volker Krause committed
572
    i2.setState(Interval::Closed);
573
574
    i2.setBegin(dt);
    i2.setEnd(i.begin());
575
    // TODO do we need to intersect this with closed rules as well?
576
    return i2;
577
}
578
579
580

Interval OpeningHours::nextInterval(const Interval &interval) const
{
581
    if (!interval.hasOpenEnd()) {
582
583
584
585
586
587
        auto endDt = interval.end();
        // ensure we move forward even on zero-length open-end intervals, otherwise we get stuck in a loop
        if (interval.hasOpenEndTime() && interval.begin() == interval.end()) {
            endDt = endDt.addSecs(3600);
        }
        auto i = this->interval(endDt);
588
589
590
591
        if (i.begin() < interval.end() && i.end() > interval.end()) {
            i.setBegin(interval.end());
        }
        return i;
592
593
594
    }
    return {};
}
595
#endif
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610

static Rule* openingHoursSpecToRule(const QJsonObject &obj)
{
    if (obj.value(QLatin1String("@type")).toString() != QLatin1String("OpeningHoursSpecification")) {
        return nullptr;
    }

    const auto opens = QTime::fromString(obj.value(QLatin1String("opens")).toString());
    const auto closes = QTime::fromString(obj.value(QLatin1String("closes")).toString());

    if (!opens.isValid() || !closes.isValid()) {
        return nullptr;
    }

    auto r = new Rule;
611
    r->setState(State::Open);
612
613
614
615
616
617
618
619
620
621
    // ### is name or description used for comments?

    r->m_timeSelector.reset(new Timespan);
    r->m_timeSelector->begin = { Time::NoEvent, opens.hour(), opens.minute() };
    r->m_timeSelector->end = { Time::NoEvent, closes.hour(), closes.minute() };

    const auto validFrom = QDate::fromString(obj.value(QLatin1String("validFrom")).toString(), Qt::ISODate);
    const auto validTo = QDate::fromString(obj.value(QLatin1String("validThrough")).toString(), Qt::ISODate);
    if (validFrom.isValid() || validTo.isValid()) {
        r->m_monthdaySelector.reset(new MonthdayRange);
622
623
        r->m_monthdaySelector->begin = { validFrom.year(), validFrom.month(), validFrom.day(), Date::FixedDate, { 0, 0, 0 } };
        r->m_monthdaySelector->end = { validTo.year(), validTo.month(), validTo.day(), Date::FixedDate, { 0, 0, 0 } };
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
    }

    const auto weekday = obj.value(QLatin1String("dayOfWeek")).toString();
    if (!weekday.isEmpty()) {
        r->m_weekdaySelector.reset(new WeekdayRange);
        int i = 1;
        for (const auto &d : { "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"}) {
            if (weekday.endsWith(QLatin1String(d))) {
                r->m_weekdaySelector->beginDay = r->m_weekdaySelector->endDay = i;
                break;
            }
            ++i;
        }
    }

    return r;
}

OpeningHours OpeningHours::fromJsonLd(const QJsonObject &obj)
{
    OpeningHours result;

    const auto oh = obj.value(QLatin1String("openingHours"));
    if (oh.isString()) {
        result = OpeningHours(oh.toString().toUtf8());
    } else if (oh.isArray()) {
        const auto ohA = oh.toArray();
        QByteArray expr;
652
        for (const auto &exprV : ohA) {
653
654
655
656
657
658
659
660
661
662
            const auto exprS = exprV.toString();
            if (exprS.isEmpty()) {
                continue;
            }
            expr += (expr.isEmpty() ? "" : "; ") + exprS.toUtf8();
        }
        result = OpeningHours(expr);
    }

    std::vector<std::unique_ptr<Rule>> rules;
663
664
    const auto ohs = obj.value(QLatin1String("openingHoursSpecification")).toArray();
    for (const auto &ohsV : ohs) {
665
666
667
668
669
        const auto r = openingHoursSpecToRule(ohsV.toObject());
        if (r) {
            rules.push_back(std::unique_ptr<Rule>(r));
        }
    }
670
671
    const auto sohs = obj.value(QLatin1String("specialOpeningHoursSpecification")).toArray();
    for (const auto &ohsV : sohs) {
672
673
674
675
676
        const auto r = openingHoursSpecToRule(ohsV.toObject());
        if (r) {
            rules.push_back(std::unique_ptr<Rule>(r));
        }
    }
677
678
    for (auto &r : rules) {
        result.d->m_rules.push_back(std::move(r));
679
680
681
682
683
    }

    result.d->validate();
    return result;
}