File indexing completed on 2024-04-21 03:52:53

0001 /*
0002   This file is part of the kcalcore library.
0003 
0004   SPDX-FileCopyrightText: 2005 Reinhold Kainhofer <reinhold@kainhofe.com>
0005   SPDX-FileCopyrightText: 2006-2008 David Jarvie <djarvie@kde.org>
0006 
0007   SPDX-License-Identifier: LGPL-2.0-or-later
0008 */
0009 
0010 #include "incidencebase.h"
0011 #include "recurrencerule.h"
0012 #include "kcalendarcore_debug.h"
0013 #include "recurrencehelper_p.h"
0014 #include "utils_p.h"
0015 
0016 #include <QDataStream>
0017 #include <QList>
0018 #include <QStringList>
0019 #include <QTime>
0020 
0021 using namespace KCalendarCore;
0022 
0023 // Maximum number of intervals to process
0024 const int LOOP_LIMIT = 10000;
0025 
0026 #ifndef NDEBUG
0027 static QString dumpTime(const QDateTime &dt, bool allDay); // for debugging
0028 #endif
0029 
0030 /*=========================================================================
0031 =                                                                         =
0032 = IMPORTANT CODING NOTE:                                                  =
0033 =                                                                         =
0034 = Recurrence handling code is time critical, especially for sub-daily     =
0035 = recurrences. For example, if getNextDate() is called repeatedly to      =
0036 = check all consecutive occurrences over a few years, on a slow machine   =
0037 = this could take many seconds to complete in the worst case. Simple      =
0038 = sub-daily recurrences are optimised by use of mTimedRepetition.         =
0039 =                                                                         =
0040 ==========================================================================*/
0041 
0042 /**************************************************************************
0043  *                               DateHelper                               *
0044  **************************************************************************/
0045 //@cond PRIVATE
0046 class DateHelper
0047 {
0048 public:
0049 #ifndef NDEBUG
0050     static QString dayName(short day);
0051 #endif
0052     static QDate getNthWeek(int year, int weeknumber, short weekstart = 1);
0053     static int weekNumbersInYear(int year, short weekstart = 1);
0054     static int getWeekNumber(const QDate &date, short weekstart, int *year = nullptr);
0055     static int getWeekNumberNeg(const QDate &date, short weekstart, int *year = nullptr);
0056     // Convert to QDate, allowing for day < 0.
0057     // month and day must be non-zero.
0058     static QDate getDate(int year, int month, int day)
0059     {
0060         if (day >= 0) {
0061             return QDate(year, month, day);
0062         } else {
0063             if (++month > 12) {
0064                 month = 1;
0065                 ++year;
0066             }
0067             return QDate(year, month, 1).addDays(day);
0068         }
0069     }
0070 };
0071 
0072 #ifndef NDEBUG
0073 // TODO: Move to a general library / class, as we need the same in the iCal
0074 //       generator and in the xcal format
0075 QString DateHelper::dayName(short day)
0076 {
0077     switch (day) {
0078     case 1:
0079         return QStringLiteral("MO");
0080     case 2:
0081         return QStringLiteral("TU");
0082     case 3:
0083         return QStringLiteral("WE");
0084     case 4:
0085         return QStringLiteral("TH");
0086     case 5:
0087         return QStringLiteral("FR");
0088     case 6:
0089         return QStringLiteral("SA");
0090     case 7:
0091         return QStringLiteral("SU");
0092     default:
0093         return QStringLiteral("??");
0094     }
0095 }
0096 #endif
0097 
0098 QDate DateHelper::getNthWeek(int year, int weeknumber, short weekstart)
0099 {
0100     if (weeknumber == 0) {
0101         return QDate();
0102     }
0103 
0104     // Adjust this to the first day of week #1 of the year and add 7*weekno days.
0105     QDate dt(year, 1, 4); // Week #1 is the week that contains Jan 4
0106     int adjust = -(7 + dt.dayOfWeek() - weekstart) % 7;
0107     if (weeknumber > 0) {
0108         dt = dt.addDays(7 * (weeknumber - 1) + adjust);
0109     } else if (weeknumber < 0) {
0110         dt = dt.addYears(1);
0111         dt = dt.addDays(7 * weeknumber + adjust);
0112     }
0113     return dt;
0114 }
0115 
0116 int DateHelper::getWeekNumber(const QDate &date, short weekstart, int *year)
0117 {
0118     int y = date.year();
0119     QDate dt(y, 1, 4); // <= definitely in week #1
0120     dt = dt.addDays(-(7 + dt.dayOfWeek() - weekstart) % 7); // begin of week #1
0121 
0122     qint64 daysto = dt.daysTo(date);
0123     if (daysto < 0) {
0124         // in first week of year
0125         --y;
0126         dt = QDate(y, 1, 4);
0127         dt = dt.addDays(-(7 + dt.dayOfWeek() - weekstart) % 7); // begin of week #1
0128         daysto = dt.daysTo(date);
0129     } else if (daysto > 355) {
0130         // near the end of the year - check if it's next year
0131         QDate dtn(y + 1, 1, 4); // <= definitely first week of next year
0132         dtn = dtn.addDays(-(7 + dtn.dayOfWeek() - weekstart) % 7);
0133         qint64 dayston = dtn.daysTo(date);
0134         if (dayston >= 0) {
0135             // in first week of next year;
0136             ++y;
0137             daysto = dayston;
0138         }
0139     }
0140     if (year) {
0141         *year = y;
0142     }
0143     return daysto / 7 + 1;
0144 }
0145 
0146 int DateHelper::weekNumbersInYear(int year, short weekstart)
0147 {
0148     QDate dt(year, 1, weekstart);
0149     QDate dt1(year + 1, 1, weekstart);
0150     return dt.daysTo(dt1) / 7;
0151 }
0152 
0153 // Week number from the end of the year
0154 int DateHelper::getWeekNumberNeg(const QDate &date, short weekstart, int *year)
0155 {
0156     int weekpos = getWeekNumber(date, weekstart, year);
0157     return weekNumbersInYear(*year, weekstart) - weekpos - 1;
0158 }
0159 //@endcond
0160 
0161 /**************************************************************************
0162  *                               WDayPos                                  *
0163  **************************************************************************/
0164 
0165 bool RecurrenceRule::WDayPos::operator==(const RecurrenceRule::WDayPos &pos2) const
0166 {
0167     return mDay == pos2.mDay && mPos == pos2.mPos;
0168 }
0169 
0170 bool RecurrenceRule::WDayPos::operator!=(const RecurrenceRule::WDayPos &pos2) const
0171 {
0172     return !operator==(pos2);
0173 }
0174 
0175 /**************************************************************************
0176  *                               Constraint                               *
0177  **************************************************************************/
0178 //@cond PRIVATE
0179 class Constraint
0180 {
0181 public:
0182     typedef QList<Constraint> List;
0183 
0184     Constraint()
0185     {
0186     }
0187     explicit Constraint(const QTimeZone &, int wkst = 1);
0188     Constraint(const QDateTime &dt, RecurrenceRule::PeriodType type, int wkst);
0189     void clear();
0190     void setYear(int n)
0191     {
0192         year = n;
0193         useCachedDt = false;
0194     }
0195     void setMonth(int n)
0196     {
0197         month = n;
0198         useCachedDt = false;
0199     }
0200     void setDay(int n)
0201     {
0202         day = n;
0203         useCachedDt = false;
0204     }
0205     void setHour(int n)
0206     {
0207         hour = n;
0208         useCachedDt = false;
0209     }
0210     void setMinute(int n)
0211     {
0212         minute = n;
0213         useCachedDt = false;
0214     }
0215     void setSecond(int n)
0216     {
0217         second = n;
0218         useCachedDt = false;
0219     }
0220     void setWeekday(int n)
0221     {
0222         weekday = n;
0223         useCachedDt = false;
0224     }
0225     void setWeekdaynr(int n)
0226     {
0227         weekdaynr = n;
0228         useCachedDt = false;
0229     }
0230     void setWeeknumber(int n)
0231     {
0232         weeknumber = n;
0233         useCachedDt = false;
0234     }
0235     void setYearday(int n)
0236     {
0237         yearday = n;
0238         useCachedDt = false;
0239     }
0240     void setWeekstart(int n)
0241     {
0242         weekstart = n;
0243         useCachedDt = false;
0244     }
0245 
0246     int year; // 0 means unspecified
0247     int month; // 0 means unspecified
0248     int day; // 0 means unspecified
0249     int hour; // -1 means unspecified
0250     int minute; // -1 means unspecified
0251     int second; // -1 means unspecified
0252     int weekday; //  0 means unspecified
0253     int weekdaynr; // index of weekday in month/year (0=unspecified)
0254     int weeknumber; //  0 means unspecified
0255     int yearday; //  0 means unspecified
0256     int weekstart; //  first day of week (1=monday, 7=sunday, 0=unspec.)
0257     QTimeZone timeZone; // time zone etc. to use
0258 
0259     bool readDateTime(const QDateTime &dt, RecurrenceRule::PeriodType type);
0260     bool matches(const QDate &dt, RecurrenceRule::PeriodType type) const;
0261     bool matches(const QDateTime &dt, RecurrenceRule::PeriodType type) const;
0262     bool merge(const Constraint &interval);
0263     bool isConsistent(RecurrenceRule::PeriodType period) const;
0264     bool increase(RecurrenceRule::PeriodType type, int freq);
0265     QDateTime intervalDateTime(RecurrenceRule::PeriodType type) const;
0266     QList<QDateTime> dateTimes(RecurrenceRule::PeriodType type) const;
0267     void appendDateTime(const QDate &date, const QTime &time, QList<QDateTime> &list) const;
0268     void dump() const;
0269 
0270 private:
0271     mutable bool useCachedDt;
0272     mutable QDateTime cachedDt;
0273 };
0274 
0275 Constraint::Constraint(const QTimeZone &timeZone, int wkst)
0276     : weekstart(wkst)
0277     , timeZone(timeZone)
0278 {
0279     clear();
0280 }
0281 
0282 Constraint::Constraint(const QDateTime &dt, RecurrenceRule::PeriodType type, int wkst)
0283     : weekstart(wkst)
0284     , timeZone(dt.timeZone())
0285 {
0286     clear();
0287     readDateTime(dt, type);
0288 }
0289 
0290 void Constraint::clear()
0291 {
0292     year = 0;
0293     month = 0;
0294     day = 0;
0295     hour = -1;
0296     minute = -1;
0297     second = -1;
0298     weekday = 0;
0299     weekdaynr = 0;
0300     weeknumber = 0;
0301     yearday = 0;
0302     useCachedDt = false;
0303 }
0304 
0305 bool Constraint::matches(const QDate &dt, RecurrenceRule::PeriodType type) const
0306 {
0307     // If the event recurs in week 53 or 1, the day might not belong to the same
0308     // year as the week it is in. E.g. Jan 1, 2005 is in week 53 of year 2004.
0309     // So we can't simply check the year in that case!
0310     if (weeknumber == 0) {
0311         if (year > 0 && year != dt.year()) {
0312             return false;
0313         }
0314     } else {
0315         int y = 0;
0316         if (weeknumber > 0 && weeknumber != DateHelper::getWeekNumber(dt, weekstart, &y)) {
0317             return false;
0318         }
0319         if (weeknumber < 0 && weeknumber != DateHelper::getWeekNumberNeg(dt, weekstart, &y)) {
0320             return false;
0321         }
0322         if (year > 0 && year != y) {
0323             return false;
0324         }
0325     }
0326 
0327     if (month > 0 && month != dt.month()) {
0328         return false;
0329     }
0330     if (day > 0 && day != dt.day()) {
0331         return false;
0332     }
0333     if (day < 0 && dt.day() != (dt.daysInMonth() + day + 1)) {
0334         return false;
0335     }
0336     if (weekday > 0) {
0337         if (weekday != dt.dayOfWeek()) {
0338             return false;
0339         }
0340         if (weekdaynr != 0) {
0341             // If it's a yearly recurrence and a month is given, the position is
0342             // still in the month, not in the year.
0343             if ((type == RecurrenceRule::rMonthly) || (type == RecurrenceRule::rYearly && month > 0)) {
0344                 // Monthly
0345                 if (weekdaynr > 0 && weekdaynr != (dt.day() - 1) / 7 + 1) {
0346                     return false;
0347                 }
0348                 if (weekdaynr < 0 && weekdaynr != -((dt.daysInMonth() - dt.day()) / 7 + 1)) {
0349                     return false;
0350                 }
0351             } else {
0352                 // Yearly
0353                 if (weekdaynr > 0 && weekdaynr != (dt.dayOfYear() - 1) / 7 + 1) {
0354                     return false;
0355                 }
0356                 if (weekdaynr < 0 && weekdaynr != -((dt.daysInYear() - dt.dayOfYear()) / 7 + 1)) {
0357                     return false;
0358                 }
0359             }
0360         }
0361     }
0362     if (yearday > 0 && yearday != dt.dayOfYear()) {
0363         return false;
0364     }
0365     if (yearday < 0 && yearday != dt.daysInYear() - dt.dayOfYear() + 1) {
0366         return false;
0367     }
0368     return true;
0369 }
0370 
0371 /* Check for a match with the specified date/time.
0372  * The date/time's time specification must correspond with that of the start date/time.
0373  */
0374 bool Constraint::matches(const QDateTime &dt, RecurrenceRule::PeriodType type) const
0375 {
0376     if ((hour >= 0 && hour != dt.time().hour()) || (minute >= 0 && minute != dt.time().minute()) || (second >= 0 && second != dt.time().second())
0377         || !matches(dt.date(), type)) {
0378         return false;
0379     }
0380     return true;
0381 }
0382 
0383 bool Constraint::isConsistent(RecurrenceRule::PeriodType /*period*/) const
0384 {
0385     // TODO: Check for consistency, e.g. byyearday=3 and bymonth=10
0386     return true;
0387 }
0388 
0389 // Return a date/time set to the constraint values, but with those parts less
0390 // significant than the given period type set to 1 (for dates) or 0 (for times).
0391 QDateTime Constraint::intervalDateTime(RecurrenceRule::PeriodType type) const
0392 {
0393     if (useCachedDt) {
0394         return cachedDt;
0395     }
0396     QDate d;
0397     QTime t(0, 0, 0);
0398     bool subdaily = true;
0399     switch (type) {
0400     case RecurrenceRule::rSecondly:
0401         t.setHMS(hour, minute, second);
0402         break;
0403     case RecurrenceRule::rMinutely:
0404         t.setHMS(hour, minute, 0);
0405         break;
0406     case RecurrenceRule::rHourly:
0407         t.setHMS(hour, 0, 0);
0408         break;
0409     case RecurrenceRule::rDaily:
0410         break;
0411     case RecurrenceRule::rWeekly:
0412         d = DateHelper::getNthWeek(year, weeknumber, weekstart);
0413         subdaily = false;
0414         break;
0415     case RecurrenceRule::rMonthly:
0416         d.setDate(year, month, 1);
0417         subdaily = false;
0418         break;
0419     case RecurrenceRule::rYearly:
0420         d.setDate(year, 1, 1);
0421         subdaily = false;
0422         break;
0423     default:
0424         break;
0425     }
0426     if (subdaily) {
0427         d = DateHelper::getDate(year, (month > 0) ? month : 1, day ? day : 1);
0428     }
0429     cachedDt = QDateTime(d, t, timeZone);
0430     useCachedDt = true;
0431     return cachedDt;
0432 }
0433 
0434 bool Constraint::merge(const Constraint &interval)
0435 {
0436 // clang-format off
0437 #define mergeConstraint( name, cmparison ) \
0438     if ( interval.name cmparison ) { \
0439         if ( !( name cmparison ) ) { \
0440             name = interval.name; \
0441         } else if ( name != interval.name ) { \
0442             return false;\
0443         } \
0444     }
0445     // clang-format on
0446 
0447     useCachedDt = false;
0448 
0449     mergeConstraint(year, > 0);
0450     mergeConstraint(month, > 0);
0451     mergeConstraint(day, != 0);
0452     mergeConstraint(hour, >= 0);
0453     mergeConstraint(minute, >= 0);
0454     mergeConstraint(second, >= 0);
0455 
0456     mergeConstraint(weekday, != 0);
0457     mergeConstraint(weekdaynr, != 0);
0458     mergeConstraint(weeknumber, != 0);
0459     mergeConstraint(yearday, != 0);
0460 
0461 #undef mergeConstraint
0462     return true;
0463 }
0464 
0465 //           Y  M  D | H  Mn S | WD #WD | WN | YD
0466 // required:
0467 //           x       | x  x  x |        |    |
0468 // 0) Trivial: Exact date given, maybe other restrictions
0469 //           x  x  x | x  x  x |        |    |
0470 // 1) Easy case: no weekly restrictions -> at most a loop through possible dates
0471 //           x  +  + | x  x  x |  -  -  |  - |  -
0472 // 2) Year day is given -> date known
0473 //           x       | x  x  x |        |    |  +
0474 // 3) week number is given -> loop through all days of that week. Further
0475 //    restrictions will be applied in the end, when we check all dates for
0476 //    consistency with the constraints
0477 //           x       | x  x  x |        |  + | (-)
0478 // 4) week day is specified ->
0479 //           x       | x  x  x |  x  ?  | (-)| (-)
0480 // 5) All possiblecases have already been treated, so this must be an error!
0481 
0482 QList<QDateTime> Constraint::dateTimes(RecurrenceRule::PeriodType type) const
0483 {
0484     QList<QDateTime> result;
0485     if (!isConsistent(type)) {
0486         return result;
0487     }
0488 
0489     // TODO_Recurrence: Handle all-day
0490     QTime tm(hour, minute, second);
0491 
0492     bool done = false;
0493     if (day && month > 0) {
0494         appendDateTime(DateHelper::getDate(year, month, day), tm, result);
0495         done = true;
0496     }
0497 
0498     if (!done && weekday == 0 && weeknumber == 0 && yearday == 0) {
0499         // Easy case: date is given, not restrictions by week or yearday
0500         uint mstart = (month > 0) ? month : 1;
0501         uint mend = (month <= 0) ? 12 : month;
0502         for (uint m = mstart; m <= mend; ++m) {
0503             uint dstart;
0504             uint dend;
0505             if (day > 0) {
0506                 dstart = dend = day;
0507             } else if (day < 0) {
0508                 QDate date(year, month, 1);
0509                 dstart = dend = date.daysInMonth() + day + 1;
0510             } else {
0511                 QDate date(year, month, 1);
0512                 dstart = 1;
0513                 dend = date.daysInMonth();
0514             }
0515             uint d = dstart;
0516             for (QDate dt(year, m, dstart);; dt = dt.addDays(1)) {
0517                 appendDateTime(dt, tm, result);
0518                 if (++d > dend) {
0519                     break;
0520                 }
0521             }
0522         }
0523         done = true;
0524     }
0525 
0526     // Else: At least one of the week / yearday restrictions was given...
0527     // If we have a yearday (and of course a year), we know the exact date
0528     if (!done && yearday != 0) {
0529         // yearday < 0 means from end of year, so we'll need Jan 1 of the next year
0530         QDate d(year + ((yearday > 0) ? 0 : 1), 1, 1);
0531         d = d.addDays(yearday - ((yearday > 0) ? 1 : 0));
0532         appendDateTime(d, tm, result);
0533         done = true;
0534     }
0535 
0536     // Else: If we have a weeknumber, we have at most 7 possible dates, loop through them
0537     if (!done && weeknumber != 0) {
0538         QDate wst(DateHelper::getNthWeek(year, weeknumber, weekstart));
0539         if (weekday != 0) {
0540             wst = wst.addDays((7 + weekday - weekstart) % 7);
0541             appendDateTime(wst, tm, result);
0542         } else {
0543             for (int i = 0; i < 7; ++i) {
0544                 appendDateTime(wst, tm, result);
0545                 wst = wst.addDays(1);
0546             }
0547         }
0548         done = true;
0549     }
0550 
0551     // weekday is given
0552     if (!done && weekday != 0) {
0553         QDate dt(year, 1, 1);
0554         // If type == yearly and month is given, pos is still in month not year!
0555         // TODO_Recurrence: Correct handling of n-th  BYDAY...
0556         int maxloop = 53;
0557         bool inMonth = (type == RecurrenceRule::rMonthly) || (type == RecurrenceRule::rYearly && month > 0);
0558         if (inMonth && month > 0) {
0559             dt = QDate(year, month, 1);
0560             maxloop = 5;
0561         }
0562         if (weekdaynr < 0) {
0563             // From end of period (month, year) => relative to begin of next period
0564             if (inMonth) {
0565                 dt = dt.addMonths(1);
0566             } else {
0567                 dt = dt.addYears(1);
0568             }
0569         }
0570         int adj = (7 + weekday - dt.dayOfWeek()) % 7;
0571         dt = dt.addDays(adj); // correct first weekday of the period
0572 
0573         if (weekdaynr > 0) {
0574             dt = dt.addDays((weekdaynr - 1) * 7);
0575             appendDateTime(dt, tm, result);
0576         } else if (weekdaynr < 0) {
0577             dt = dt.addDays(weekdaynr * 7);
0578             appendDateTime(dt, tm, result);
0579         } else {
0580             // loop through all possible weeks, non-matching will be filtered later
0581             for (int i = 0; i < maxloop; ++i) {
0582                 appendDateTime(dt, tm, result);
0583                 dt = dt.addDays(7);
0584             }
0585         }
0586     } // weekday != 0
0587 
0588     // Only use those times that really match all other constraints, too
0589     QList<QDateTime> valid;
0590     for (int i = 0, iend = result.count(); i < iend; ++i) {
0591         if (matches(result[i], type)) {
0592             valid.append(result[i]);
0593         }
0594     }
0595     // Don't sort it here, would be unnecessary work. The results from all
0596     // constraints will be merged to one big list of the interval. Sort that one!
0597     return valid;
0598 }
0599 
0600 void Constraint::appendDateTime(const QDate &date, const QTime &time, QList<QDateTime> &list) const
0601 {
0602     QDateTime dt(date, time, timeZone);
0603     if (dt.isValid()) {
0604         list.append(dt);
0605     }
0606 }
0607 
0608 bool Constraint::increase(RecurrenceRule::PeriodType type, int freq)
0609 {
0610     // convert the first day of the interval to QDateTime
0611     intervalDateTime(type);
0612 
0613     // Now add the intervals
0614     switch (type) {
0615     case RecurrenceRule::rSecondly:
0616         cachedDt = cachedDt.addSecs(freq);
0617         break;
0618     case RecurrenceRule::rMinutely:
0619         cachedDt = cachedDt.addSecs(60 * freq);
0620         break;
0621     case RecurrenceRule::rHourly:
0622         cachedDt = cachedDt.addSecs(3600 * freq);
0623         break;
0624     case RecurrenceRule::rDaily:
0625         cachedDt = cachedDt.addDays(freq);
0626         break;
0627     case RecurrenceRule::rWeekly:
0628         cachedDt = cachedDt.addDays(7 * freq);
0629         break;
0630     case RecurrenceRule::rMonthly:
0631         cachedDt = cachedDt.addMonths(freq);
0632         break;
0633     case RecurrenceRule::rYearly:
0634         cachedDt = cachedDt.addYears(freq);
0635         break;
0636     default:
0637         break;
0638     }
0639     // Convert back from QDateTime to the Constraint class
0640     readDateTime(cachedDt, type);
0641     useCachedDt = true; // readDateTime() resets this
0642 
0643     return true;
0644 }
0645 
0646 // Set the constraint's value appropriate to 'type', to the value contained in a date/time.
0647 bool Constraint::readDateTime(const QDateTime &dt, RecurrenceRule::PeriodType type)
0648 {
0649     switch (type) {
0650     // Really fall through! Only weekly needs to be treated differently!
0651     case RecurrenceRule::rSecondly:
0652         second = dt.time().second();
0653         Q_FALLTHROUGH();
0654     case RecurrenceRule::rMinutely:
0655         minute = dt.time().minute();
0656         Q_FALLTHROUGH();
0657     case RecurrenceRule::rHourly:
0658         hour = dt.time().hour();
0659         Q_FALLTHROUGH();
0660     case RecurrenceRule::rDaily:
0661         day = dt.date().day();
0662         Q_FALLTHROUGH();
0663     case RecurrenceRule::rMonthly:
0664         month = dt.date().month();
0665         Q_FALLTHROUGH();
0666     case RecurrenceRule::rYearly:
0667         year = dt.date().year();
0668         break;
0669     case RecurrenceRule::rWeekly:
0670         // Determine start day of the current week, calculate the week number from that
0671         weeknumber = DateHelper::getWeekNumber(dt.date(), weekstart, &year);
0672         break;
0673     default:
0674         break;
0675     }
0676     useCachedDt = false;
0677     return true;
0678 }
0679 //@endcond
0680 
0681 /**************************************************************************
0682  *                        RecurrenceRule::Private                         *
0683  **************************************************************************/
0684 
0685 //@cond PRIVATE
0686 class Q_DECL_HIDDEN KCalendarCore::RecurrenceRule::Private
0687 {
0688 public:
0689     Private(RecurrenceRule *parent)
0690         : mParent(parent)
0691         , mPeriod(rNone)
0692         , mFrequency(0)
0693         , mDuration(-1)
0694         , mWeekStart(1)
0695         , mIsReadOnly(false)
0696         , mAllDay(false)
0697     {
0698         setDirty();
0699     }
0700 
0701     Private(RecurrenceRule *parent, const Private &p);
0702 
0703     Private &operator=(const Private &other);
0704     bool operator==(const Private &other) const;
0705     void clear();
0706     void setDirty();
0707     void buildConstraints();
0708     bool buildCache() const;
0709     Constraint getNextValidDateInterval(const QDateTime &preDate, PeriodType type) const;
0710     Constraint getPreviousValidDateInterval(const QDateTime &afterDate, PeriodType type) const;
0711     QList<QDateTime> datesForInterval(const Constraint &interval, PeriodType type) const;
0712 
0713     RecurrenceRule *mParent;
0714     QString mRRule; // RRULE string
0715     PeriodType mPeriod;
0716     QDateTime mDateStart; // start of recurrence (but mDateStart is not an occurrence
0717     // unless it matches the rule)
0718     uint mFrequency;
0719     /** how often it recurs:
0720            < 0 means no end date,
0721            0 means an explicit end date,
0722            positive values give the number of occurrences */
0723     int mDuration;
0724     QDateTime mDateEnd;
0725 
0726     QList<int> mBySeconds; // values: second 0-59
0727     QList<int> mByMinutes; // values: minute 0-59
0728     QList<int> mByHours; // values: hour 0-23
0729 
0730     QList<WDayPos> mByDays; // n-th weekday of the month or year
0731     QList<int> mByMonthDays; // values: day -31 to -1 and 1-31
0732     QList<int> mByYearDays; // values: day -366 to -1 and 1-366
0733     QList<int> mByWeekNumbers; // values: week -53 to -1 and 1-53
0734     QList<int> mByMonths; // values: month 1-12
0735     QList<int> mBySetPos; // values: position -366 to -1 and 1-366
0736     short mWeekStart; // first day of the week (1=Monday, 7=Sunday)
0737 
0738     Constraint::List mConstraints;
0739     QList<RuleObserver *> mObservers;
0740 
0741     // Cache for duration
0742     mutable QList<QDateTime> mCachedDates;
0743     mutable QDateTime mCachedDateEnd;
0744     mutable QDateTime mCachedLastDate; // when mCachedDateEnd invalid, last date checked
0745     mutable bool mCached;
0746 
0747     bool mIsReadOnly;
0748     bool mAllDay;
0749     bool mNoByRules; // no BySeconds, ByMinutes, ... rules exist
0750     uint mTimedRepetition; // repeats at a regular number of seconds interval, or 0
0751 };
0752 
0753 RecurrenceRule::Private::Private(RecurrenceRule *parent, const Private &p)
0754     : mParent(parent)
0755     , mRRule(p.mRRule)
0756     , mPeriod(p.mPeriod)
0757     , mDateStart(p.mDateStart)
0758     , mFrequency(p.mFrequency)
0759     , mDuration(p.mDuration)
0760     , mDateEnd(p.mDateEnd)
0761     ,
0762 
0763     mBySeconds(p.mBySeconds)
0764     , mByMinutes(p.mByMinutes)
0765     , mByHours(p.mByHours)
0766     , mByDays(p.mByDays)
0767     , mByMonthDays(p.mByMonthDays)
0768     , mByYearDays(p.mByYearDays)
0769     , mByWeekNumbers(p.mByWeekNumbers)
0770     , mByMonths(p.mByMonths)
0771     , mBySetPos(p.mBySetPos)
0772     , mWeekStart(p.mWeekStart)
0773     ,
0774 
0775     mIsReadOnly(p.mIsReadOnly)
0776     , mAllDay(p.mAllDay)
0777     , mNoByRules(p.mNoByRules)
0778 {
0779     setDirty();
0780 }
0781 
0782 RecurrenceRule::Private &RecurrenceRule::Private::operator=(const Private &p)
0783 {
0784     // check for self assignment
0785     if (&p == this) {
0786         return *this;
0787     }
0788 
0789     mRRule = p.mRRule;
0790     mPeriod = p.mPeriod;
0791     mDateStart = p.mDateStart;
0792     mFrequency = p.mFrequency;
0793     mDuration = p.mDuration;
0794     mDateEnd = p.mDateEnd;
0795 
0796     mBySeconds = p.mBySeconds;
0797     mByMinutes = p.mByMinutes;
0798     mByHours = p.mByHours;
0799     mByDays = p.mByDays;
0800     mByMonthDays = p.mByMonthDays;
0801     mByYearDays = p.mByYearDays;
0802     mByWeekNumbers = p.mByWeekNumbers;
0803     mByMonths = p.mByMonths;
0804     mBySetPos = p.mBySetPos;
0805     mWeekStart = p.mWeekStart;
0806 
0807     mIsReadOnly = p.mIsReadOnly;
0808     mAllDay = p.mAllDay;
0809     mNoByRules = p.mNoByRules;
0810 
0811     setDirty();
0812 
0813     return *this;
0814 }
0815 
0816 bool RecurrenceRule::Private::operator==(const Private &r) const
0817 {
0818     return mPeriod == r.mPeriod && identical(mDateStart, r.mDateStart) && mDuration == r.mDuration
0819         && identical(mDateEnd, r.mDateEnd) && mFrequency == r.mFrequency && mIsReadOnly == r.mIsReadOnly
0820         && mAllDay == r.mAllDay && mBySeconds == r.mBySeconds && mByMinutes == r.mByMinutes && mByHours == r.mByHours && mByDays == r.mByDays
0821         && mByMonthDays == r.mByMonthDays && mByYearDays == r.mByYearDays && mByWeekNumbers == r.mByWeekNumbers && mByMonths == r.mByMonths
0822         && mBySetPos == r.mBySetPos && mWeekStart == r.mWeekStart && mNoByRules == r.mNoByRules;
0823 }
0824 
0825 void RecurrenceRule::Private::clear()
0826 {
0827     if (mIsReadOnly) {
0828         return;
0829     }
0830     mPeriod = rNone;
0831     mBySeconds.clear();
0832     mByMinutes.clear();
0833     mByHours.clear();
0834     mByDays.clear();
0835     mByMonthDays.clear();
0836     mByYearDays.clear();
0837     mByWeekNumbers.clear();
0838     mByMonths.clear();
0839     mBySetPos.clear();
0840     mWeekStart = 1;
0841     mNoByRules = false;
0842 
0843     setDirty();
0844 }
0845 
0846 void RecurrenceRule::Private::setDirty()
0847 {
0848     buildConstraints();
0849     mCached = false;
0850     mCachedDates.clear();
0851     for (int i = 0, iend = mObservers.count(); i < iend; ++i) {
0852         if (mObservers[i]) {
0853             mObservers[i]->recurrenceChanged(mParent);
0854         }
0855     }
0856 }
0857 //@endcond
0858 
0859 /**************************************************************************
0860  *                              RecurrenceRule                            *
0861  **************************************************************************/
0862 
0863 RecurrenceRule::RecurrenceRule()
0864     : d(new Private(this))
0865 {
0866 }
0867 
0868 RecurrenceRule::RecurrenceRule(const RecurrenceRule &r)
0869     : d(new Private(this, *r.d))
0870 {
0871 }
0872 
0873 RecurrenceRule::~RecurrenceRule()
0874 {
0875     delete d;
0876 }
0877 
0878 bool RecurrenceRule::operator==(const RecurrenceRule &r) const
0879 {
0880     return *d == *r.d;
0881 }
0882 
0883 RecurrenceRule &RecurrenceRule::operator=(const RecurrenceRule &r)
0884 {
0885     // check for self assignment
0886     if (&r == this) {
0887         return *this;
0888     }
0889 
0890     *d = *r.d;
0891 
0892     return *this;
0893 }
0894 
0895 void RecurrenceRule::addObserver(RuleObserver *observer)
0896 {
0897     if (!d->mObservers.contains(observer)) {
0898         d->mObservers.append(observer);
0899     }
0900 }
0901 
0902 void RecurrenceRule::removeObserver(RuleObserver *observer)
0903 {
0904     d->mObservers.removeAll(observer);
0905 }
0906 
0907 void RecurrenceRule::setRecurrenceType(PeriodType period)
0908 {
0909     if (isReadOnly()) {
0910         return;
0911     }
0912     d->mPeriod = period;
0913     d->setDirty();
0914 }
0915 
0916 QDateTime RecurrenceRule::endDt(bool *result) const
0917 {
0918     if (result) {
0919         *result = false;
0920     }
0921     if (d->mPeriod == rNone) {
0922         return QDateTime();
0923     }
0924     if (d->mDuration < 0) {
0925         return QDateTime();
0926     }
0927     if (d->mDuration == 0) {
0928         if (result) {
0929             *result = true;
0930         }
0931         return d->mDateEnd;
0932     }
0933 
0934     // N occurrences. Check if we have a full cache. If so, return the cached end date.
0935     if (!d->mCached) {
0936         // If not enough occurrences can be found (i.e. inconsistent constraints)
0937         if (!d->buildCache()) {
0938             return QDateTime();
0939         }
0940     }
0941     if (result) {
0942         *result = true;
0943     }
0944     return d->mCachedDateEnd;
0945 }
0946 
0947 void RecurrenceRule::setEndDt(const QDateTime &dateTime)
0948 {
0949     if (isReadOnly()) {
0950         return;
0951     }
0952     d->mDateEnd = dateTime;
0953     if (d->mDateEnd.isValid()) {
0954         d->mDuration = 0; // set to 0 because there is an end date/time
0955     }
0956     d->setDirty();
0957 }
0958 
0959 void RecurrenceRule::setDuration(int duration)
0960 {
0961     if (isReadOnly()) {
0962         return;
0963     }
0964     d->mDuration = duration;
0965     d->setDirty();
0966 }
0967 
0968 void RecurrenceRule::setAllDay(bool allDay)
0969 {
0970     if (isReadOnly()) {
0971         return;
0972     }
0973     d->mAllDay = allDay;
0974     d->setDirty();
0975 }
0976 
0977 void RecurrenceRule::clear()
0978 {
0979     d->clear();
0980 }
0981 
0982 void RecurrenceRule::setDirty()
0983 {
0984     d->setDirty();
0985 }
0986 
0987 void RecurrenceRule::setStartDt(const QDateTime &start)
0988 {
0989     if (isReadOnly()) {
0990         return;
0991     }
0992     d->mDateStart = start;
0993     d->setDirty();
0994 }
0995 
0996 void RecurrenceRule::setFrequency(int freq)
0997 {
0998     if (isReadOnly() || freq <= 0) {
0999         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 
1353 bool RecurrenceRule::dateMatchesRules(const QDateTime &kdt) const
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. */
1555 int RecurrenceRule::durationTo(const QDateTime &dt) const
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 
1582 QDateTime RecurrenceRule::getPreviousDate(const QDateTime &afterDate) const
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 
1648 QDateTime RecurrenceRule::getNextDate(const QDateTime &preDate) const
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 
1710 QList<QDateTime> RecurrenceRule::timesInInterval(const QDateTime &dtStart, const QDateTime &dtEnd) const
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 
2005 void RecurrenceRule::dump() const
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 
2081 QDateTime RecurrenceRule::startDt() const
2082 {
2083     return d->mDateStart;
2084 }
2085 
2086 RecurrenceRule::PeriodType RecurrenceRule::recurrenceType() const
2087 {
2088     return d->mPeriod;
2089 }
2090 
2091 uint RecurrenceRule::frequency() const
2092 {
2093     return d->mFrequency;
2094 }
2095 
2096 int RecurrenceRule::duration() const
2097 {
2098     return d->mDuration;
2099 }
2100 
2101 QString RecurrenceRule::rrule() const
2102 {
2103     return d->mRRule;
2104 }
2105 
2106 void RecurrenceRule::setRRule(const QString &rrule)
2107 {
2108     d->mRRule = rrule;
2109 }
2110 
2111 bool RecurrenceRule::isReadOnly() const
2112 {
2113     return d->mIsReadOnly;
2114 }
2115 
2116 void RecurrenceRule::setReadOnly(bool readOnly)
2117 {
2118     d->mIsReadOnly = readOnly;
2119 }
2120 
2121 bool RecurrenceRule::recurs() const
2122 {
2123     return d->mPeriod != rNone;
2124 }
2125 
2126 bool RecurrenceRule::allDay() const
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 
2229 KCALENDARCORE_EXPORT QDataStream &KCalendarCore::operator<<(QDataStream &out, const KCalendarCore::RecurrenceRule::WDayPos &w)
2230 {
2231     out << w.mDay << w.mPos;
2232     return out;
2233 }
2234 
2235 KCALENDARCORE_EXPORT QDataStream &KCalendarCore::operator>>(QDataStream &in, KCalendarCore::RecurrenceRule::WDayPos &w)
2236 {
2237     in >> w.mDay >> w.mPos;
2238     return in;
2239 }
2240 
2241 KCALENDARCORE_EXPORT QDataStream &KCalendarCore::operator<<(QDataStream &out, const KCalendarCore::RecurrenceRule *r)
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 
2258 KCALENDARCORE_EXPORT QDataStream &KCalendarCore::operator>>(QDataStream &in, const KCalendarCore::RecurrenceRule *r)
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 }