KOpeningHours

selectors.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 "selectors_p.h"
8#include "logging.h"
9#include "openinghours_p.h"
10
11#include <cstdlib>
12#include <cassert>
13
14using namespace KOpeningHours;
15
16static QByteArray twoDigits(int n)
17{
19 if (ret.size() < 2) {
20 ret.prepend('0');
21 }
22 return ret;
23}
24
25bool Time::isValid(Time t)
26{
27 return t.hour >= 0 && t.hour <= 48 && t.minute >= 0 && t.minute < 60;
28}
29
30void Time::convertFromAm(Time &t)
31{
32 if (t.hour == 12) {
33 t.hour = 0;
34 }
35}
36
37void Time::convertFromPm(Time &t)
38{
39 if (t.hour < 12) {
40 t.hour += 12;
41 }
42}
43
44Time Time::parse(const char *begin, const char *end)
45{
46 Time t{ Time::NoEvent, 0, 0 };
47
48 char *it = nullptr;
49 t.hour = std::strtol(begin, &it, 10);
50
51 for (const auto sep : {':', 'h', 'H'}) {
52 if (*it == sep) {
53 ++it;
54 break;
55 }
56 }
57 if (it != end) {
58 t.minute = std::strtol(it, nullptr, 10);
59 }
60 return t;
61}
62
63QByteArray Time::toExpression(bool end) const
64{
65 QByteArray expr;
66 switch (event) {
67 case Time::NoEvent:
68 if (hour % 24 == 0 && minute == 0 && end)
69 return "24:00";
70 return twoDigits(hour) + ':' + twoDigits(minute);
71 case Time::Dawn:
72 expr = "dawn";
73 break;
74 case Time::Sunrise:
75 expr = "sunrise";
76 break;
77 case Time::Dusk:
78 expr = "dusk";
79 break;
80 case Time::Sunset:
81 expr = "sunset";
82 break;
83 }
84 const int minutes = hour * 60 + minute;
85 if (minutes != 0) {
86 const QByteArray hhmm = twoDigits(qAbs(hour)) + ':' + twoDigits(qAbs(minute));
87 expr = '(' + expr + (minutes > 0 ? '+' : '-') + hhmm + ')';
88 }
89 return expr;
90}
91
92int Timespan::requiredCapabilities() const
93{
94 int c = Capability::None;
95 if ((interval > 0 || pointInTime) && !openEnd) {
96 c |= Capability::PointInTime;
97 } else {
98 c |= Capability::Interval;
99 }
100 if (begin.event != Time::NoEvent || end.event != Time::NoEvent) {
101 c |= Capability::Location;
102 }
103 return next ? (next->requiredCapabilities() | c) : c;
104}
105
106static QByteArray intervalToExpression(int minutes)
107{
108 if (minutes < 60) {
109 return twoDigits(minutes);
110 } else {
111 const int hours = minutes / 60;
112 minutes -= hours * 60;
113 return twoDigits(hours) + ':' + twoDigits(minutes);
114 }
115}
116
117QByteArray Timespan::toExpression() const
118{
119 QByteArray expr = begin.toExpression(false);
120 if (!pointInTime) {
121 expr += '-' + end.toExpression(true);
122 }
123 if (openEnd) {
124 expr += '+';
125 }
126 if (interval) {
127 expr += '/' + intervalToExpression(interval);
128 }
129 if (next) {
130 expr += ',' + next->toExpression();
131 }
132 return expr;
133}
134
135Time Timespan::adjustedEnd() const
136{
137 if (begin == end && !pointInTime) {
138 return { end.event, end.hour + 24, end.minute };
139 }
140 return end;
141}
142
143bool Timespan::operator==(Timespan &other) const
144{
145 return begin == other.begin &&
146 end == other.end &&
147 openEnd == other.openEnd &&
148 interval == other.interval &&
149 bool(next) == (bool)other.next &&
150 (!next || *next == *other.next);
151}
152
153int WeekdayRange::requiredCapabilities() const
154{
155 // only ranges or nthSequence are allowed, not both at the same time, enforced by parser
156 assert(beginDay == endDay || !nthSequence);
157
158 int c = Capability::None;
159 switch (holiday) {
160 case NoHoliday:
161 if ((offset > 0 && !nthSequence)) {
162 c |= Capability::NotImplemented;
163 }
164 break;
165 case PublicHoliday:
166 c |= Capability::PublicHoliday;
167 break;
168 case SchoolHoliday:
169 c |= Capability::SchoolHoliday;
170 break;
171 }
172
173 c |= lhsAndSelector ? lhsAndSelector->requiredCapabilities() : Capability::None;
174 c |= rhsAndSelector ? rhsAndSelector->requiredCapabilities() : Capability::None;
175 c |= next ? next->requiredCapabilities() : Capability::None;
176 return c;
177}
178
179static constexpr const char* s_weekDays[] = { "ERROR", "Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"};
180
181QByteArray WeekdayRange::toExpression() const
182{
183 QByteArray expr;
184 if (lhsAndSelector && rhsAndSelector) {
185 expr = lhsAndSelector->toExpression() + ' ' + rhsAndSelector->toExpression();
186 } else {
187 switch (holiday) {
188 case NoHoliday: {
189 expr = s_weekDays[beginDay];
190 if (endDay != beginDay) {
191 expr += '-';
192 expr += s_weekDays[endDay];
193 }
194 break;
195 }
196 case PublicHoliday:
197 expr = "PH";
198 break;
199 case SchoolHoliday:
200 expr = "SH";
201 break;
202 }
203 if (nthSequence) {
204 expr += '[' + nthSequence->toExpression() + ']';
205 }
206 if (offset > 0) {
207 expr += " +" + QByteArray::number(offset) + ' ' + (offset > 1 ? "days" : "day");
208 } else if (offset < 0) {
209 expr += " -" + QByteArray::number(-offset) + ' ' + (offset < -1 ? "days" : "day");
210 }
211 }
212 if (next) {
213 expr += ',' + next->toExpression();
214 }
215 return expr;
216}
217
218void WeekdayRange::simplify()
219{
220 QMap<int, WeekdayRange *> endToSelectorMap;
221 bool seenDays[8];
222 const int endIdx = sizeof(seenDays);
223 std::fill(std::begin(seenDays), std::end(seenDays), false);
224 for (WeekdayRange *selector = this; selector; selector = selector->next.get()) {
225 // Ensure it's all just week days, no other features
226 if (selector->nthSequence || selector->lhsAndSelector || selector->holiday != NoHoliday || selector->offset) {
227 return;
228 }
229 const bool wrap = selector->beginDay > selector->endDay;
230 for (int day = selector->beginDay; day <= selector->endDay + (wrap ? 7 : 0); ++day) {
231 seenDays[(day - 1) % 7 + 1] = true;
232 }
233 endToSelectorMap.insert(selector->endDay, selector);
234 }
235
236 QString str;
237 for (int idx = 1; idx < endIdx; ++idx) {
238 str += QLatin1Char(seenDays[idx] ? '1' : '0');
239 }
240
241 // Clear everything and refill
242 next.reset(nullptr);
243
244 int startIdx = 1;
245
246 // -1 and +1 in a wrapping world
247 auto prevIdx = [&](int idx) {
248 Q_ASSERT(idx > 0 && idx < 8);
249 return idx == 1 ? 7 : (idx - 1);
250 };
251 auto nextIdx = [&](int idx) {
252 Q_ASSERT(idx > 0 && idx < 8);
253 return idx % 7 + 1;
254 };
255
256 // like std::find, but let's use indexes - and wrap at 8
257 auto find = [&](int idx, bool value) {
258 do {
259 if (seenDays[idx] == value)
260 return idx;
261 idx = nextIdx(idx);
262 } while(idx != startIdx);
263 return idx;
264 };
265 auto findPrev = [&](int idx, bool value) {
266 for (; idx > 0; --idx) {
267 if (seenDays[idx] == value)
268 return idx;
269 }
270 return 0;
271 };
272
273 WeekdayRange *prev = nullptr;
274 WeekdayRange *selector = this;
275
276 auto addRange = [&](int from, int to) {
277 if (prev) {
278 selector = new WeekdayRange;
279 prev->next.reset(selector);
280 }
281 selector->beginDay = from;
282 selector->endDay = to;
283 prev = selector;
284
285 };
286
287 int idx = 0;
288 if (seenDays[1]) {
289 // monday is set, try going further back
290 idx = findPrev(7, false);
291 if (idx) {
292 idx = nextIdx(idx);
293 }
294 }
295 if (idx == 0) {
296 // start at first day being set (Tu or more)
297 idx = find(1, true);
298 }
299 startIdx = idx;
300 Q_ASSERT(startIdx > 0);
301 do {
302 // find end of 'true' range
303 const int finishIdx = find(idx, false);
304 // if the range is only 2 items, prefer Mo,Tu over Mo-Tu
305 if (finishIdx == nextIdx(nextIdx(idx))) {
306 addRange(idx, idx);
307 const int n = nextIdx(idx);
308 addRange(n, n);
309 } else {
310 addRange(idx, prevIdx(finishIdx));
311 }
312 idx = find(finishIdx, true);
313 } while (idx != startIdx);
314}
315
316int Week::requiredCapabilities() const
317{
318 if (endWeek < beginWeek) { // is this even officially allowed?
319 return Capability::NotImplemented;
320 }
321 return next ? next->requiredCapabilities() : Capability::None;
322}
323
324QByteArray Week::toExpression() const
325{
326 QByteArray expr = twoDigits(beginWeek);
327 if (endWeek != beginWeek) {
328 expr += '-';
329 expr += twoDigits(endWeek);
330 }
331 if (interval > 1) {
332 expr += '/';
333 expr += QByteArray::number(interval);
334 }
335 if (next) {
336 expr += ',' + next->toExpression();
337 }
338 return expr;
339}
340
341QByteArray Date::toExpression(const Date &refDate, const MonthdayRange &prev) const
342{
343 QByteArray expr;
344 auto maybeSpace = [&]() {
345 if (!expr.isEmpty()) {
346 expr += ' ';
347 }
348 };
349 switch (variableDate) {
350 case FixedDate: {
351 const bool needYear = year && (year != refDate.year || (day && month && month != refDate.month));
352 if (needYear) {
353 expr += QByteArray::number(year);
354 }
355 if (month) {
356 const bool combineWithPrev = prev.begin.month == prev.end.month && month == prev.begin.month;
357 const bool implicitMonth = month == refDate.month || (refDate.month == 0 && combineWithPrev);
358 if (needYear || !implicitMonth || hasOffset()) {
359 static const char* s_monthName[] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };
360 maybeSpace();
361 expr += s_monthName[month-1];
362 }
363 }
364 if (day && *this != refDate) {
365 maybeSpace();
366 expr += twoDigits(day);
367 }
368 break;
369 }
370 case Date::Easter:
371 if (year) {
372 expr += QByteArray::number(year) + ' ';
373 }
374 expr += "easter";
375 break;
376 }
377
378 if (offset.nthWeekday) {
379 expr += ' ';
380 expr += s_weekDays[offset.weekday];
381 expr += '[' + QByteArray::number(offset.nthWeekday) + ']';
382 }
383
384 if (offset.dayOffset > 0) {
385 expr += " +" + QByteArray::number(offset.dayOffset) + ' ' + (offset.dayOffset > 1 ? "days" : "day");
386 } else if (offset.dayOffset < 0) {
387 expr += " -" + QByteArray::number(-offset.dayOffset) + ' ' + (offset.dayOffset < -1 ? "days" : "day");
388 }
389 return expr;
390}
391
392bool DateOffset::operator==(DateOffset other) const
393{
394 return weekday == other.weekday && nthWeekday == other.nthWeekday && dayOffset == other.dayOffset;
395}
396
397DateOffset &DateOffset::operator+=(DateOffset other)
398{
399 // Only dayOffset really supports += (this is for whitsun)
400 dayOffset += other.dayOffset;
401 // The others can't possibly be set already
402 Q_ASSERT(weekday == 0);
403 Q_ASSERT(nthWeekday == 0);
404 weekday = other.weekday;
405 nthWeekday = other.nthWeekday;
406 return *this;
407}
408
409bool Date::operator==(Date other) const
410{
411 if (variableDate != other.variableDate)
412 return false;
413 if (variableDate == FixedDate && other.variableDate == FixedDate) {
414 if (!(year == other.year && month == other.month && day == other.day)) {
415 return false;
416 }
417 }
418 return offset == other.offset;
419}
420
421bool Date::hasOffset() const
422{
423 return offset.dayOffset || offset.weekday;
424}
425
426int MonthdayRange::requiredCapabilities() const
427{
428 return Capability::None;
429}
430
431QByteArray MonthdayRange::toExpression(const MonthdayRange &prev) const
432{
433 QByteArray expr = begin.toExpression({}, prev);
434 if (end != begin) {
435 expr += '-' + end.toExpression(begin, prev);
436 }
437 if (next) {
438 expr += ',' + next->toExpression(*this);
439 }
440 return expr;
441}
442
443void MonthdayRange::simplify()
444{
445 // "Feb 1-29" => "Feb" (#446252)
446 if (begin.variableDate == Date::FixedDate &&
447 end.variableDate == Date::FixedDate &&
448 begin.year == end.year &&
449 begin.month && end.month &&
450 begin.month == end.month &&
451 begin.day && end.day) {
452 // The year doesn't matter, but take one with a leap day, for Feb 1-29
453 const int lastDay = QDate{2004, end.month, end.day}.daysInMonth();
454 if (begin.day == 1 && end.day == lastDay) {
455 begin.day = 0;
456 end.day = 0;
457 }
458 }
459}
460
461int YearRange::requiredCapabilities() const
462{
463 return Capability::None;
464}
465
466QByteArray YearRange::toExpression() const
467{
468 QByteArray expr = QByteArray::number(begin);
469 if (end == 0 && interval == 1) {
470 expr += '+';
471 } else if (end != begin && end != 0) {
472 expr += '-';
473 expr += QByteArray::number(end);
474 }
475 if (interval > 1) {
476 expr += '/';
477 expr += QByteArray::number(interval);
478 }
479 if (next) {
480 expr += ',' + next->toExpression();
481 }
482 return expr;
483}
484
485void NthSequence::add(NthEntry range)
486{
487 sequence.push_back(std::move(range));
488}
489
490QByteArray NthSequence::toExpression() const
491{
492 QByteArray ret;
493 for (const NthEntry &entry : sequence) {
494 if (!ret.isEmpty())
495 ret += ',';
496 ret += entry.toExpression();
497 }
498 return ret;
499}
500
501QByteArray NthEntry::toExpression() const
502{
503 if (begin == end)
504 return QByteArray::number(begin);
505 return QByteArray::number(begin) + '-' + QByteArray::number(end);
506}
QAction * end(const QObject *recvr, const char *slot, QObject *parent)
OSM opening hours parsing and evaluation.
Definition display.h:16
QAction * findPrev(const QObject *recvr, const char *slot, QObject *parent)
QAction * next(const QObject *recvr, const char *slot, QObject *parent)
KGuiItem find()
const QList< QKeySequence > & begin()
virtual bool event(QEvent *e) override
bool isEmpty() const const
QByteArray number(double n, char format, int precision)
QByteArray & prepend(QByteArrayView ba)
void push_back(QByteArrayView str)
qsizetype size() const const
int daysInMonth() const const
iterator insert(const Key &key, const T &value)
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.