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 }