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 
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 <QStringList>
18 #include <QTime>
19 #include <QVector>
20 
21 using namespace KCalendarCore;
22 
23 // Maximum number of intervals to process
24 const int LOOP_LIMIT = 10000;
25 
26 #ifndef NDEBUG
27 static 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
46 class DateHelper
47 {
48 public:
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
75 QString 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 
98 QDate 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 
116 int 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 
146 int 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
154 int 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 
165 bool RecurrenceRule::WDayPos::operator==(const RecurrenceRule::WDayPos &pos2) const
166 {
167  return mDay == pos2.mDay && mPos == pos2.mPos;
168 }
169 
170 bool RecurrenceRule::WDayPos::operator!=(const RecurrenceRule::WDayPos &pos2) const
171 {
172  return !operator==(pos2);
173 }
174 
175 /**************************************************************************
176  * Constraint *
177  **************************************************************************/
178 //@cond PRIVATE
179 class Constraint
180 {
181 public:
182  typedef QVector<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 
270 private:
271  mutable bool useCachedDt;
272  mutable QDateTime cachedDt;
273 };
274 
275 Constraint::Constraint(const QTimeZone &timeZone, int wkst)
276  : weekstart(wkst)
277  , timeZone(timeZone)
278 {
279  clear();
280 }
281 
282 Constraint::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 
290 void 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 
305 bool 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  */
374 bool 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 
383 bool 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).
391 QDateTime 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 
434 bool 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 
482 QList<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 
600 void 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 
608 bool 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.
647 bool 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
686 class Q_DECL_HIDDEN KCalendarCore::RecurrenceRule::Private
687 {
688 public:
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 
753 RecurrenceRule::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 
782 RecurrenceRule::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 
816 bool 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 
825 void 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 
846 void 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 
873 RecurrenceRule::~RecurrenceRule()
874 {
875  delete d;
876 }
877 
878 bool RecurrenceRule::operator==(const RecurrenceRule &r) const
879 {
880  return *d == *r.d;
881 }
882 
883 RecurrenceRule &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 
895 void RecurrenceRule::addObserver(RuleObserver *observer)
896 {
897  if (!d->mObservers.contains(observer)) {
898  d->mObservers.append(observer);
899  }
900 }
901 
902 void RecurrenceRule::removeObserver(RuleObserver *observer)
903 {
904  d->mObservers.removeAll(observer);
905 }
906 
907 void RecurrenceRule::setRecurrenceType(PeriodType period)
908 {
909  if (isReadOnly()) {
910  return;
911  }
912  d->mPeriod = period;
913  d->setDirty();
914 }
915 
916 QDateTime RecurrenceRule::endDt(bool *result) const
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 
947 void RecurrenceRule::setEndDt(const QDateTime &dateTime)
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 
959 void RecurrenceRule::setDuration(int duration)
960 {
961  if (isReadOnly()) {
962  return;
963  }
964  d->mDuration = duration;
965  d->setDirty();
966 }
967 
968 void RecurrenceRule::setAllDay(bool allDay)
969 {
970  if (isReadOnly()) {
971  return;
972  }
973  d->mAllDay = allDay;
974  d->setDirty();
975 }
976 
978 {
979  d->clear();
980 }
981 
982 void 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 
1005 void RecurrenceRule::setBySeconds(const QList<int> &bySeconds)
1006 {
1007  if (isReadOnly()) {
1008  return;
1009  }
1010  d->mBySeconds = bySeconds;
1011  d->setDirty();
1012 }
1013 
1014 void RecurrenceRule::setByMinutes(const QList<int> &byMinutes)
1015 {
1016  if (isReadOnly()) {
1017  return;
1018  }
1019  d->mByMinutes = byMinutes;
1020  d->setDirty();
1021 }
1022 
1023 void RecurrenceRule::setByHours(const QList<int> &byHours)
1024 {
1025  if (isReadOnly()) {
1026  return;
1027  }
1028  d->mByHours = byHours;
1029  d->setDirty();
1030 }
1031 
1032 void RecurrenceRule::setByDays(const QList<WDayPos> &byDays)
1033 {
1034  if (isReadOnly()) {
1035  return;
1036  }
1037  d->mByDays = byDays;
1038  d->setDirty();
1039 }
1040 
1041 void RecurrenceRule::setByMonthDays(const QList<int> &byMonthDays)
1042 {
1043  if (isReadOnly()) {
1044  return;
1045  }
1046  d->mByMonthDays = byMonthDays;
1047  d->setDirty();
1048 }
1049 
1050 void RecurrenceRule::setByYearDays(const QList<int> &byYearDays)
1051 {
1052  if (isReadOnly()) {
1053  return;
1054  }
1055  d->mByYearDays = byYearDays;
1056  d->setDirty();
1057 }
1058 
1059 void RecurrenceRule::setByWeekNumbers(const QList<int> &byWeekNumbers)
1060 {
1061  if (isReadOnly()) {
1062  return;
1063  }
1064  d->mByWeekNumbers = byWeekNumbers;
1065  d->setDirty();
1066 }
1067 
1068 void RecurrenceRule::setByMonths(const QList<int> &byMonths)
1069 {
1070  if (isReadOnly()) {
1071  return;
1072  }
1073  d->mByMonths = byMonths;
1074  d->setDirty();
1075 }
1076 
1077 void RecurrenceRule::setBySetPos(const QList<int> &bySetPos)
1078 {
1079  if (isReadOnly()) {
1080  return;
1081  }
1082  d->mBySetPos = bySetPos;
1083  d->setDirty();
1084 }
1085 
1086 void RecurrenceRule::setWeekStart(short weekStart)
1087 {
1088  if (isReadOnly()) {
1089  return;
1090  }
1091  d->mWeekStart = weekStart;
1092  d->setDirty();
1093 }
1094 
1095 void 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
1163 void 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.
1307 bool 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 
1364 bool 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 
1503 bool RecurrenceRule::recursAt(const QDateTime &kdt) const
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 
1539 TimeList 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 
1577 int 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.
1808 Constraint 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.
1880 Constraint RecurrenceRule::Private::getNextValidDateInterval(const QDateTime &dt, PeriodType type) const
1881 {
1882  // TODO: Simplify this!
1883  long periods = 0;
1884  QDateTime start = mDateStart;
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 
1950 QList<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
2054 void 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 
2061 QString 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 
2086 RecurrenceRule::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 
2101 QString RecurrenceRule::rrule() const
2102 {
2103  return d->mRRule;
2104 }
2105 
2107 {
2108  d->mRRule = rrule;
2109 }
2110 
2112 {
2113  return d->mIsReadOnly;
2114 }
2115 
2116 void RecurrenceRule::setReadOnly(bool readOnly)
2117 {
2118  d->mIsReadOnly = readOnly;
2119 }
2120 
2122 {
2123  return d->mPeriod != rNone;
2124 }
2125 
2127 {
2128  return d->mAllDay;
2129 }
2130 
2131 const QList<int> &RecurrenceRule::bySeconds() const
2132 {
2133  return d->mBySeconds;
2134 }
2135 
2136 const QList<int> &RecurrenceRule::byMinutes() const
2137 {
2138  return d->mByMinutes;
2139 }
2140 
2141 const QList<int> &RecurrenceRule::byHours() const
2142 {
2143  return d->mByHours;
2144 }
2145 
2146 const QList<RecurrenceRule::WDayPos> &RecurrenceRule::byDays() const
2147 {
2148  return d->mByDays;
2149 }
2150 
2151 const QList<int> &RecurrenceRule::byMonthDays() const
2152 {
2153  return d->mByMonthDays;
2154 }
2155 
2156 const QList<int> &RecurrenceRule::byYearDays() const
2157 {
2158  return d->mByYearDays;
2159 }
2160 
2161 const QList<int> &RecurrenceRule::byWeekNumbers() const
2162 {
2163  return d->mByWeekNumbers;
2164 }
2165 
2166 const QList<int> &RecurrenceRule::byMonths() const
2167 {
2168  return d->mByMonths;
2169 }
2170 
2171 const QList<int> &RecurrenceRule::bySetPos() const
2172 {
2173  return d->mBySetPos;
2174 }
2175 
2176 short RecurrenceRule::weekStart() const
2177 {
2178  return d->mWeekStart;
2179 }
2180 
2181 RecurrenceRule::RuleObserver::~RuleObserver()
2182 {
2183 }
2184 
2185 RecurrenceRule::WDayPos::WDayPos(int ps, short dy)
2186  : mDay(dy)
2187  , mPos(ps)
2188 {
2189 }
2190 
2191 void RecurrenceRule::WDayPos::setDay(short dy)
2192 {
2193  mDay = dy;
2194 }
2195 
2196 short RecurrenceRule::WDayPos::day() const
2197 {
2198  return mDay;
2199 }
2200 
2201 void RecurrenceRule::WDayPos::setPos(int ps)
2202 {
2203  mPos = ps;
2204 }
2205 
2206 int RecurrenceRule::WDayPos::pos() const
2207 {
2208  return mPos;
2209 }
2210 
2211 QDataStream &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 
2220 QDataStream &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 }
void append(const T &value)
void removeObserver(RuleObserver *observer)
Removes an observer that was added with addObserver.
QDateTime addSecs(qint64 s) const const
int month() const const
void setAllDay(bool allDay)
Sets whether the dtstart is all-day (i.e.
KCALENDARCORE_EXPORT QDataStream & operator<<(QDataStream &out, const KCalendarCore::Alarm::Ptr &)
Alarm serializer.
Definition: alarm.cpp:820
QString number(int n, int base)
QTimeZone timeZone() const const
Type type(const QSqlDatabase &db)
void setRRule(const QString &rrule)
Set the RRULE string for the rule.
int count(const T &value) const const
QDateTime addDays(qint64 ndays) const const
Namespace for all KCalendarCore types.
Definition: alarm.h:36
QTime time() const const
int year() const const
QDateTime getPreviousDate(const QDateTime &afterDateTime) const
Returns the date and time of the last previous recurrence, before the specified date/time.
RecurrenceRule()
/************************************************************************** RecurrenceRule *
KCALENDARCORE_EXPORT bool identical(QDateTime, QDateTime)
Compare two QDateTimes for extended equality.
KCALENDARCORE_EXPORT QDataStream & operator>>(QDataStream &in, const KCalendarCore::Alarm::Ptr &)
Alarm deserializer.
Definition: alarm.cpp:833
void setReadOnly(bool readOnly)
Set if recurrence is read-only or can be changed.
KIOFILEWIDGETS_EXPORT QStringList list(const QString &fileClass)
void addObserver(RuleObserver *observer)
Installs an observer.
QDateTime endDt(bool *result=nullptr) const
Returns the date and time of the last recurrence.
QDateTime toTimeZone(const QTimeZone &timeZone) const const
friend KCALENDARCORE_EXPORT QDataStream & operator<<(QDataStream &out, const KCalendarCore::RecurrenceRule *)
RecurrenceRule serializer and deserializer.
uint frequency() const
Returns the recurrence frequency, in terms of the recurrence time period type.
Q_SCRIPTABLE Q_NOREPLY void start()
bool operator==(const Qt3DRender::QGraphicsApiFilter &reference, const Qt3DRender::QGraphicsApiFilter &sample)
bool recursOn(const QDate &date, const QTimeZone &timeZone) const
Returns true if the date specified is one on which the event will recur.
structure for describing the n-th weekday of the month/year.
qint64 secsTo(const QDateTime &other) const const
int dayOfYear() const const
void dump() const
Debug output.
QStringView merge(QStringView lhs, QStringView rhs)
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...
bool isValid() const const
QDateTime getNextDate(const QDateTime &preDateTime) const
Returns the date and time of the next recurrence, after the specified date/time.
bool isEmpty() const const
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.
QDateTime addMonths(int nmonths) const const
QString join(const QString &separator) const const
QDate addDays(qint64 ndays) const const
QDateTime addYears(int nyears) const const
void setFrequency(int freq)
Sets the recurrence frequency, in terms of the recurrence time period type.
bool recursAt(const QDateTime &dt) const
Returns true if the date/time specified is one at which the event will recur.
This class represents a recurrence rule for a calendar incidence.
int hour() const const
void setDuration(int duration)
Sets the total number of times the event is to occur, including both the first and last.
int daysInMonth() const const
void setEndDt(const QDateTime &endDateTime)
Sets the date and time of the last recurrence.
QAction * clear(const QObject *recvr, const char *slot, QObject *parent)
int duration() const
Returns -1 if the event recurs infinitely, 0 if the end date is set, otherwise the total number of re...
bool isReadOnly() const
Returns true if the recurrence is read-only; false if it can be changed.
bool dateMatchesRules(const QDateTime &dt) const
Returns true if the date matches the rules.
int second() const const
bool allDay() const
Returns whether the start date has no time associated.
PeriodType
enum for describing the frequency how an event recurs, if at all.
void clear()
Removes all recurrence and exception rules and dates.
Definition: recurrence.cpp:565
QDate date() const const
bool isValid() const const
void clear()
int durationTo(const QDateTime &dt) const
Returns the number of recurrences up to and including the date/time specified.
bool setDate(int year, int month, int day)
QDateTime startDt() const
Returns the recurrence start date/time.
void setStartDt(const QDateTime &start)
Sets the recurrence start date/time.
QString toString(Qt::DateFormat format) const const
int dayOfWeek() const const
int daysInYear() const const
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 minute() const const
void clear()
Turns off recurrence for the event.
qint64 daysTo(const QDate &d) const const
bool recurs() const
Returns the event's recurrence status.
int day() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2023 The KDE developers.
Generated on Thu Sep 21 2023 04:00:46 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.