KCalendarCore

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

KDE's Doxygen guidelines are available online.