KCalendarCore

recurrencerule.cpp
1/*
2 This file is part of the kcalcore library.
3
4 SPDX-FileCopyrightText: 2005 Reinhold Kainhofer <reinhold@kainhofe.com>
5 SPDX-FileCopyrightText: 2006-2008 David Jarvie <djarvie@kde.org>
6
7 SPDX-License-Identifier: LGPL-2.0-or-later
8*/
9
10#include "incidencebase.h"
11#include "recurrencerule.h"
12#include "kcalendarcore_debug.h"
13#include "recurrencehelper_p.h"
14#include "utils_p.h"
15
16#include <QDataStream>
17#include <QList>
18#include <QStringList>
19#include <QTime>
20
21using namespace KCalendarCore;
22
23// Maximum number of intervals to process
24const int LOOP_LIMIT = 10000;
25
26#ifndef NDEBUG
27static QString dumpTime(const QDateTime &dt, bool allDay); // for debugging
28#endif
29
30/*=========================================================================
31= =
32= IMPORTANT CODING NOTE: =
33= =
34= Recurrence handling code is time critical, especially for sub-daily =
35= recurrences. For example, if getNextDate() is called repeatedly to =
36= check all consecutive occurrences over a few years, on a slow machine =
37= this could take many seconds to complete in the worst case. Simple =
38= sub-daily recurrences are optimised by use of mTimedRepetition. =
39= =
40==========================================================================*/
41
42/**************************************************************************
43 * DateHelper *
44 **************************************************************************/
45//@cond PRIVATE
46class DateHelper
47{
48public:
49#ifndef NDEBUG
50 static QString dayName(short day);
51#endif
52 static QDate getNthWeek(int year, int weeknumber, short weekstart = 1);
53 static int weekNumbersInYear(int year, short weekstart = 1);
54 static int getWeekNumber(const QDate &date, short weekstart, int *year = nullptr);
55 static int getWeekNumberNeg(const QDate &date, short weekstart, int *year = nullptr);
56 // Convert to QDate, allowing for day < 0.
57 // month and day must be non-zero.
58 static QDate getDate(int year, int month, int day)
59 {
60 if (day >= 0) {
61 return QDate(year, month, day);
62 } else {
63 if (++month > 12) {
64 month = 1;
65 ++year;
66 }
67 return QDate(year, month, 1).addDays(day);
68 }
69 }
70};
71
72#ifndef NDEBUG
73// TODO: Move to a general library / class, as we need the same in the iCal
74// generator and in the xcal format
75QString DateHelper::dayName(short day)
76{
77 switch (day) {
78 case 1:
79 return QStringLiteral("MO");
80 case 2:
81 return QStringLiteral("TU");
82 case 3:
83 return QStringLiteral("WE");
84 case 4:
85 return QStringLiteral("TH");
86 case 5:
87 return QStringLiteral("FR");
88 case 6:
89 return QStringLiteral("SA");
90 case 7:
91 return QStringLiteral("SU");
92 default:
93 return QStringLiteral("??");
94 }
95}
96#endif
97
98QDate DateHelper::getNthWeek(int year, int weeknumber, short weekstart)
99{
100 if (weeknumber == 0) {
101 return QDate();
102 }
103
104 // Adjust this to the first day of week #1 of the year and add 7*weekno days.
105 QDate dt(year, 1, 4); // Week #1 is the week that contains Jan 4
106 int adjust = -(7 + dt.dayOfWeek() - weekstart) % 7;
107 if (weeknumber > 0) {
108 dt = dt.addDays(7 * (weeknumber - 1) + adjust);
109 } else if (weeknumber < 0) {
110 dt = dt.addYears(1);
111 dt = dt.addDays(7 * weeknumber + adjust);
112 }
113 return dt;
114}
115
116int DateHelper::getWeekNumber(const QDate &date, short weekstart, int *year)
117{
118 int y = date.year();
119 QDate dt(y, 1, 4); // <= definitely in week #1
120 dt = dt.addDays(-(7 + dt.dayOfWeek() - weekstart) % 7); // begin of week #1
121
122 qint64 daysto = dt.daysTo(date);
123 if (daysto < 0) {
124 // in first week of year
125 --y;
126 dt = QDate(y, 1, 4);
127 dt = dt.addDays(-(7 + dt.dayOfWeek() - weekstart) % 7); // begin of week #1
128 daysto = dt.daysTo(date);
129 } else if (daysto > 355) {
130 // near the end of the year - check if it's next year
131 QDate dtn(y + 1, 1, 4); // <= definitely first week of next year
132 dtn = dtn.addDays(-(7 + dtn.dayOfWeek() - weekstart) % 7);
133 qint64 dayston = dtn.daysTo(date);
134 if (dayston >= 0) {
135 // in first week of next year;
136 ++y;
137 daysto = dayston;
138 }
139 }
140 if (year) {
141 *year = y;
142 }
143 return daysto / 7 + 1;
144}
145
146int DateHelper::weekNumbersInYear(int year, short weekstart)
147{
148 QDate dt(year, 1, weekstart);
149 QDate dt1(year + 1, 1, weekstart);
150 return dt.daysTo(dt1) / 7;
151}
152
153// Week number from the end of the year
154int DateHelper::getWeekNumberNeg(const QDate &date, short weekstart, int *year)
155{
156 int weekpos = getWeekNumber(date, weekstart, year);
157 return weekNumbersInYear(*year, weekstart) - weekpos - 1;
158}
159//@endcond
160
161/**************************************************************************
162 * WDayPos *
163 **************************************************************************/
164
165bool RecurrenceRule::WDayPos::operator==(const RecurrenceRule::WDayPos &pos2) const
166{
167 return mDay == pos2.mDay && mPos == pos2.mPos;
168}
169
170bool RecurrenceRule::WDayPos::operator!=(const RecurrenceRule::WDayPos &pos2) const
171{
172 return !operator==(pos2);
173}
174
175/**************************************************************************
176 * Constraint *
177 **************************************************************************/
178//@cond PRIVATE
179class Constraint
180{
181public:
182 typedef QList<Constraint> List;
183
184 Constraint()
185 {
186 }
187 explicit Constraint(const QTimeZone &, int wkst = 1);
188 Constraint(const QDateTime &dt, RecurrenceRule::PeriodType type, int wkst);
189 void clear();
190 void setYear(int n)
191 {
192 year = n;
193 useCachedDt = false;
194 }
195 void setMonth(int n)
196 {
197 month = n;
198 useCachedDt = false;
199 }
200 void setDay(int n)
201 {
202 day = n;
203 useCachedDt = false;
204 }
205 void setHour(int n)
206 {
207 hour = n;
208 useCachedDt = false;
209 }
210 void setMinute(int n)
211 {
212 minute = n;
213 useCachedDt = false;
214 }
215 void setSecond(int n)
216 {
217 second = n;
218 useCachedDt = false;
219 }
220 void setWeekday(int n)
221 {
222 weekday = n;
223 useCachedDt = false;
224 }
225 void setWeekdaynr(int n)
226 {
227 weekdaynr = n;
228 useCachedDt = false;
229 }
230 void setWeeknumber(int n)
231 {
232 weeknumber = n;
233 useCachedDt = false;
234 }
235 void setYearday(int n)
236 {
237 yearday = n;
238 useCachedDt = false;
239 }
240 void setWeekstart(int n)
241 {
242 weekstart = n;
243 useCachedDt = false;
244 }
245
246 int year; // 0 means unspecified
247 int month; // 0 means unspecified
248 int day; // 0 means unspecified
249 int hour; // -1 means unspecified
250 int minute; // -1 means unspecified
251 int second; // -1 means unspecified
252 int weekday; // 0 means unspecified
253 int weekdaynr; // index of weekday in month/year (0=unspecified)
254 int weeknumber; // 0 means unspecified
255 int yearday; // 0 means unspecified
256 int weekstart; // first day of week (1=monday, 7=sunday, 0=unspec.)
257 QTimeZone timeZone; // time zone etc. to use
258
259 bool readDateTime(const QDateTime &dt, RecurrenceRule::PeriodType type);
260 bool matches(const QDate &dt, RecurrenceRule::PeriodType type) const;
261 bool matches(const QDateTime &dt, RecurrenceRule::PeriodType type) const;
262 bool merge(const Constraint &interval);
263 bool isConsistent(RecurrenceRule::PeriodType period) const;
264 bool increase(RecurrenceRule::PeriodType type, int freq);
265 QDateTime intervalDateTime(RecurrenceRule::PeriodType type) const;
266 QList<QDateTime> dateTimes(RecurrenceRule::PeriodType type) const;
267 void appendDateTime(const QDate &date, const QTime &time, QList<QDateTime> &list) const;
268 void dump() const;
269
270private:
271 mutable bool useCachedDt;
272 mutable QDateTime cachedDt;
273};
274
275Constraint::Constraint(const QTimeZone &timeZone, int wkst)
276 : weekstart(wkst)
277 , timeZone(timeZone)
278{
279 clear();
280}
281
282Constraint::Constraint(const QDateTime &dt, RecurrenceRule::PeriodType type, int wkst)
283 : weekstart(wkst)
284 , timeZone(dt.timeZone())
285{
286 clear();
287 readDateTime(dt, type);
288}
289
290void Constraint::clear()
291{
292 year = 0;
293 month = 0;
294 day = 0;
295 hour = -1;
296 minute = -1;
297 second = -1;
298 weekday = 0;
299 weekdaynr = 0;
300 weeknumber = 0;
301 yearday = 0;
302 useCachedDt = false;
303}
304
305bool Constraint::matches(const QDate &dt, RecurrenceRule::PeriodType type) const
306{
307 // If the event recurs in week 53 or 1, the day might not belong to the same
308 // year as the week it is in. E.g. Jan 1, 2005 is in week 53 of year 2004.
309 // So we can't simply check the year in that case!
310 if (weeknumber == 0) {
311 if (year > 0 && year != dt.year()) {
312 return false;
313 }
314 } else {
315 int y = 0;
316 if (weeknumber > 0 && weeknumber != DateHelper::getWeekNumber(dt, weekstart, &y)) {
317 return false;
318 }
319 if (weeknumber < 0 && weeknumber != DateHelper::getWeekNumberNeg(dt, weekstart, &y)) {
320 return false;
321 }
322 if (year > 0 && year != y) {
323 return false;
324 }
325 }
326
327 if (month > 0 && month != dt.month()) {
328 return false;
329 }
330 if (day > 0 && day != dt.day()) {
331 return false;
332 }
333 if (day < 0 && dt.day() != (dt.daysInMonth() + day + 1)) {
334 return false;
335 }
336 if (weekday > 0) {
337 if (weekday != dt.dayOfWeek()) {
338 return false;
339 }
340 if (weekdaynr != 0) {
341 // If it's a yearly recurrence and a month is given, the position is
342 // still in the month, not in the year.
343 if ((type == RecurrenceRule::rMonthly) || (type == RecurrenceRule::rYearly && month > 0)) {
344 // Monthly
345 if (weekdaynr > 0 && weekdaynr != (dt.day() - 1) / 7 + 1) {
346 return false;
347 }
348 if (weekdaynr < 0 && weekdaynr != -((dt.daysInMonth() - dt.day()) / 7 + 1)) {
349 return false;
350 }
351 } else {
352 // Yearly
353 if (weekdaynr > 0 && weekdaynr != (dt.dayOfYear() - 1) / 7 + 1) {
354 return false;
355 }
356 if (weekdaynr < 0 && weekdaynr != -((dt.daysInYear() - dt.dayOfYear()) / 7 + 1)) {
357 return false;
358 }
359 }
360 }
361 }
362 if (yearday > 0 && yearday != dt.dayOfYear()) {
363 return false;
364 }
365 if (yearday < 0 && yearday != dt.daysInYear() - dt.dayOfYear() + 1) {
366 return false;
367 }
368 return true;
369}
370
371/* Check for a match with the specified date/time.
372 * The date/time's time specification must correspond with that of the start date/time.
373 */
374bool Constraint::matches(const QDateTime &dt, RecurrenceRule::PeriodType type) const
375{
376 if ((hour >= 0 && hour != dt.time().hour()) || (minute >= 0 && minute != dt.time().minute()) || (second >= 0 && second != dt.time().second())
377 || !matches(dt.date(), type)) {
378 return false;
379 }
380 return true;
381}
382
383bool Constraint::isConsistent(RecurrenceRule::PeriodType /*period*/) const
384{
385 // TODO: Check for consistency, e.g. byyearday=3 and bymonth=10
386 return true;
387}
388
389// Return a date/time set to the constraint values, but with those parts less
390// significant than the given period type set to 1 (for dates) or 0 (for times).
391QDateTime Constraint::intervalDateTime(RecurrenceRule::PeriodType type) const
392{
393 if (useCachedDt) {
394 return cachedDt;
395 }
396 QDate d;
397 QTime t(0, 0, 0);
398 bool subdaily = true;
399 switch (type) {
400 case RecurrenceRule::rSecondly:
401 t.setHMS(hour, minute, second);
402 break;
403 case RecurrenceRule::rMinutely:
404 t.setHMS(hour, minute, 0);
405 break;
406 case RecurrenceRule::rHourly:
407 t.setHMS(hour, 0, 0);
408 break;
409 case RecurrenceRule::rDaily:
410 break;
411 case RecurrenceRule::rWeekly:
412 d = DateHelper::getNthWeek(year, weeknumber, weekstart);
413 subdaily = false;
414 break;
415 case RecurrenceRule::rMonthly:
416 d.setDate(year, month, 1);
417 subdaily = false;
418 break;
419 case RecurrenceRule::rYearly:
420 d.setDate(year, 1, 1);
421 subdaily = false;
422 break;
423 default:
424 break;
425 }
426 if (subdaily) {
427 d = DateHelper::getDate(year, (month > 0) ? month : 1, day ? day : 1);
428 }
429 cachedDt = QDateTime(d, t, timeZone);
430 useCachedDt = true;
431 return cachedDt;
432}
433
434bool Constraint::merge(const Constraint &interval)
435{
436// clang-format off
437#define mergeConstraint( name, cmparison ) \
438 if ( interval.name cmparison ) { \
439 if ( !( name cmparison ) ) { \
440 name = interval.name; \
441 } else if ( name != interval.name ) { \
442 return false;\
443 } \
444 }
445 // clang-format on
446
447 useCachedDt = false;
448
449 mergeConstraint(year, > 0);
450 mergeConstraint(month, > 0);
451 mergeConstraint(day, != 0);
452 mergeConstraint(hour, >= 0);
453 mergeConstraint(minute, >= 0);
454 mergeConstraint(second, >= 0);
455
456 mergeConstraint(weekday, != 0);
457 mergeConstraint(weekdaynr, != 0);
458 mergeConstraint(weeknumber, != 0);
459 mergeConstraint(yearday, != 0);
460
461#undef mergeConstraint
462 return true;
463}
464
465// Y M D | H Mn S | WD #WD | WN | YD
466// required:
467// x | x x x | | |
468// 0) Trivial: Exact date given, maybe other restrictions
469// x x x | x x x | | |
470// 1) Easy case: no weekly restrictions -> at most a loop through possible dates
471// x + + | x x x | - - | - | -
472// 2) Year day is given -> date known
473// x | x x x | | | +
474// 3) week number is given -> loop through all days of that week. Further
475// restrictions will be applied in the end, when we check all dates for
476// consistency with the constraints
477// x | x x x | | + | (-)
478// 4) week day is specified ->
479// x | x x x | x ? | (-)| (-)
480// 5) All possiblecases have already been treated, so this must be an error!
481
482QList<QDateTime> Constraint::dateTimes(RecurrenceRule::PeriodType type) const
483{
484 QList<QDateTime> result;
485 if (!isConsistent(type)) {
486 return result;
487 }
488
489 // TODO_Recurrence: Handle all-day
490 QTime tm(hour, minute, second);
491
492 bool done = false;
493 if (day && month > 0) {
494 appendDateTime(DateHelper::getDate(year, month, day), tm, result);
495 done = true;
496 }
497
498 if (!done && weekday == 0 && weeknumber == 0 && yearday == 0) {
499 // Easy case: date is given, not restrictions by week or yearday
500 uint mstart = (month > 0) ? month : 1;
501 uint mend = (month <= 0) ? 12 : month;
502 for (uint m = mstart; m <= mend; ++m) {
503 uint dstart;
504 uint dend;
505 if (day > 0) {
506 dstart = dend = day;
507 } else if (day < 0) {
508 QDate date(year, month, 1);
509 dstart = dend = date.daysInMonth() + day + 1;
510 } else {
511 QDate date(year, month, 1);
512 dstart = 1;
513 dend = date.daysInMonth();
514 }
515 uint d = dstart;
516 for (QDate dt(year, m, dstart);; dt = dt.addDays(1)) {
517 appendDateTime(dt, tm, result);
518 if (++d > dend) {
519 break;
520 }
521 }
522 }
523 done = true;
524 }
525
526 // Else: At least one of the week / yearday restrictions was given...
527 // If we have a yearday (and of course a year), we know the exact date
528 if (!done && yearday != 0) {
529 // yearday < 0 means from end of year, so we'll need Jan 1 of the next year
530 QDate d(year + ((yearday > 0) ? 0 : 1), 1, 1);
531 d = d.addDays(yearday - ((yearday > 0) ? 1 : 0));
532 appendDateTime(d, tm, result);
533 done = true;
534 }
535
536 // Else: If we have a weeknumber, we have at most 7 possible dates, loop through them
537 if (!done && weeknumber != 0) {
538 QDate wst(DateHelper::getNthWeek(year, weeknumber, weekstart));
539 if (weekday != 0) {
540 wst = wst.addDays((7 + weekday - weekstart) % 7);
541 appendDateTime(wst, tm, result);
542 } else {
543 for (int i = 0; i < 7; ++i) {
544 appendDateTime(wst, tm, result);
545 wst = wst.addDays(1);
546 }
547 }
548 done = true;
549 }
550
551 // weekday is given
552 if (!done && weekday != 0) {
553 QDate dt(year, 1, 1);
554 // If type == yearly and month is given, pos is still in month not year!
555 // TODO_Recurrence: Correct handling of n-th BYDAY...
556 int maxloop = 53;
557 bool inMonth = (type == RecurrenceRule::rMonthly) || (type == RecurrenceRule::rYearly && month > 0);
558 if (inMonth && month > 0) {
559 dt = QDate(year, month, 1);
560 maxloop = 5;
561 }
562 if (weekdaynr < 0) {
563 // From end of period (month, year) => relative to begin of next period
564 if (inMonth) {
565 dt = dt.addMonths(1);
566 } else {
567 dt = dt.addYears(1);
568 }
569 }
570 int adj = (7 + weekday - dt.dayOfWeek()) % 7;
571 dt = dt.addDays(adj); // correct first weekday of the period
572
573 if (weekdaynr > 0) {
574 dt = dt.addDays((weekdaynr - 1) * 7);
575 appendDateTime(dt, tm, result);
576 } else if (weekdaynr < 0) {
577 dt = dt.addDays(weekdaynr * 7);
578 appendDateTime(dt, tm, result);
579 } else {
580 // loop through all possible weeks, non-matching will be filtered later
581 for (int i = 0; i < maxloop; ++i) {
582 appendDateTime(dt, tm, result);
583 dt = dt.addDays(7);
584 }
585 }
586 } // weekday != 0
587
588 // Only use those times that really match all other constraints, too
589 QList<QDateTime> valid;
590 for (int i = 0, iend = result.count(); i < iend; ++i) {
591 if (matches(result[i], type)) {
592 valid.append(result[i]);
593 }
594 }
595 // Don't sort it here, would be unnecessary work. The results from all
596 // constraints will be merged to one big list of the interval. Sort that one!
597 return valid;
598}
599
600void Constraint::appendDateTime(const QDate &date, const QTime &time, QList<QDateTime> &list) const
601{
602 QDateTime dt(date, time, timeZone);
603 if (dt.isValid()) {
604 list.append(dt);
605 }
606}
607
608bool Constraint::increase(RecurrenceRule::PeriodType type, int freq)
609{
610 // convert the first day of the interval to QDateTime
611 intervalDateTime(type);
612
613 // Now add the intervals
614 switch (type) {
615 case RecurrenceRule::rSecondly:
616 cachedDt = cachedDt.addSecs(freq);
617 break;
618 case RecurrenceRule::rMinutely:
619 cachedDt = cachedDt.addSecs(60 * freq);
620 break;
621 case RecurrenceRule::rHourly:
622 cachedDt = cachedDt.addSecs(3600 * freq);
623 break;
624 case RecurrenceRule::rDaily:
625 cachedDt = cachedDt.addDays(freq);
626 break;
627 case RecurrenceRule::rWeekly:
628 cachedDt = cachedDt.addDays(7 * freq);
629 break;
630 case RecurrenceRule::rMonthly:
631 cachedDt = cachedDt.addMonths(freq);
632 break;
633 case RecurrenceRule::rYearly:
634 cachedDt = cachedDt.addYears(freq);
635 break;
636 default:
637 break;
638 }
639 // Convert back from QDateTime to the Constraint class
640 readDateTime(cachedDt, type);
641 useCachedDt = true; // readDateTime() resets this
642
643 return true;
644}
645
646// Set the constraint's value appropriate to 'type', to the value contained in a date/time.
647bool Constraint::readDateTime(const QDateTime &dt, RecurrenceRule::PeriodType type)
648{
649 switch (type) {
650 // Really fall through! Only weekly needs to be treated differently!
651 case RecurrenceRule::rSecondly:
652 second = dt.time().second();
653 Q_FALLTHROUGH();
654 case RecurrenceRule::rMinutely:
655 minute = dt.time().minute();
656 Q_FALLTHROUGH();
657 case RecurrenceRule::rHourly:
658 hour = dt.time().hour();
659 Q_FALLTHROUGH();
660 case RecurrenceRule::rDaily:
661 day = dt.date().day();
662 Q_FALLTHROUGH();
663 case RecurrenceRule::rMonthly:
664 month = dt.date().month();
665 Q_FALLTHROUGH();
666 case RecurrenceRule::rYearly:
667 year = dt.date().year();
668 break;
669 case RecurrenceRule::rWeekly:
670 // Determine start day of the current week, calculate the week number from that
671 weeknumber = DateHelper::getWeekNumber(dt.date(), weekstart, &year);
672 break;
673 default:
674 break;
675 }
676 useCachedDt = false;
677 return true;
678}
679//@endcond
680
681/**************************************************************************
682 * RecurrenceRule::Private *
683 **************************************************************************/
684
685//@cond PRIVATE
686class Q_DECL_HIDDEN KCalendarCore::RecurrenceRule::Private
687{
688public:
689 Private(RecurrenceRule *parent)
690 : mParent(parent)
691 , mPeriod(rNone)
692 , mFrequency(0)
693 , mDuration(-1)
694 , mWeekStart(1)
695 , mIsReadOnly(false)
696 , mAllDay(false)
697 {
698 setDirty();
699 }
700
701 Private(RecurrenceRule *parent, const Private &p);
702
703 Private &operator=(const Private &other);
704 bool operator==(const Private &other) const;
705 void clear();
706 void setDirty();
707 void buildConstraints();
708 bool buildCache() const;
709 Constraint getNextValidDateInterval(const QDateTime &preDate, PeriodType type) const;
710 Constraint getPreviousValidDateInterval(const QDateTime &afterDate, PeriodType type) const;
711 QList<QDateTime> datesForInterval(const Constraint &interval, PeriodType type) const;
712
713 RecurrenceRule *mParent;
714 QString mRRule; // RRULE string
715 PeriodType mPeriod;
716 QDateTime mDateStart; // start of recurrence (but mDateStart is not an occurrence
717 // unless it matches the rule)
718 uint mFrequency;
719 /** how often it recurs:
720 < 0 means no end date,
721 0 means an explicit end date,
722 positive values give the number of occurrences */
723 int mDuration;
724 QDateTime mDateEnd;
725
726 QList<int> mBySeconds; // values: second 0-59
727 QList<int> mByMinutes; // values: minute 0-59
728 QList<int> mByHours; // values: hour 0-23
729
730 QList<WDayPos> mByDays; // n-th weekday of the month or year
731 QList<int> mByMonthDays; // values: day -31 to -1 and 1-31
732 QList<int> mByYearDays; // values: day -366 to -1 and 1-366
733 QList<int> mByWeekNumbers; // values: week -53 to -1 and 1-53
734 QList<int> mByMonths; // values: month 1-12
735 QList<int> mBySetPos; // values: position -366 to -1 and 1-366
736 short mWeekStart; // first day of the week (1=Monday, 7=Sunday)
737
738 Constraint::List mConstraints;
739 QList<RuleObserver *> mObservers;
740
741 // Cache for duration
742 mutable QList<QDateTime> mCachedDates;
743 mutable QDateTime mCachedDateEnd;
744 mutable QDateTime mCachedLastDate; // when mCachedDateEnd invalid, last date checked
745 mutable bool mCached;
746
747 bool mIsReadOnly;
748 bool mAllDay;
749 bool mNoByRules; // no BySeconds, ByMinutes, ... rules exist
750 uint mTimedRepetition; // repeats at a regular number of seconds interval, or 0
751};
752
753RecurrenceRule::Private::Private(RecurrenceRule *parent, const Private &p)
754 : mParent(parent)
755 , mRRule(p.mRRule)
756 , mPeriod(p.mPeriod)
757 , mDateStart(p.mDateStart)
758 , mFrequency(p.mFrequency)
759 , mDuration(p.mDuration)
760 , mDateEnd(p.mDateEnd)
761 ,
762
763 mBySeconds(p.mBySeconds)
764 , mByMinutes(p.mByMinutes)
765 , mByHours(p.mByHours)
766 , mByDays(p.mByDays)
767 , mByMonthDays(p.mByMonthDays)
768 , mByYearDays(p.mByYearDays)
769 , mByWeekNumbers(p.mByWeekNumbers)
770 , mByMonths(p.mByMonths)
771 , mBySetPos(p.mBySetPos)
772 , mWeekStart(p.mWeekStart)
773 ,
774
775 mIsReadOnly(p.mIsReadOnly)
776 , mAllDay(p.mAllDay)
777 , mNoByRules(p.mNoByRules)
778{
779 setDirty();
780}
781
782RecurrenceRule::Private &RecurrenceRule::Private::operator=(const Private &p)
783{
784 // check for self assignment
785 if (&p == this) {
786 return *this;
787 }
788
789 mRRule = p.mRRule;
790 mPeriod = p.mPeriod;
791 mDateStart = p.mDateStart;
792 mFrequency = p.mFrequency;
793 mDuration = p.mDuration;
794 mDateEnd = p.mDateEnd;
795
796 mBySeconds = p.mBySeconds;
797 mByMinutes = p.mByMinutes;
798 mByHours = p.mByHours;
799 mByDays = p.mByDays;
800 mByMonthDays = p.mByMonthDays;
801 mByYearDays = p.mByYearDays;
802 mByWeekNumbers = p.mByWeekNumbers;
803 mByMonths = p.mByMonths;
804 mBySetPos = p.mBySetPos;
805 mWeekStart = p.mWeekStart;
806
807 mIsReadOnly = p.mIsReadOnly;
808 mAllDay = p.mAllDay;
809 mNoByRules = p.mNoByRules;
810
811 setDirty();
812
813 return *this;
814}
815
816bool RecurrenceRule::Private::operator==(const Private &r) const
817{
818 return mPeriod == r.mPeriod && identical(mDateStart, r.mDateStart) && mDuration == r.mDuration
819 && identical(mDateEnd, r.mDateEnd) && mFrequency == r.mFrequency && mIsReadOnly == r.mIsReadOnly
820 && mAllDay == r.mAllDay && mBySeconds == r.mBySeconds && mByMinutes == r.mByMinutes && mByHours == r.mByHours && mByDays == r.mByDays
821 && mByMonthDays == r.mByMonthDays && mByYearDays == r.mByYearDays && mByWeekNumbers == r.mByWeekNumbers && mByMonths == r.mByMonths
822 && mBySetPos == r.mBySetPos && mWeekStart == r.mWeekStart && mNoByRules == r.mNoByRules;
823}
824
825void RecurrenceRule::Private::clear()
826{
827 if (mIsReadOnly) {
828 return;
829 }
830 mPeriod = rNone;
831 mBySeconds.clear();
832 mByMinutes.clear();
833 mByHours.clear();
834 mByDays.clear();
835 mByMonthDays.clear();
836 mByYearDays.clear();
837 mByWeekNumbers.clear();
838 mByMonths.clear();
839 mBySetPos.clear();
840 mWeekStart = 1;
841 mNoByRules = false;
842
843 setDirty();
844}
845
846void RecurrenceRule::Private::setDirty()
847{
848 buildConstraints();
849 mCached = false;
850 mCachedDates.clear();
851 for (int i = 0, iend = mObservers.count(); i < iend; ++i) {
852 if (mObservers[i]) {
853 mObservers[i]->recurrenceChanged(mParent);
854 }
855 }
856}
857//@endcond
858
859/**************************************************************************
860 * RecurrenceRule *
861 **************************************************************************/
862
864 : d(new Private(this))
865{
866}
867
869 : d(new Private(this, *r.d))
870{
871}
872
873RecurrenceRule::~RecurrenceRule()
874{
875 delete d;
876}
877
878bool RecurrenceRule::operator==(const RecurrenceRule &r) const
879{
880 return *d == *r.d;
881}
882
883RecurrenceRule &RecurrenceRule::operator=(const RecurrenceRule &r)
884{
885 // check for self assignment
886 if (&r == this) {
887 return *this;
888 }
889
890 *d = *r.d;
891
892 return *this;
893}
894
895void RecurrenceRule::addObserver(RuleObserver *observer)
896{
897 if (!d->mObservers.contains(observer)) {
898 d->mObservers.append(observer);
899 }
900}
901
902void RecurrenceRule::removeObserver(RuleObserver *observer)
903{
904 d->mObservers.removeAll(observer);
905}
906
907void RecurrenceRule::setRecurrenceType(PeriodType period)
908{
909 if (isReadOnly()) {
910 return;
911 }
912 d->mPeriod = period;
913 d->setDirty();
914}
915
917{
918 if (result) {
919 *result = false;
920 }
921 if (d->mPeriod == rNone) {
922 return QDateTime();
923 }
924 if (d->mDuration < 0) {
925 return QDateTime();
926 }
927 if (d->mDuration == 0) {
928 if (result) {
929 *result = true;
930 }
931 return d->mDateEnd;
932 }
933
934 // N occurrences. Check if we have a full cache. If so, return the cached end date.
935 if (!d->mCached) {
936 // If not enough occurrences can be found (i.e. inconsistent constraints)
937 if (!d->buildCache()) {
938 return QDateTime();
939 }
940 }
941 if (result) {
942 *result = true;
943 }
944 return d->mCachedDateEnd;
945}
946
948{
949 if (isReadOnly()) {
950 return;
951 }
952 d->mDateEnd = dateTime;
953 if (d->mDateEnd.isValid()) {
954 d->mDuration = 0; // set to 0 because there is an end date/time
955 }
956 d->setDirty();
957}
958
960{
961 if (isReadOnly()) {
962 return;
963 }
964 d->mDuration = duration;
965 d->setDirty();
966}
967
969{
970 if (isReadOnly()) {
971 return;
972 }
973 d->mAllDay = allDay;
974 d->setDirty();
975}
976
978{
979 d->clear();
980}
981
982void RecurrenceRule::setDirty()
983{
984 d->setDirty();
985}
986
988{
989 if (isReadOnly()) {
990 return;
991 }
992 d->mDateStart = start;
993 d->setDirty();
994}
995
997{
998 if (isReadOnly() || freq <= 0) {
999 return;
1000 }
1001 d->mFrequency = freq;
1002 d->setDirty();
1003}
1004
1005void RecurrenceRule::setBySeconds(const QList<int> &bySeconds)
1006{
1007 if (isReadOnly()) {
1008 return;
1009 }
1010 d->mBySeconds = bySeconds;
1011 d->setDirty();
1012}
1013
1014void RecurrenceRule::setByMinutes(const QList<int> &byMinutes)
1015{
1016 if (isReadOnly()) {
1017 return;
1018 }
1019 d->mByMinutes = byMinutes;
1020 d->setDirty();
1021}
1022
1023void RecurrenceRule::setByHours(const QList<int> &byHours)
1024{
1025 if (isReadOnly()) {
1026 return;
1027 }
1028 d->mByHours = byHours;
1029 d->setDirty();
1030}
1031
1032void RecurrenceRule::setByDays(const QList<WDayPos> &byDays)
1033{
1034 if (isReadOnly()) {
1035 return;
1036 }
1037 d->mByDays = byDays;
1038 d->setDirty();
1039}
1040
1041void RecurrenceRule::setByMonthDays(const QList<int> &byMonthDays)
1042{
1043 if (isReadOnly()) {
1044 return;
1045 }
1046 d->mByMonthDays = byMonthDays;
1047 d->setDirty();
1048}
1049
1050void RecurrenceRule::setByYearDays(const QList<int> &byYearDays)
1051{
1052 if (isReadOnly()) {
1053 return;
1054 }
1055 d->mByYearDays = byYearDays;
1056 d->setDirty();
1057}
1058
1059void RecurrenceRule::setByWeekNumbers(const QList<int> &byWeekNumbers)
1060{
1061 if (isReadOnly()) {
1062 return;
1063 }
1064 d->mByWeekNumbers = byWeekNumbers;
1065 d->setDirty();
1066}
1067
1068void RecurrenceRule::setByMonths(const QList<int> &byMonths)
1069{
1070 if (isReadOnly()) {
1071 return;
1072 }
1073 d->mByMonths = byMonths;
1074 d->setDirty();
1075}
1076
1077void RecurrenceRule::setBySetPos(const QList<int> &bySetPos)
1078{
1079 if (isReadOnly()) {
1080 return;
1081 }
1082 d->mBySetPos = bySetPos;
1083 d->setDirty();
1084}
1085
1086void RecurrenceRule::setWeekStart(short weekStart)
1087{
1088 if (isReadOnly()) {
1089 return;
1090 }
1091 d->mWeekStart = weekStart;
1092 d->setDirty();
1093}
1094
1095void RecurrenceRule::shiftTimes(const QTimeZone &oldTz, const QTimeZone &newTz)
1096{
1097 d->mDateStart = d->mDateStart.toTimeZone(oldTz);
1098 d->mDateStart.setTimeZone(newTz);
1099 if (d->mDuration == 0) {
1100 d->mDateEnd = d->mDateEnd.toTimeZone(oldTz);
1101 d->mDateEnd.setTimeZone(newTz);
1102 }
1103 d->setDirty();
1104}
1105
1106// Taken from recurrence.cpp
1107// int RecurrenceRule::maxIterations() const
1108// {
1109// /* Find the maximum number of iterations which may be needed to reach the
1110// * next actual occurrence of a monthly or yearly recurrence.
1111// * More than one iteration may be needed if, for example, it's the 29th February,
1112// * the 31st day of the month or the 5th Monday, and the month being checked is
1113// * February or a 30-day month.
1114// * The following recurrences may never occur:
1115// * - For rMonthlyDay: if the frequency is a whole number of years.
1116// * - For rMonthlyPos: if the frequency is an even whole number of years.
1117// * - For rYearlyDay, rYearlyMonth: if the frequency is a multiple of 4 years.
1118// * - For rYearlyPos: if the frequency is an even number of years.
1119// * The maximum number of iterations needed, assuming that it does actually occur,
1120// * was found empirically.
1121// */
1122// switch (recurs) {
1123// case rMonthlyDay:
1124// return (rFreq % 12) ? 6 : 8;
1125//
1126// case rMonthlyPos:
1127// if (rFreq % 12 == 0) {
1128// // Some of these frequencies may never occur
1129// return (rFreq % 84 == 0) ? 364 // frequency = multiple of 7 years
1130// : (rFreq % 48 == 0) ? 7 // frequency = multiple of 4 years
1131// : (rFreq % 24 == 0) ? 14 : 28; // frequency = multiple of 2 or 1 year
1132// }
1133// // All other frequencies will occur sometime
1134// if (rFreq > 120)
1135// return 364; // frequencies of > 10 years will hit the date limit first
1136// switch (rFreq) {
1137// case 23: return 50;
1138// case 46: return 38;
1139// case 56: return 138;
1140// case 66: return 36;
1141// case 89: return 54;
1142// case 112: return 253;
1143// default: return 25; // most frequencies will need < 25 iterations
1144// }
1145//
1146// case rYearlyMonth:
1147// case rYearlyDay:
1148// return 8; // only 29th Feb or day 366 will need more than one iteration
1149//
1150// case rYearlyPos:
1151// if (rFreq % 7 == 0)
1152// return 364; // frequencies of a multiple of 7 years will hit the date limit first
1153// if (rFreq % 2 == 0) {
1154// // Some of these frequencies may never occur
1155// return (rFreq % 4 == 0) ? 7 : 14; // frequency = even number of years
1156// }
1157// return 28;
1158// }
1159// return 1;
1160// }
1161
1162//@cond PRIVATE
1163void RecurrenceRule::Private::buildConstraints()
1164{
1165 mTimedRepetition = 0;
1166 mNoByRules = mBySetPos.isEmpty();
1167 mConstraints.clear();
1168 Constraint con(mDateStart.timeZone());
1169 if (mWeekStart > 0) {
1170 con.setWeekstart(mWeekStart);
1171 }
1172 mConstraints.append(con);
1173
1174 int c;
1175 int cend;
1176 int i;
1177 int iend;
1178 Constraint::List tmp;
1179
1180// clang-format off
1181#define intConstraint( list, setElement ) \
1182 if ( !list.isEmpty() ) { \
1183 mNoByRules = false; \
1184 iend = list.count(); \
1185 if ( iend == 1 ) { \
1186 for ( c = 0, cend = mConstraints.count(); c < cend; ++c ) { \
1187 mConstraints[c].setElement( list[0] ); \
1188 } \
1189 } else { \
1190 tmp.reserve(mConstraints.count() * iend); \
1191 for ( c = 0, cend = mConstraints.count(); c < cend; ++c ) { \
1192 for ( i = 0; i < iend; ++i ) { \
1193 con = mConstraints[c]; \
1194 con.setElement( list[i] ); \
1195 tmp.append( con ); \
1196 } \
1197 } \
1198 mConstraints = tmp; \
1199 tmp.clear(); \
1200 } \
1201 }
1202 // clang-format on
1203
1204 intConstraint(mBySeconds, setSecond);
1205 intConstraint(mByMinutes, setMinute);
1206 intConstraint(mByHours, setHour);
1207 intConstraint(mByMonthDays, setDay);
1208 intConstraint(mByMonths, setMonth);
1209 intConstraint(mByYearDays, setYearday);
1210 intConstraint(mByWeekNumbers, setWeeknumber);
1211#undef intConstraint
1212
1213 if (!mByDays.isEmpty()) {
1214 mNoByRules = false;
1215 tmp.reserve(mConstraints.count() * mByDays.count());
1216 for (c = 0, cend = mConstraints.count(); c < cend; ++c) {
1217 for (i = 0, iend = mByDays.count(); i < iend; ++i) {
1218 con = mConstraints[c];
1219 con.setWeekday(mByDays[i].day());
1220 con.setWeekdaynr(mByDays[i].pos());
1221 tmp.append(con);
1222 }
1223 }
1224 mConstraints = tmp;
1225 tmp.clear();
1226 }
1227
1228// clang-format off
1229#define fixConstraint( setElement, value ) \
1230 { \
1231 for ( c = 0, cend = mConstraints.count(); c < cend; ++c ) { \
1232 mConstraints[c].setElement( value ); \
1233 } \
1234 }
1235 // clang-format on
1236 // Now determine missing values from DTSTART. This can speed up things,
1237 // because we have more restrictions and save some loops.
1238
1239 // TODO: Does RFC 2445 intend to restrict the weekday in all cases of weekly?
1240 if (mPeriod == rWeekly && mByDays.isEmpty()) {
1241 fixConstraint(setWeekday, mDateStart.date().dayOfWeek());
1242 }
1243
1244 // Really fall through in the cases, because all smaller time intervals are
1245 // constrained from dtstart
1246 switch (mPeriod) {
1247 case rYearly:
1248 if (mByDays.isEmpty() && mByWeekNumbers.isEmpty() && mByYearDays.isEmpty() && mByMonths.isEmpty()) {
1249 fixConstraint(setMonth, mDateStart.date().month());
1250 }
1251 Q_FALLTHROUGH();
1252 case rMonthly:
1253 if (mByDays.isEmpty() && mByWeekNumbers.isEmpty() && mByYearDays.isEmpty() && mByMonthDays.isEmpty()) {
1254 fixConstraint(setDay, mDateStart.date().day());
1255 }
1256 Q_FALLTHROUGH();
1257 case rWeekly:
1258 case rDaily:
1259 if (mByHours.isEmpty()) {
1260 fixConstraint(setHour, mDateStart.time().hour());
1261 }
1262 Q_FALLTHROUGH();
1263 case rHourly:
1264 if (mByMinutes.isEmpty()) {
1265 fixConstraint(setMinute, mDateStart.time().minute());
1266 }
1267 Q_FALLTHROUGH();
1268 case rMinutely:
1269 if (mBySeconds.isEmpty()) {
1270 fixConstraint(setSecond, mDateStart.time().second());
1271 }
1272 Q_FALLTHROUGH();
1273 case rSecondly:
1274 default:
1275 break;
1276 }
1277#undef fixConstraint
1278
1279 if (mNoByRules) {
1280 switch (mPeriod) {
1281 case rHourly:
1282 mTimedRepetition = mFrequency * 3600;
1283 break;
1284 case rMinutely:
1285 mTimedRepetition = mFrequency * 60;
1286 break;
1287 case rSecondly:
1288 mTimedRepetition = mFrequency;
1289 break;
1290 default:
1291 break;
1292 }
1293 } else {
1294 for (c = 0, cend = mConstraints.count(); c < cend;) {
1295 if (mConstraints[c].isConsistent(mPeriod)) {
1296 ++c;
1297 } else {
1298 mConstraints.removeAt(c);
1299 --cend;
1300 }
1301 }
1302 }
1303}
1304
1305// Build and cache a list of all occurrences.
1306// Only call buildCache() if mDuration > 0.
1307bool RecurrenceRule::Private::buildCache() const
1308{
1309 Q_ASSERT(mDuration > 0);
1310 // Build the list of all occurrences of this event (we need that to determine
1311 // the end date!)
1312 Constraint interval(getNextValidDateInterval(mDateStart, mPeriod));
1313
1314 auto dts = datesForInterval(interval, mPeriod);
1315 // Only use dates after the event has started (start date is only included
1316 // if it matches)
1317 const auto it = strictLowerBound(dts.begin(), dts.end(), mDateStart);
1318 if (it != dts.end()) {
1319 dts.erase(dts.begin(), it + 1);
1320 }
1321
1322 // some validity checks to avoid infinite loops (i.e. if we have
1323 // done this loop already 10000 times, bail out )
1324 for (int loopnr = 0; loopnr < LOOP_LIMIT && dts.count() < mDuration; ++loopnr) {
1325 interval.increase(mPeriod, mFrequency);
1326 // The returned date list is already sorted!
1327 dts += datesForInterval(interval, mPeriod);
1328 }
1329 if (dts.count() > mDuration) {
1330 // we have picked up more occurrences than necessary, remove them
1331 dts.erase(dts.begin() + mDuration, dts.end());
1332 }
1333 mCached = true;
1334 mCachedDates = dts;
1335
1336 // it = dts.begin();
1337 // while ( it != dts.end() ) {
1338 // qCDebug(KCALCORE_LOG) << " -=>" << dumpTime(*it);
1339 // ++it;
1340 // }
1341 if (int(dts.count()) == mDuration) {
1342 mCachedDateEnd = dts.last();
1343 return true;
1344 } else {
1345 // The cached date list is incomplete
1346 mCachedDateEnd = QDateTime();
1347 mCachedLastDate = interval.intervalDateTime(mPeriod);
1348 return false;
1349 }
1350}
1351//@endcond
1352
1354{
1355 QDateTime dt = kdt.toTimeZone(d->mDateStart.timeZone());
1356 for (int i = 0, iend = d->mConstraints.count(); i < iend; ++i) {
1357 if (d->mConstraints[i].matches(dt, recurrenceType())) {
1358 return true;
1359 }
1360 }
1361 return false;
1362}
1363
1364bool RecurrenceRule::recursOn(const QDate &qd, const QTimeZone &timeZone) const
1365{
1366 int i;
1367 int iend;
1368
1369 if (!qd.isValid() || !d->mDateStart.isValid()) {
1370 // There can't be recurrences on invalid dates
1371 return false;
1372 }
1373
1374 if (allDay()) {
1375 // It's a date-only rule, so it has no time specification.
1376 // Therefore ignore 'timeSpec'.
1377 if (qd < d->mDateStart.date()) {
1378 return false;
1379 }
1380 // Start date is only included if it really matches
1381 QDate endDate;
1382 if (d->mDuration >= 0) {
1383 endDate = endDt().date();
1384 if (qd > endDate) {
1385 return false;
1386 }
1387 }
1388
1389 // The date must be in an appropriate interval (getNextValidDateInterval),
1390 // Plus it must match at least one of the constraints
1391 bool match = false;
1392 for (i = 0, iend = d->mConstraints.count(); i < iend && !match; ++i) {
1393 match = d->mConstraints[i].matches(qd, recurrenceType());
1394 }
1395 if (!match) {
1396 return false;
1397 }
1398
1399 QDateTime start(qd, QTime(0, 0, 0), timeZone); // d->mDateStart.timeZone());
1400 Constraint interval(d->getNextValidDateInterval(start, recurrenceType()));
1401 // Constraint::matches is quite efficient, so first check if it can occur at
1402 // all before we calculate all actual dates.
1403 if (!interval.matches(qd, recurrenceType())) {
1404 return false;
1405 }
1406 // We really need to obtain the list of dates in this interval, since
1407 // otherwise BYSETPOS will not work (i.e. the date will match the interval,
1408 // but BYSETPOS selects only one of these matching dates!
1409 QDateTime end = start.addDays(1);
1410 do {
1411 auto dts = d->datesForInterval(interval, recurrenceType());
1412 for (i = 0, iend = dts.count(); i < iend; ++i) {
1413 if (dts[i].date() >= qd) {
1414 return dts[i].date() == qd;
1415 }
1416 }
1417 interval.increase(recurrenceType(), frequency());
1418 } while (interval.intervalDateTime(recurrenceType()) < end);
1419 return false;
1420 }
1421
1422 // It's a date-time rule, so we need to take the time specification into account.
1423 QDateTime start(qd, QTime(0, 0, 0), timeZone);
1424 QDateTime end = start.addDays(1).toTimeZone(d->mDateStart.timeZone());
1425 start = start.toTimeZone(d->mDateStart.timeZone());
1426 if (end < d->mDateStart) {
1427 return false;
1428 }
1429 if (start < d->mDateStart) {
1430 start = d->mDateStart;
1431 }
1432
1433 // Start date is only included if it really matches
1434 if (d->mDuration >= 0) {
1435 QDateTime endRecur = endDt();
1436 if (endRecur.isValid()) {
1437 if (start > endRecur) {
1438 return false;
1439 }
1440 if (end > endRecur) {
1441 end = endRecur; // limit end-of-day time to end of recurrence rule
1442 }
1443 }
1444 }
1445
1446 if (d->mTimedRepetition) {
1447 // It's a simple sub-daily recurrence with no constraints
1448 int n = static_cast<int>((d->mDateStart.secsTo(start) - 1) % d->mTimedRepetition);
1449 return start.addSecs(d->mTimedRepetition - n) < end;
1450 }
1451
1452 // Find the start and end dates in the time spec for the rule
1453 QDate startDay = start.date();
1454 QDate endDay = end.addSecs(-1).date();
1455 int dayCount = startDay.daysTo(endDay) + 1;
1456
1457 // The date must be in an appropriate interval (getNextValidDateInterval),
1458 // Plus it must match at least one of the constraints
1459 bool match = false;
1460 for (i = 0, iend = d->mConstraints.count(); i < iend && !match; ++i) {
1461 match = d->mConstraints[i].matches(startDay, recurrenceType());
1462 for (int day = 1; day < dayCount && !match; ++day) {
1463 match = d->mConstraints[i].matches(startDay.addDays(day), recurrenceType());
1464 }
1465 }
1466 if (!match) {
1467 return false;
1468 }
1469
1470 Constraint interval(d->getNextValidDateInterval(start, recurrenceType()));
1471 // Constraint::matches is quite efficient, so first check if it can occur at
1472 // all before we calculate all actual dates.
1473 Constraint intervalm = interval;
1474 do {
1475 match = intervalm.matches(startDay, recurrenceType());
1476 for (int day = 1; day < dayCount && !match; ++day) {
1477 match = intervalm.matches(startDay.addDays(day), recurrenceType());
1478 }
1479 if (match) {
1480 break;
1481 }
1482 intervalm.increase(recurrenceType(), frequency());
1483 } while (intervalm.intervalDateTime(recurrenceType()).isValid() && intervalm.intervalDateTime(recurrenceType()) < end);
1484 if (!match) {
1485 return false;
1486 }
1487
1488 // We really need to obtain the list of dates in this interval, since
1489 // otherwise BYSETPOS will not work (i.e. the date will match the interval,
1490 // but BYSETPOS selects only one of these matching dates!
1491 do {
1492 auto dts = d->datesForInterval(interval, recurrenceType());
1493 const auto it = std::lower_bound(dts.constBegin(), dts.constEnd(), start);
1494 if (it != dts.constEnd()) {
1495 return *it <= end;
1496 }
1497 interval.increase(recurrenceType(), frequency());
1498 } while (interval.intervalDateTime(recurrenceType()).isValid() && interval.intervalDateTime(recurrenceType()) < end);
1499
1500 return false;
1501}
1502
1504{
1505 // Convert to the time spec used by this recurrence rule
1506 QDateTime dt(kdt.toTimeZone(d->mDateStart.timeZone()));
1507
1508 if (allDay()) {
1509 return recursOn(dt.date(), dt.timeZone());
1510 }
1511 if (dt < d->mDateStart) {
1512 return false;
1513 }
1514 // Start date is only included if it really matches
1515 if (d->mDuration >= 0 && dt > endDt()) {
1516 return false;
1517 }
1518
1519 if (d->mTimedRepetition) {
1520 // It's a simple sub-daily recurrence with no constraints
1521 return !(d->mDateStart.secsTo(dt) % d->mTimedRepetition);
1522 }
1523
1524 // The date must be in an appropriate interval (getNextValidDateInterval),
1525 // Plus it must match at least one of the constraints
1526 if (!dateMatchesRules(dt)) {
1527 return false;
1528 }
1529 // if it recurs every interval, speed things up...
1530 // if ( d->mFrequency == 1 && d->mBySetPos.isEmpty() && d->mByDays.isEmpty() ) return true;
1531 Constraint interval(d->getNextValidDateInterval(dt, recurrenceType()));
1532 // TODO_Recurrence: Does this work with BySetPos???
1533 if (interval.matches(dt, recurrenceType())) {
1534 return true;
1535 }
1536 return false;
1537}
1538
1539TimeList RecurrenceRule::recurTimesOn(const QDate &date, const QTimeZone &timeZone) const
1540{
1541 TimeList lst;
1542 if (allDay()) {
1543 return lst;
1544 }
1545 QDateTime start(date, QTime(0, 0, 0), timeZone);
1546 QDateTime end = start.addDays(1).addSecs(-1);
1547 auto dts = timesInInterval(start, end); // returns between start and end inclusive
1548 for (int i = 0, iend = dts.count(); i < iend; ++i) {
1549 lst += dts[i].toTimeZone(timeZone).time();
1550 }
1551 return lst;
1552}
1553
1554/** Returns the number of recurrences up to and including the date/time specified. */
1556{
1557 // Convert to the time spec used by this recurrence rule
1558 QDateTime toDate(dt.toTimeZone(d->mDateStart.timeZone()));
1559 // Easy cases:
1560 // either before start, or after all recurrences and we know their number
1561 if (toDate < d->mDateStart) {
1562 return 0;
1563 }
1564 // Start date is only included if it really matches
1565 if (d->mDuration > 0 && toDate >= endDt()) {
1566 return d->mDuration;
1567 }
1568
1569 if (d->mTimedRepetition) {
1570 // It's a simple sub-daily recurrence with no constraints
1571 return static_cast<int>(d->mDateStart.secsTo(toDate) / d->mTimedRepetition);
1572 }
1573
1574 return timesInInterval(d->mDateStart, toDate).count();
1575}
1576
1577int RecurrenceRule::durationTo(const QDate &date) const
1578{
1579 return durationTo(QDateTime(date, QTime(23, 59, 59), d->mDateStart.timeZone()));
1580}
1581
1583{
1584 // Convert to the time spec used by this recurrence rule
1585 QDateTime toDate(afterDate.toTimeZone(d->mDateStart.timeZone()));
1586
1587 // Invalid starting point, or beyond end of recurrence
1588 if (!toDate.isValid() || toDate < d->mDateStart) {
1589 return QDateTime();
1590 }
1591
1592 if (d->mTimedRepetition) {
1593 // It's a simple sub-daily recurrence with no constraints
1594 QDateTime prev = toDate;
1595 if (d->mDuration >= 0 && endDt().isValid() && toDate > endDt()) {
1596 prev = endDt().addSecs(1).toTimeZone(d->mDateStart.timeZone());
1597 }
1598 int n = static_cast<int>((d->mDateStart.secsTo(prev) - 1) % d->mTimedRepetition);
1599 if (n < 0) {
1600 return QDateTime(); // before recurrence start
1601 }
1602 prev = prev.addSecs(-n - 1);
1603 return prev >= d->mDateStart ? prev : QDateTime();
1604 }
1605
1606 // If we have a cache (duration given), use that
1607 if (d->mDuration > 0) {
1608 if (!d->mCached) {
1609 d->buildCache();
1610 }
1611 const auto it = strictLowerBound(d->mCachedDates.constBegin(), d->mCachedDates.constEnd(), toDate);
1612 if (it != d->mCachedDates.constEnd()) {
1613 return *it;
1614 }
1615 return QDateTime();
1616 }
1617
1618 QDateTime prev = toDate;
1619 if (d->mDuration >= 0 && endDt().isValid() && toDate > endDt()) {
1620 prev = endDt().addSecs(1).toTimeZone(d->mDateStart.timeZone());
1621 }
1622
1623 Constraint interval(d->getPreviousValidDateInterval(prev, recurrenceType()));
1624 const auto dts = d->datesForInterval(interval, recurrenceType());
1625 const auto it = strictLowerBound(dts.begin(), dts.end(), prev);
1626 if (it != dts.end()) {
1627 return ((*it) >= d->mDateStart) ? (*it) : QDateTime();
1628 }
1629
1630 // Previous interval. As soon as we find an occurrence, we're done.
1631 while (interval.intervalDateTime(recurrenceType()) > d->mDateStart) {
1632 interval.increase(recurrenceType(), -int(frequency()));
1633 // The returned date list is sorted
1634 auto dts = d->datesForInterval(interval, recurrenceType());
1635 // The list is sorted, so take the last one.
1636 if (!dts.isEmpty()) {
1637 prev = dts.last();
1638 if (prev.isValid() && prev >= d->mDateStart) {
1639 return prev;
1640 } else {
1641 return QDateTime();
1642 }
1643 }
1644 }
1645 return QDateTime();
1646}
1647
1649{
1650 // Convert to the time spec used by this recurrence rule
1651 QDateTime fromDate(preDate.toTimeZone(d->mDateStart.timeZone()));
1652 // Beyond end of recurrence
1653 if (d->mDuration >= 0 && endDt().isValid() && fromDate >= endDt()) {
1654 return QDateTime();
1655 }
1656
1657 // Start date is only included if it really matches
1658 if (fromDate < d->mDateStart) {
1659 fromDate = d->mDateStart.addSecs(-1);
1660 }
1661
1662 if (d->mTimedRepetition) {
1663 // It's a simple sub-daily recurrence with no constraints
1664 int n = static_cast<int>((d->mDateStart.secsTo(fromDate) + 1) % d->mTimedRepetition);
1665 QDateTime next = fromDate.addSecs(d->mTimedRepetition - n + 1);
1666 return d->mDuration < 0 || !endDt().isValid() || next <= endDt() ? next : QDateTime();
1667 }
1668
1669 if (d->mDuration > 0) {
1670 if (!d->mCached) {
1671 d->buildCache();
1672 }
1673 const auto it = std::upper_bound(d->mCachedDates.constBegin(), d->mCachedDates.constEnd(), fromDate);
1674 if (it != d->mCachedDates.constEnd()) {
1675 return *it;
1676 }
1677 }
1678
1679 QDateTime end = endDt();
1680 Constraint interval(d->getNextValidDateInterval(fromDate, recurrenceType()));
1681 const auto dts = d->datesForInterval(interval, recurrenceType());
1682 const auto it = std::upper_bound(dts.begin(), dts.end(), fromDate);
1683 if (it != dts.end()) {
1684 return (d->mDuration < 0 || *it <= end) ? *it : QDateTime();
1685 }
1686 interval.increase(recurrenceType(), frequency());
1687 if (d->mDuration >= 0 && interval.intervalDateTime(recurrenceType()) > end) {
1688 return QDateTime();
1689 }
1690
1691 // Increase the interval. The first occurrence that we find is the result (if
1692 // if's before the end date).
1693 // TODO: some validity checks to avoid infinite loops for contradictory constraints
1694 int loop = 0;
1695 do {
1696 auto dts = d->datesForInterval(interval, recurrenceType());
1697 if (!dts.isEmpty()) {
1698 QDateTime ret(dts[0]);
1699 if (d->mDuration >= 0 && ret > end) {
1700 return QDateTime();
1701 } else {
1702 return ret;
1703 }
1704 }
1705 interval.increase(recurrenceType(), frequency());
1706 } while (++loop < LOOP_LIMIT && (d->mDuration < 0 || interval.intervalDateTime(recurrenceType()) < end));
1707 return QDateTime();
1708}
1709
1711{
1712 const QDateTime start = dtStart.toTimeZone(d->mDateStart.timeZone());
1713 const QDateTime end = dtEnd.toTimeZone(d->mDateStart.timeZone());
1714 QList<QDateTime> result;
1715 if (end < d->mDateStart) {
1716 return result; // before start of recurrence
1717 }
1718 QDateTime enddt = end;
1719 if (d->mDuration >= 0) {
1720 const QDateTime endRecur = endDt();
1721 if (endRecur.isValid()) {
1722 if (start > endRecur) {
1723 return result; // beyond end of recurrence
1724 }
1725 if (end >= endRecur) {
1726 enddt = endRecur; // limit end time to end of recurrence rule
1727 }
1728 }
1729 }
1730
1731 if (d->mTimedRepetition) {
1732 // It's a simple sub-daily recurrence with no constraints
1733
1734 // Seconds to add to interval start, to get first occurrence which is within interval
1735 qint64 offsetFromNextOccurrence;
1736 if (d->mDateStart < start) {
1737 offsetFromNextOccurrence = d->mTimedRepetition - (d->mDateStart.secsTo(start) % d->mTimedRepetition);
1738 } else {
1739 offsetFromNextOccurrence = -(d->mDateStart.secsTo(start) % d->mTimedRepetition);
1740 }
1741 QDateTime dt = start.addSecs(offsetFromNextOccurrence);
1742 if (dt <= enddt) {
1743 int numberOfOccurrencesWithinInterval = static_cast<int>(dt.secsTo(enddt) / d->mTimedRepetition) + 1;
1744 // limit n by a sane value else we can "explode".
1745 numberOfOccurrencesWithinInterval = qMin(numberOfOccurrencesWithinInterval, LOOP_LIMIT);
1746 for (int i = 0; i < numberOfOccurrencesWithinInterval; dt = dt.addSecs(d->mTimedRepetition), ++i) {
1747 result += dt;
1748 }
1749 }
1750 return result;
1751 }
1752
1753 QDateTime st = start < d->mDateStart ? d->mDateStart : start;
1754 bool done = false;
1755 if (d->mDuration > 0) {
1756 if (!d->mCached) {
1757 d->buildCache();
1758 }
1759 if (d->mCachedDateEnd.isValid() && start > d->mCachedDateEnd) {
1760 return result; // beyond end of recurrence
1761 }
1762 const auto it = std::lower_bound(d->mCachedDates.constBegin(), d->mCachedDates.constEnd(), start);
1763 if (it != d->mCachedDates.constEnd()) {
1764 const auto itEnd = std::upper_bound(it, d->mCachedDates.constEnd(), enddt);
1765 if (itEnd != d->mCachedDates.constEnd()) {
1766 done = true;
1767 }
1768 std::copy(it, itEnd, std::back_inserter(result));
1769 }
1770 if (d->mCachedDateEnd.isValid()) {
1771 done = true;
1772 } else if (!result.isEmpty()) {
1773 result += QDateTime(); // indicate that the returned list is incomplete
1774 done = true;
1775 }
1776 if (done) {
1777 return result;
1778 }
1779 // We don't have any result yet, but we reached the end of the incomplete cache
1780 st = d->mCachedLastDate.addSecs(1);
1781 }
1782
1783 Constraint interval(d->getNextValidDateInterval(st, recurrenceType()));
1784 int loop = 0;
1785 do {
1786 auto dts = d->datesForInterval(interval, recurrenceType());
1787 auto it = dts.begin();
1788 auto itEnd = dts.end();
1789 if (loop == 0) {
1790 it = std::lower_bound(dts.begin(), dts.end(), st);
1791 }
1792 itEnd = std::upper_bound(it, dts.end(), enddt);
1793 if (itEnd != dts.end()) {
1794 loop = LOOP_LIMIT;
1795 }
1796 std::copy(it, itEnd, std::back_inserter(result));
1797 // Increase the interval.
1798 interval.increase(recurrenceType(), frequency());
1799 } while (++loop < LOOP_LIMIT && interval.intervalDateTime(recurrenceType()) < end);
1800 return result;
1801}
1802
1803//@cond PRIVATE
1804// Find the date/time of the occurrence at or before a date/time,
1805// for a given period type.
1806// Return a constraint whose value appropriate to 'type', is set to
1807// the value contained in the date/time.
1808Constraint RecurrenceRule::Private::getPreviousValidDateInterval(const QDateTime &dt, PeriodType type) const
1809{
1810 long periods = 0;
1811 QDateTime start = mDateStart;
1812 QDateTime nextValid(start);
1813 int modifier = 1;
1814 QDateTime toDate(dt.toTimeZone(start.timeZone()));
1815 // for super-daily recurrences, don't care about the time part
1816
1817 // Find the #intervals since the dtstart and round to the next multiple of
1818 // the frequency
1819 switch (type) {
1820 // Really fall through for sub-daily, since the calculations only differ
1821 // by the factor 60 and 60*60! Same for weekly and daily (factor 7)
1822 case rHourly:
1823 modifier *= 60;
1824 Q_FALLTHROUGH();
1825 case rMinutely:
1826 modifier *= 60;
1827 Q_FALLTHROUGH();
1828 case rSecondly:
1829 periods = static_cast<int>(start.secsTo(toDate) / modifier);
1830 // round it down to the next lower multiple of frequency:
1831 if (mFrequency > 0) {
1832 periods = (periods / mFrequency) * mFrequency;
1833 }
1834 nextValid = start.addSecs(modifier * periods);
1835 break;
1836 case rWeekly:
1837 toDate = toDate.addDays(-(7 + toDate.date().dayOfWeek() - mWeekStart) % 7);
1838 start = start.addDays(-(7 + start.date().dayOfWeek() - mWeekStart) % 7);
1839 modifier *= 7;
1840 Q_FALLTHROUGH();
1841 case rDaily:
1842 periods = start.daysTo(toDate) / modifier;
1843 // round it down to the next lower multiple of frequency:
1844 if (mFrequency > 0) {
1845 periods = (periods / mFrequency) * mFrequency;
1846 }
1847 nextValid = start.addDays(modifier * periods);
1848 break;
1849 case rMonthly: {
1850 periods = 12 * (toDate.date().year() - start.date().year()) + (toDate.date().month() - start.date().month());
1851 // round it down to the next lower multiple of frequency:
1852 if (mFrequency > 0) {
1853 periods = (periods / mFrequency) * mFrequency;
1854 }
1855 // set the day to the first day of the month, so we don't have problems
1856 // with non-existent days like Feb 30 or April 31
1857 start.setDate(QDate(start.date().year(), start.date().month(), 1));
1858 nextValid.setDate(start.date().addMonths(periods));
1859 break;
1860 }
1861 case rYearly:
1862 periods = (toDate.date().year() - start.date().year());
1863 // round it down to the next lower multiple of frequency:
1864 if (mFrequency > 0) {
1865 periods = (periods / mFrequency) * mFrequency;
1866 }
1867 nextValid.setDate(start.date().addYears(periods));
1868 break;
1869 default:
1870 break;
1871 }
1872
1873 return Constraint(nextValid, type, mWeekStart);
1874}
1875
1876// Find the date/time of the next occurrence at or after a date/time,
1877// for a given period type.
1878// Return a constraint whose value appropriate to 'type', is set to the
1879// value contained in the date/time.
1880Constraint RecurrenceRule::Private::getNextValidDateInterval(const QDateTime &dt, PeriodType type) const
1881{
1882 // TODO: Simplify this!
1883 long periods = 0;
1884 QDateTime start = mDateStart.isValid() ? mDateStart : dt;
1885 QDateTime nextValid(start);
1886 int modifier = 1;
1887 QDateTime toDate(dt.toTimeZone(start.timeZone()));
1888 // for super-daily recurrences, don't care about the time part
1889
1890 // Find the #intervals since the dtstart and round to the next multiple of
1891 // the frequency
1892 switch (type) {
1893 // Really fall through for sub-daily, since the calculations only differ
1894 // by the factor 60 and 60*60! Same for weekly and daily (factor 7)
1895 case rHourly:
1896 modifier *= 60;
1897 Q_FALLTHROUGH();
1898 case rMinutely:
1899 modifier *= 60;
1900 Q_FALLTHROUGH();
1901 case rSecondly:
1902 periods = static_cast<int>(start.secsTo(toDate) / modifier);
1903 periods = qMax(0L, periods);
1904 if (periods > 0 && mFrequency > 0) {
1905 periods += (mFrequency - 1 - ((periods - 1) % mFrequency));
1906 }
1907 nextValid = start.addSecs(modifier * periods);
1908 break;
1909 case rWeekly:
1910 // correct both start date and current date to start of week
1911 toDate = toDate.addDays(-(7 + toDate.date().dayOfWeek() - mWeekStart) % 7);
1912 start = start.addDays(-(7 + start.date().dayOfWeek() - mWeekStart) % 7);
1913 modifier *= 7;
1914 Q_FALLTHROUGH();
1915 case rDaily:
1916 periods = start.daysTo(toDate) / modifier;
1917 periods = qMax(0L, periods);
1918 if (periods > 0 && mFrequency > 0) {
1919 periods += (mFrequency - 1 - ((periods - 1) % mFrequency));
1920 }
1921 nextValid = start.addDays(modifier * periods);
1922 break;
1923 case rMonthly: {
1924 periods = 12 * (toDate.date().year() - start.date().year()) + (toDate.date().month() - start.date().month());
1925 periods = qMax(0L, periods);
1926 if (periods > 0 && mFrequency > 0) {
1927 periods += (mFrequency - 1 - ((periods - 1) % mFrequency));
1928 }
1929 // set the day to the first day of the month, so we don't have problems
1930 // with non-existent days like Feb 30 or April 31
1931 start.setDate(QDate(start.date().year(), start.date().month(), 1));
1932 nextValid.setDate(start.date().addMonths(periods));
1933 break;
1934 }
1935 case rYearly:
1936 periods = (toDate.date().year() - start.date().year());
1937 periods = qMax(0L, periods);
1938 if (periods > 0 && mFrequency > 0) {
1939 periods += (mFrequency - 1 - ((periods - 1) % mFrequency));
1940 }
1941 nextValid.setDate(start.date().addYears(periods));
1942 break;
1943 default:
1944 break;
1945 }
1946
1947 return Constraint(nextValid, type, mWeekStart);
1948}
1949
1950QList<QDateTime> RecurrenceRule::Private::datesForInterval(const Constraint &interval, PeriodType type) const
1951{
1952 /* -) Loop through constraints,
1953 -) merge interval with each constraint
1954 -) if merged constraint is not consistent => ignore that constraint
1955 -) if complete => add that one date to the date list
1956 -) Loop through all missing fields => For each add the resulting
1957 */
1958 QList<QDateTime> lst;
1959 for (int i = 0, iend = mConstraints.count(); i < iend; ++i) {
1960 Constraint merged(interval);
1961 if (merged.merge(mConstraints[i])) {
1962 // If the information is incomplete, we can't use this constraint
1963 if (merged.year > 0 && merged.hour >= 0 && merged.minute >= 0 && merged.second >= 0) {
1964 // We have a valid constraint, so get all datetimes that match it and
1965 // append it to all date/times of this interval
1966 QList<QDateTime> lstnew = merged.dateTimes(type);
1967 lst += lstnew;
1968 }
1969 }
1970 }
1971 // Sort it so we can apply the BySetPos. Also some logic relies on this being sorted
1972 sortAndRemoveDuplicates(lst);
1973
1974 /*if ( lst.isEmpty() ) {
1975 qCDebug(KCALCORE_LOG) << " No Dates in Interval";
1976 } else {
1977 qCDebug(KCALCORE_LOG) << " Dates:";
1978 for ( int i = 0, iend = lst.count(); i < iend; ++i ) {
1979 qCDebug(KCALCORE_LOG)<< " -)" << dumpTime(lst[i]);
1980 }
1981 qCDebug(KCALCORE_LOG) << " ---------------------";
1982 }*/
1983 if (!mBySetPos.isEmpty()) {
1984 auto tmplst = lst;
1985 lst.clear();
1986 for (int i = 0, iend = mBySetPos.count(); i < iend; ++i) {
1987 int pos = mBySetPos[i];
1988 if (pos > 0) {
1989 --pos;
1990 }
1991 if (pos < 0) {
1992 pos += tmplst.count();
1993 }
1994 if (pos >= 0 && pos < tmplst.count()) {
1995 lst.append(tmplst[pos]);
1996 }
1997 }
1998 sortAndRemoveDuplicates(lst);
1999 }
2000
2001 return lst;
2002}
2003//@endcond
2004
2006{
2007#ifndef NDEBUG
2008 if (!d->mRRule.isEmpty()) {
2009 qCDebug(KCALCORE_LOG) << " RRULE=" << d->mRRule;
2010 }
2011 qCDebug(KCALCORE_LOG) << " Read-Only:" << isReadOnly();
2012
2013 qCDebug(KCALCORE_LOG) << " Period type:" << int(recurrenceType()) << ", frequency:" << frequency();
2014 qCDebug(KCALCORE_LOG) << " #occurrences:" << duration();
2015 qCDebug(KCALCORE_LOG) << " start date:" << dumpTime(startDt(), allDay()) << ", end date:" << dumpTime(endDt(), allDay());
2016// clang-format off
2017#define dumpByIntList(list,label) \
2018 if ( !list.isEmpty() ) {\
2019 QStringList lst;\
2020 for ( int i = 0, iend = list.count(); i < iend; ++i ) {\
2021 lst.append( QString::number( list[i] ) );\
2022 }\
2023 qCDebug(KCALCORE_LOG) << " " << label << lst.join(QLatin1String(", ") );\
2024 }
2025 // clang-format on
2026 dumpByIntList(d->mBySeconds, QStringLiteral("BySeconds: "));
2027 dumpByIntList(d->mByMinutes, QStringLiteral("ByMinutes: "));
2028 dumpByIntList(d->mByHours, QStringLiteral("ByHours: "));
2029 if (!d->mByDays.isEmpty()) {
2030 QStringList lst;
2031 for (int i = 0, iend = d->mByDays.count(); i < iend; ++i) {
2032 lst.append((d->mByDays[i].pos() ? QString::number(d->mByDays[i].pos()) : QLatin1String("")) + DateHelper::dayName(d->mByDays[i].day()));
2033 }
2034 qCDebug(KCALCORE_LOG) << " ByDays: " << lst.join(QLatin1String(", "));
2035 }
2036 dumpByIntList(d->mByMonthDays, QStringLiteral("ByMonthDays:"));
2037 dumpByIntList(d->mByYearDays, QStringLiteral("ByYearDays: "));
2038 dumpByIntList(d->mByWeekNumbers, QStringLiteral("ByWeekNr: "));
2039 dumpByIntList(d->mByMonths, QStringLiteral("ByMonths: "));
2040 dumpByIntList(d->mBySetPos, QStringLiteral("BySetPos: "));
2041#undef dumpByIntList
2042
2043 qCDebug(KCALCORE_LOG) << " Week start:" << DateHelper::dayName(d->mWeekStart);
2044
2045 qCDebug(KCALCORE_LOG) << " Constraints:";
2046 // dump constraints
2047 for (int i = 0, iend = d->mConstraints.count(); i < iend; ++i) {
2048 d->mConstraints[i].dump();
2049 }
2050#endif
2051}
2052
2053//@cond PRIVATE
2054void Constraint::dump() const
2055{
2056 qCDebug(KCALCORE_LOG) << " ~> Y=" << year << ", M=" << month << ", D=" << day << ", H=" << hour << ", m=" << minute << ", S=" << second
2057 << ", wd=" << weekday << ",#wd=" << weekdaynr << ", #w=" << weeknumber << ", yd=" << yearday;
2058}
2059//@endcond
2060
2061QString dumpTime(const QDateTime &dt, bool isAllDay)
2062{
2063#ifndef NDEBUG
2064 if (!dt.isValid()) {
2065 return QString();
2066 }
2067 QString result;
2068 if (isAllDay) {
2069 result = dt.toString(QStringLiteral("ddd yyyy-MM-dd t"));
2070 } else {
2071 result = dt.toString(QStringLiteral("ddd yyyy-MM-dd hh:mm:ss t"));
2072 }
2073 return result;
2074#else
2075 Q_UNUSED(dt);
2076 Q_UNUSED(isAllDay);
2077 return QString();
2078#endif
2079}
2080
2082{
2083 return d->mDateStart;
2084}
2085
2086RecurrenceRule::PeriodType RecurrenceRule::recurrenceType() const
2087{
2088 return d->mPeriod;
2089}
2090
2092{
2093 return d->mFrequency;
2094}
2095
2097{
2098 return d->mDuration;
2099}
2100
2101QString RecurrenceRule::rrule() const
2102{
2103 return d->mRRule;
2104}
2105
2107{
2108 d->mRRule = rrule;
2109}
2110
2112{
2113 return d->mIsReadOnly;
2114}
2115
2117{
2118 d->mIsReadOnly = readOnly;
2119}
2120
2122{
2123 return d->mPeriod != rNone;
2124}
2125
2127{
2128 return d->mAllDay;
2129}
2130
2131const QList<int> &RecurrenceRule::bySeconds() const
2132{
2133 return d->mBySeconds;
2134}
2135
2136const QList<int> &RecurrenceRule::byMinutes() const
2137{
2138 return d->mByMinutes;
2139}
2140
2141const QList<int> &RecurrenceRule::byHours() const
2142{
2143 return d->mByHours;
2144}
2145
2146const QList<RecurrenceRule::WDayPos> &RecurrenceRule::byDays() const
2147{
2148 return d->mByDays;
2149}
2150
2151const QList<int> &RecurrenceRule::byMonthDays() const
2152{
2153 return d->mByMonthDays;
2154}
2155
2156const QList<int> &RecurrenceRule::byYearDays() const
2157{
2158 return d->mByYearDays;
2159}
2160
2161const QList<int> &RecurrenceRule::byWeekNumbers() const
2162{
2163 return d->mByWeekNumbers;
2164}
2165
2166const QList<int> &RecurrenceRule::byMonths() const
2167{
2168 return d->mByMonths;
2169}
2170
2171const QList<int> &RecurrenceRule::bySetPos() const
2172{
2173 return d->mBySetPos;
2174}
2175
2176short RecurrenceRule::weekStart() const
2177{
2178 return d->mWeekStart;
2179}
2180
2181RecurrenceRule::RuleObserver::~RuleObserver()
2182{
2183}
2184
2185RecurrenceRule::WDayPos::WDayPos(int ps, short dy)
2186 : mDay(dy)
2187 , mPos(ps)
2188{
2189}
2190
2191void RecurrenceRule::WDayPos::setDay(short dy)
2192{
2193 mDay = dy;
2194}
2195
2196short RecurrenceRule::WDayPos::day() const
2197{
2198 return mDay;
2199}
2200
2201void RecurrenceRule::WDayPos::setPos(int ps)
2202{
2203 mPos = ps;
2204}
2205
2206int RecurrenceRule::WDayPos::pos() const
2207{
2208 return mPos;
2209}
2210
2211QDataStream &operator<<(QDataStream &out, const Constraint &c)
2212{
2213 out << c.year << c.month << c.day << c.hour << c.minute << c.second << c.weekday << c.weekdaynr << c.weeknumber << c.yearday << c.weekstart;
2214 serializeQTimeZoneAsSpec(out, c.timeZone);
2215 out << false; // for backwards compatibility
2216
2217 return out;
2218}
2219
2220QDataStream &operator>>(QDataStream &in, Constraint &c)
2221{
2222 bool secondOccurrence; // no longer used
2223 in >> c.year >> c.month >> c.day >> c.hour >> c.minute >> c.second >> c.weekday >> c.weekdaynr >> c.weeknumber >> c.yearday >> c.weekstart;
2224 deserializeSpecAsQTimeZone(in, c.timeZone);
2225 in >> secondOccurrence;
2226 return in;
2227}
2228
2230{
2231 out << w.mDay << w.mPos;
2232 return out;
2233}
2234
2236{
2237 in >> w.mDay >> w.mPos;
2238 return in;
2239}
2240
2242{
2243 if (!r) {
2244 return out;
2245 }
2246
2247 RecurrenceRule::Private *d = r->d;
2248 out << d->mRRule << static_cast<quint32>(d->mPeriod);
2249 serializeQDateTimeAsKDateTime(out, d->mDateStart);
2250 out << d->mFrequency << d->mDuration;
2251 serializeQDateTimeAsKDateTime(out, d->mDateEnd);
2252 out << d->mBySeconds << d->mByMinutes << d->mByHours << d->mByDays << d->mByMonthDays << d->mByYearDays << d->mByWeekNumbers << d->mByMonths << d->mBySetPos
2253 << d->mWeekStart << d->mConstraints << d->mAllDay << d->mNoByRules << d->mTimedRepetition << d->mIsReadOnly;
2254
2255 return out;
2256}
2257
2259{
2260 if (!r) {
2261 return in;
2262 }
2263
2264 RecurrenceRule::Private *d = r->d;
2265 quint32 period;
2266 in >> d->mRRule >> period;
2267 deserializeKDateTimeAsQDateTime(in, d->mDateStart);
2268 in >> d->mFrequency >> d->mDuration;
2269 deserializeKDateTimeAsQDateTime(in, d->mDateEnd);
2270 in >> d->mBySeconds >> d->mByMinutes >> d->mByHours >> d->mByDays >> d->mByMonthDays >> d->mByYearDays >> d->mByWeekNumbers >> d->mByMonths >> d->mBySetPos
2271 >> d->mWeekStart >> d->mConstraints >> d->mAllDay >> d->mNoByRules >> d->mTimedRepetition >> d->mIsReadOnly;
2272
2273 d->mPeriod = static_cast<RecurrenceRule::PeriodType>(period);
2274
2275 return in;
2276}
structure for describing the n-th weekday of the month/year.
This class represents a recurrence rule for a calendar incidence.
void setAllDay(bool allDay)
Sets whether the dtstart is all-day (i.e.
void setReadOnly(bool readOnly)
Set if recurrence is read-only or can be changed.
QDateTime startDt() const
Returns the recurrence start date/time.
QDateTime endDt(bool *result=nullptr) const
Returns the date and time of the last recurrence.
QDateTime getNextDate(const QDateTime &preDateTime) const
Returns the date and time of the next recurrence, after the specified date/time.
void clear()
Turns off recurrence for the event.
void setDuration(int duration)
Sets the total number of times the event is to occur, including both the first and last.
bool allDay() const
Returns whether the start date has no time associated.
void dump() const
Debug output.
bool recurs() const
Returns the event's recurrence status.
void setFrequency(int freq)
Sets the recurrence frequency, in terms of the recurrence time period type.
void setEndDt(const QDateTime &endDateTime)
Sets the date and time of the last recurrence.
QDateTime getPreviousDate(const QDateTime &afterDateTime) const
Returns the date and time of the last previous recurrence, before the specified date/time.
RecurrenceRule()
/************************************************************************** RecurrenceRule *
uint frequency() const
Returns the recurrence frequency, in terms of the recurrence time period type.
int duration() const
Returns -1 if the event recurs infinitely, 0 if the end date is set, otherwise the total number of re...
QList< QDateTime > timesInInterval(const QDateTime &start, const QDateTime &end) const
Returns a list of all the times at which the recurrence will occur between two specified times.
PeriodType
enum for describing the frequency how an event recurs, if at all.
bool recursOn(const QDate &date, const QTimeZone &timeZone) const
Returns true if the date specified is one on which the event will recur.
bool dateMatchesRules(const QDateTime &dt) const
Returns true if the date matches the rules.
void setRRule(const QString &rrule)
Set the RRULE string for the rule.
void setStartDt(const QDateTime &start)
Sets the recurrence start date/time.
int durationTo(const QDateTime &dt) const
Returns the number of recurrences up to and including the date/time specified.
bool recursAt(const QDateTime &dt) const
Returns true if the date/time specified is one at which the event will recur.
bool isReadOnly() const
Returns true if the recurrence is read-only; false if it can be changed.
void addObserver(RuleObserver *observer)
Installs an observer.
TimeList recurTimesOn(const QDate &date, const QTimeZone &timeZone) const
Returns a list of the times on the specified date at which the recurrence will occur.
void shiftTimes(const QTimeZone &oldTz, const QTimeZone &newTz)
Shift the times of the rule so that they appear at the same clock time as before but in a new time zo...
friend KCALENDARCORE_EXPORT QDataStream & operator<<(QDataStream &out, const KCalendarCore::RecurrenceRule *)
RecurrenceRule serializer and deserializer.
void removeObserver(RuleObserver *observer)
Removes an observer that was added with addObserver.
Q_SCRIPTABLE Q_NOREPLY void start()
This file is part of the API for handling calendar data and defines the IncidenceBase class.
Type type(const QSqlDatabase &db)
Namespace for all KCalendarCore types.
Definition alarm.h:37
KCALENDARCORE_EXPORT QDataStream & operator>>(QDataStream &in, const KCalendarCore::Alarm::Ptr &)
Alarm deserializer.
Definition alarm.cpp:833
KCALENDARCORE_EXPORT QDataStream & operator<<(QDataStream &out, const KCalendarCore::Alarm::Ptr &)
Alarm serializer.
Definition alarm.cpp:820
KCALENDARCORE_EXPORT bool identical(const QDateTime &dt1, const QDateTime &dt2)
Compare two QDateTimes for extended equality.
QStringView merge(QStringView lhs, QStringView rhs)
KIOCORE_EXPORT QStringList list(const QString &fileClass)
bool operator==(const StyleDelim &l, const StyleDelim &r)
QDate addDays(qint64 ndays) const const
int day() const const
int dayOfWeek() const const
int dayOfYear() const const
int daysInMonth() const const
int daysInYear() const const
qint64 daysTo(QDate d) const const
bool isValid(int year, int month, int day)
int month() const const
bool setDate(int year, int month, int day)
int year() const const
QDateTime addDays(qint64 ndays) const const
QDateTime addMonths(int nmonths) const const
QDateTime addSecs(qint64 s) const const
QDateTime addYears(int nyears) const const
QDate date() const const
bool isValid() const const
qint64 secsTo(const QDateTime &other) const const
QTime time() const const
QTimeZone timeZone() const const
QString toString(QStringView format, QCalendar cal) const const
QDateTime toTimeZone(const QTimeZone &timeZone) const const
void append(QList< T > &&value)
void clear()
qsizetype count() const const
bool isEmpty() const const
QString number(double n, char format, int precision)
QString join(QChar separator) const const
int hour() const const
int minute() const const
int second() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Sat Dec 21 2024 16:57:05 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.