File indexing completed on 2025-02-16 04:48:43
0001 /* 0002 * karecurrence.cpp - recurrence with special yearly February 29th handling 0003 * This file is part of kalarmcalendar library, which provides access to KAlarm 0004 * calendar data. 0005 * Program: kalarm 0006 * SPDX-FileCopyrightText: 2005-2023 David Jarvie <djarvie@kde.org> 0007 * 0008 * SPDX-License-Identifier: LGPL-2.0-or-later 0009 */ 0010 0011 #include "karecurrence.h" 0012 0013 #include <KCalendarCore/Recurrence> 0014 #include <KCalendarCore/ICalFormat> 0015 0016 #include <QDate> 0017 #include <QLocale> 0018 0019 using namespace KCalendarCore; 0020 0021 namespace 0022 { 0023 QDateTime msecs0(const KAlarmCal::KADateTime&); 0024 } 0025 0026 namespace KAlarmCal 0027 { 0028 0029 class Recurrence_p : public Recurrence 0030 { 0031 public: 0032 using Recurrence::setNewRecurrenceType; 0033 Recurrence_p() : Recurrence() {} 0034 explicit Recurrence_p(const Recurrence& r) : Recurrence(r) {} 0035 Recurrence_p(const Recurrence_p& r) = default; 0036 Recurrence_p& operator=(const Recurrence_p& r) = delete; 0037 }; 0038 0039 class Q_DECL_HIDDEN KARecurrence::Private 0040 { 0041 public: 0042 Private() = default; 0043 explicit Private(const Recurrence& r) : mRecurrence(r) {} 0044 void clear() 0045 { 0046 mRecurrence.clear(); 0047 mFeb29Type = Feb29_None; 0048 mCachedType = -1; 0049 } 0050 bool set(Type, int freq, int count, int f29, const KADateTime& start, const KADateTime& end); 0051 bool init(RecurrenceRule::PeriodType, int freq, int count, int feb29Type, const KADateTime& start, const KADateTime& end); 0052 void fix(); 0053 void writeRecurrence(const KARecurrence* q, Recurrence& recur) const; 0054 KADateTime endDateTime() const; 0055 int combineDurations(const RecurrenceRule*, const RecurrenceRule*, QDate& end) const; 0056 static QTimeZone toTimeZone(const KADateTime::Spec& spec); 0057 0058 static Feb29Type mDefaultFeb29; 0059 Recurrence_p mRecurrence; 0060 Feb29Type mFeb29Type = Feb29_None; // yearly recurrence on Feb 29th (leap years) / Mar 1st (non-leap years) 0061 mutable int mCachedType = -1; 0062 }; 0063 0064 QTimeZone KARecurrence::Private::toTimeZone(const KADateTime::Spec& spec) 0065 { 0066 switch (spec.type()) 0067 { 0068 case KADateTime::LocalZone: 0069 return QTimeZone::systemTimeZone(); 0070 case KADateTime::UTC: 0071 return QTimeZone::utc(); 0072 case KADateTime::TimeZone: 0073 return spec.timeZone(); 0074 case KADateTime::OffsetFromUTC: 0075 return QTimeZone(spec.utcOffset()); 0076 case KADateTime::Invalid: 0077 default: 0078 return {}; 0079 } 0080 } 0081 0082 /*============================================================================= 0083 = Class KARecurrence 0084 = The purpose of this class is to represent the restricted range of recurrence 0085 = types which are handled by KAlarm, and to translate between these and the 0086 = libkcal Recurrence class. In particular, it handles yearly recurrences on 0087 = 29th February specially: 0088 = 0089 = KARecurrence allows annual 29th February recurrences to fall on 28th 0090 = February or 1st March, or not at all, in non-leap years. It allows such 0091 = 29th February recurrences to be combined with the 29th of other months in 0092 = a simple way, represented simply as the 29th of multiple months including 0093 = February. For storage in the libkcal calendar, the 29th day of the month 0094 = recurrence for other months is combined with a last-day-of-February or a 0095 = 60th-day-of-the-year recurrence rule, thereby conforming to RFC2445. 0096 =============================================================================*/ 0097 0098 KARecurrence::Feb29Type KARecurrence::Private::mDefaultFeb29 = KARecurrence::Feb29_None; 0099 0100 KARecurrence::KARecurrence() 0101 : d(new Private) 0102 { } 0103 0104 KARecurrence::KARecurrence(const KCalendarCore::Recurrence& r) 0105 : d(new Private(r)) 0106 { 0107 fix(); 0108 } 0109 0110 KARecurrence::KARecurrence(const KARecurrence& r) 0111 : d(new Private(*r.d)) 0112 { } 0113 0114 KARecurrence::~KARecurrence() 0115 { 0116 delete d; 0117 } 0118 0119 bool KARecurrence::operator==(const KARecurrence& r) const 0120 { 0121 return d->mRecurrence == r.d->mRecurrence 0122 && d->mFeb29Type == r.d->mFeb29Type; 0123 } 0124 0125 KARecurrence::Feb29Type KARecurrence::feb29Type() const 0126 { 0127 return d->mFeb29Type; 0128 } 0129 0130 KARecurrence::Feb29Type KARecurrence::defaultFeb29Type() 0131 { 0132 return Private::mDefaultFeb29; 0133 } 0134 0135 void KARecurrence::setDefaultFeb29Type(Feb29Type t) 0136 { 0137 Private::mDefaultFeb29 = t; 0138 } 0139 0140 /****************************************************************************** 0141 * Set up a KARecurrence from recurrence parameters, using the start date to 0142 * determine the recurrence day/month as appropriate. 0143 * Only a restricted subset of recurrence types is allowed. 0144 * Reply = true if successful. 0145 */ 0146 bool KARecurrence::set(Type t, int freq, int count, const KADateTime& start, const KADateTime& end) 0147 { 0148 return d->set(t, freq, count, -1, start, end); 0149 } 0150 0151 bool KARecurrence::set(Type t, int freq, int count, const KADateTime& start, const KADateTime& end, Feb29Type f29) 0152 { 0153 return d->set(t, freq, count, f29, start, end); 0154 } 0155 0156 bool KARecurrence::Private::set(Type recurType, int freq, int count, int f29, const KADateTime& start, const KADateTime& end) 0157 { 0158 mCachedType = -1; 0159 RecurrenceRule::PeriodType rrtype; 0160 switch (recurType) 0161 { 0162 case MINUTELY: rrtype = RecurrenceRule::rMinutely; break; 0163 case DAILY: rrtype = RecurrenceRule::rDaily; break; 0164 case WEEKLY: rrtype = RecurrenceRule::rWeekly; break; 0165 case MONTHLY_DAY: rrtype = RecurrenceRule::rMonthly; break; 0166 case ANNUAL_DATE: rrtype = RecurrenceRule::rYearly; break; 0167 case NO_RECUR: rrtype = RecurrenceRule::rNone; break; 0168 default: 0169 return false; 0170 } 0171 if (!init(rrtype, freq, count, f29, start, end)) 0172 return false; 0173 switch (recurType) 0174 { 0175 case WEEKLY: 0176 { 0177 QBitArray days(7); 0178 days.setBit(start.date().dayOfWeek() - 1); 0179 mRecurrence.addWeeklyDays(days); 0180 break; 0181 } 0182 case MONTHLY_DAY: 0183 mRecurrence.addMonthlyDate(start.date().day()); 0184 break; 0185 case ANNUAL_DATE: 0186 mRecurrence.addYearlyDate(start.date().day()); 0187 mRecurrence.addYearlyMonth(start.date().month()); 0188 break; 0189 default: 0190 break; 0191 } 0192 return true; 0193 } 0194 0195 /****************************************************************************** 0196 * Initialise a KARecurrence from recurrence parameters. 0197 * Reply = true if successful. 0198 */ 0199 bool KARecurrence::init(KCalendarCore::RecurrenceRule::PeriodType t, int freq, int count, 0200 const KADateTime& start, const KADateTime& end) 0201 { 0202 return d->init(t, freq, count, -1, start, end); 0203 } 0204 0205 bool KARecurrence::init(RecurrenceRule::PeriodType t, int freq, int count, 0206 const KADateTime& start, const KADateTime& end, Feb29Type f29) 0207 { 0208 return d->init(t, freq, count, f29, start, end); 0209 } 0210 0211 bool KARecurrence::Private::init(RecurrenceRule::PeriodType recurType, int freq, int count, 0212 int f29, const KADateTime& start, const KADateTime& end) 0213 { 0214 clear(); 0215 const Feb29Type feb29Type = (f29 == -1) ? mDefaultFeb29 : static_cast<Feb29Type>(f29); 0216 if (count < -1) 0217 return false; 0218 const bool dateOnly = start.isDateOnly(); 0219 if (!count && ((!dateOnly && !end.isValid()) 0220 || (dateOnly && !end.date().isValid()))) 0221 return false; 0222 switch (recurType) 0223 { 0224 case RecurrenceRule::rMinutely: 0225 case RecurrenceRule::rDaily: 0226 case RecurrenceRule::rWeekly: 0227 case RecurrenceRule::rMonthly: 0228 case RecurrenceRule::rYearly: 0229 break; 0230 case RecurrenceRule::rNone: 0231 return true; 0232 default: 0233 return false; 0234 } 0235 mRecurrence.setNewRecurrenceType(recurType, freq); 0236 if (count) 0237 mRecurrence.setDuration(count); 0238 else if (dateOnly) 0239 mRecurrence.setEndDate(end.date()); 0240 else 0241 mRecurrence.setEndDateTime(msecs0(end)); 0242 KADateTime startdt = start; 0243 if (recurType == RecurrenceRule::rYearly 0244 && (feb29Type == Feb29_Feb28 || feb29Type == Feb29_Mar1)) 0245 { 0246 int year = startdt.date().year(); 0247 if (!QDate::isLeapYear(year) 0248 && startdt.date().dayOfYear() == (feb29Type == Feb29_Mar1 ? 60 : 59)) 0249 { 0250 /* The event start date is February 28th or March 1st, but it 0251 * is a recurrence on February 29th (recurring on February 28th 0252 * or March 1st in non-leap years). Adjust the start date to 0253 * be on February 29th in the last previous leap year. 0254 * This is necessary because KARecurrence represents all types 0255 * of 29th February recurrences by a simple 29th February. 0256 */ 0257 while (!QDate::isLeapYear(--year)) {} 0258 startdt.setDate(QDate(year, 2, 29)); 0259 } 0260 mFeb29Type = feb29Type; 0261 } 0262 mRecurrence.setStartDateTime(msecs0(startdt), dateOnly); // sets recurrence all-day if date-only 0263 return true; 0264 } 0265 0266 /****************************************************************************** 0267 * Initialise the recurrence from an iCalendar RRULE string. 0268 */ 0269 bool KARecurrence::set(const QString& icalRRULE) 0270 { 0271 static const QString RRULE = QStringLiteral("RRULE:"); 0272 d->clear(); 0273 if (icalRRULE.isEmpty()) 0274 return true; 0275 ICalFormat format; 0276 if (!format.fromString(d->mRecurrence.defaultRRule(true), 0277 (icalRRULE.startsWith(RRULE) ? icalRRULE.mid(RRULE.length()) : icalRRULE))) 0278 return false; 0279 fix(); 0280 return true; 0281 } 0282 0283 void KARecurrence::clear() 0284 { 0285 d->clear(); 0286 } 0287 0288 /****************************************************************************** 0289 * Must be called after presetting with a KCal::Recurrence, to convert the 0290 * recurrence to KARecurrence types: 0291 * - Convert hourly recurrences to minutely. 0292 * - Remove all but the first day in yearly date recurrences. 0293 * - Check for yearly recurrences falling on February 29th and adjust them as 0294 * necessary. A 29th of the month rule can be combined with either a 60th day 0295 * of the year rule or a last day of February rule. 0296 */ 0297 void KARecurrence::fix() 0298 { 0299 d->fix(); 0300 } 0301 0302 void KARecurrence::Private::fix() 0303 { 0304 mCachedType = -1; 0305 mFeb29Type = Feb29_None; 0306 int convert = 0; 0307 int days[2] = { 0, 0 }; 0308 RecurrenceRule* rrules[2]; 0309 const RecurrenceRule::List rrulelist = mRecurrence.rRules(); 0310 int rri = 0; 0311 const int rrend = rrulelist.count(); 0312 for (int i = 0; i < 2 && rri < rrend; ++i, ++rri) 0313 { 0314 RecurrenceRule* rrule = rrulelist[rri]; 0315 rrules[i] = rrule; 0316 bool stop = true; 0317 switch (mRecurrence.recurrenceType(rrule)) 0318 { 0319 case Recurrence::rHourly: 0320 // Convert an hourly recurrence to a minutely one 0321 rrule->setRecurrenceType(RecurrenceRule::rMinutely); 0322 rrule->setFrequency(rrule->frequency() * 60); 0323 [[fallthrough]]; // fall through to rMinutely 0324 case Recurrence::rMinutely: 0325 case Recurrence::rDaily: 0326 case Recurrence::rWeekly: 0327 case Recurrence::rMonthlyDay: 0328 case Recurrence::rMonthlyPos: 0329 case Recurrence::rYearlyPos: 0330 if (!convert) 0331 ++rri; // remove all rules except the first 0332 break; 0333 case Recurrence::rOther: 0334 if (dailyType(rrule)) 0335 { 0336 // it's a daily rule with BYDAYS 0337 if (!convert) 0338 ++rri; // remove all rules except the first 0339 } 0340 break; 0341 case Recurrence::rYearlyDay: 0342 { 0343 // Ensure that the yearly day number is 60 (i.e. Feb 29th/Mar 1st) 0344 if (convert) 0345 { 0346 // This is the second rule. 0347 // Ensure that it can be combined with the first one. 0348 if (days[0] != 29 0349 || rrule->frequency() != rrules[0]->frequency() 0350 || rrule->startDt() != rrules[0]->startDt()) 0351 break; 0352 } 0353 const QList<int> ds = rrule->byYearDays(); 0354 if (!ds.isEmpty() && ds.first() == 60) 0355 { 0356 ++convert; // this rule needs to be converted 0357 days[i] = 60; 0358 stop = false; 0359 break; 0360 } 0361 break; // not day 60, so remove this rule 0362 } 0363 case Recurrence::rYearlyMonth: 0364 { 0365 QList<int> ds = rrule->byMonthDays(); 0366 if (!ds.isEmpty()) 0367 { 0368 int day = ds.first(); 0369 if (convert) 0370 { 0371 // This is the second rule. 0372 // Ensure that it can be combined with the first one. 0373 if (day == days[0] || (day == -1 && days[0] == 60) 0374 || rrule->frequency() != rrules[0]->frequency() 0375 || rrule->startDt() != rrules[0]->startDt()) 0376 break; 0377 } 0378 if (ds.count() > 1) 0379 { 0380 ds.clear(); // remove all but the first day 0381 ds.append(day); 0382 rrule->setByMonthDays(ds); 0383 } 0384 if (day == -1) 0385 { 0386 // Last day of the month - only combine if it's February 0387 const QList<int> months = rrule->byMonths(); 0388 if (months.count() != 1 || months.first() != 2) 0389 day = 0; 0390 } 0391 if (day == 29 || day == -1) 0392 { 0393 ++convert; // this rule may need to be converted 0394 days[i] = day; 0395 stop = false; 0396 break; 0397 } 0398 } 0399 if (!convert) 0400 ++rri; 0401 break; 0402 } 0403 default: 0404 break; 0405 } 0406 if (stop) 0407 break; 0408 } 0409 0410 // Remove surplus rules 0411 for (; rri < rrend; ++rri) 0412 mRecurrence.deleteRRule(rrulelist[rri]); 0413 0414 QDate end; 0415 int count; 0416 QList<int> months; 0417 if (convert == 2) 0418 { 0419 // There are two yearly recurrence rules to combine into a February 29th recurrence. 0420 // Combine the two recurrence rules into a single rYearlyMonth rule falling on Feb 29th. 0421 // Find the duration of the two RRULEs combined, using the shorter of the two if they differ. 0422 if (days[0] != 29) 0423 { 0424 // Swap the two rules so that the 29th rule is the first 0425 RecurrenceRule* rr = rrules[0]; 0426 rrules[0] = rrules[1]; // the 29th rule 0427 rrules[1] = rr; 0428 const int d = days[0]; 0429 days[0] = days[1]; 0430 days[1] = d; // the non-29th day 0431 } 0432 // If February is included in the 29th rule, remove it to avoid duplication 0433 months = rrules[0]->byMonths(); 0434 if (months.removeAll(2)) 0435 rrules[0]->setByMonths(months); 0436 0437 count = combineDurations(rrules[0], rrules[1], end); 0438 mFeb29Type = (days[1] == 60) ? Feb29_Mar1 : Feb29_Feb28; 0439 } 0440 else if (convert == 1 && days[0] == 60) 0441 { 0442 // There is a single 60th day of the year rule. 0443 // Convert it to a February 29th recurrence. 0444 count = mRecurrence.duration(); 0445 if (!count) 0446 end = mRecurrence.endDate(); 0447 mFeb29Type = Feb29_Mar1; 0448 } 0449 else 0450 return; 0451 0452 // Create the new February 29th recurrence 0453 mRecurrence.setNewRecurrenceType(RecurrenceRule::rYearly, mRecurrence.frequency()); 0454 RecurrenceRule* rrule = mRecurrence.defaultRRule(); 0455 months.append(2); 0456 rrule->setByMonths(months); 0457 QList<int> ds; 0458 ds.append(29); 0459 rrule->setByMonthDays(ds); 0460 if (count) 0461 mRecurrence.setDuration(count); 0462 else 0463 mRecurrence.setEndDate(end); 0464 } 0465 0466 /****************************************************************************** 0467 * Initialise a KCal::Recurrence to be the same as this instance. 0468 * Additional recurrence rules are created as necessary if it recurs on Feb 29th. 0469 */ 0470 void KARecurrence::writeRecurrence(KCalendarCore::Recurrence& recur) const 0471 { 0472 d->writeRecurrence(this, recur); 0473 } 0474 0475 void KARecurrence::Private::writeRecurrence(const KARecurrence* q, Recurrence& recur) const 0476 { 0477 recur.clear(); 0478 recur.setStartDateTime(mRecurrence.startDateTime(), q->allDay()); 0479 recur.setExDates(mRecurrence.exDates()); 0480 recur.setExDateTimes(mRecurrence.exDateTimes()); 0481 const RecurrenceRule* rrule = mRecurrence.defaultRRuleConst(); 0482 if (!rrule) 0483 return; 0484 int freq = mRecurrence.frequency(); 0485 int count = mRecurrence.duration(); 0486 static_cast<Recurrence_p*>(&recur)->setNewRecurrenceType(rrule->recurrenceType(), freq); 0487 if (count) 0488 recur.setDuration(count); 0489 else 0490 recur.setEndDateTime(endDateTime().qDateTime()); 0491 switch (q->type()) 0492 { 0493 case DAILY: 0494 if (rrule->byDays().isEmpty()) 0495 break; 0496 [[fallthrough]]; // fall through to rWeekly 0497 case WEEKLY: 0498 case MONTHLY_POS: 0499 recur.defaultRRule(true)->setByDays(rrule->byDays()); 0500 break; 0501 case MONTHLY_DAY: 0502 recur.defaultRRule(true)->setByMonthDays(rrule->byMonthDays()); 0503 break; 0504 case ANNUAL_POS: 0505 recur.defaultRRule(true)->setByMonths(rrule->byMonths()); 0506 recur.defaultRRule()->setByDays(rrule->byDays()); 0507 break; 0508 case ANNUAL_DATE: 0509 { 0510 QList<int> months = rrule->byMonths(); 0511 const QList<int> days = mRecurrence.monthDays(); 0512 const bool special = (mFeb29Type != Feb29_None && !days.isEmpty() 0513 && days.first() == 29 && months.removeAll(2)); 0514 RecurrenceRule* rrule1 = recur.defaultRRule(); 0515 rrule1->setByMonths(months); 0516 rrule1->setByMonthDays(days); 0517 if (!special) 0518 break; 0519 0520 // It recurs on the 29th February. 0521 // Create an additional 60th day of the year, or last day of February, rule. 0522 auto rrule2 = new RecurrenceRule(); 0523 rrule2->setRecurrenceType(RecurrenceRule::rYearly); 0524 rrule2->setFrequency(freq); 0525 rrule2->setStartDt(mRecurrence.startDateTime()); 0526 rrule2->setAllDay(mRecurrence.allDay()); 0527 if (!count) 0528 rrule2->setEndDt(endDateTime().qDateTime()); 0529 if (mFeb29Type == Feb29_Mar1) 0530 { 0531 QList<int> ds; 0532 ds.append(60); 0533 rrule2->setByYearDays(ds); 0534 } 0535 else 0536 { 0537 QList<int> ds; 0538 ds.append(-1); 0539 rrule2->setByMonthDays(ds); 0540 QList<int> ms; 0541 ms.append(2); 0542 rrule2->setByMonths(ms); 0543 } 0544 0545 if (months.isEmpty()) 0546 { 0547 // Only February recurs. 0548 // Replace the RRULE and keep the recurrence count the same. 0549 if (count) 0550 rrule2->setDuration(count); 0551 recur.unsetRecurs(); 0552 } 0553 else 0554 { 0555 // Months other than February also recur on the 29th. 0556 // Remove February from the list and add a separate RRULE for February. 0557 if (count) 0558 { 0559 rrule1->setDuration(-1); 0560 rrule2->setDuration(-1); 0561 if (count > 0) 0562 { 0563 /* Adjust counts in the two rules to keep the correct occurrence total. 0564 * Note that durationTo() always includes the start date. Since for an 0565 * individual RRULE the start date may not actually be included, we need 0566 * to decrement the count if the start date doesn't actually recur in 0567 * this RRULE. 0568 * Note that if the count is small, one of the rules may not recur at 0569 * all. In that case, retain it so that the February 29th characteristic 0570 * is not lost should the user later change the recurrence count. 0571 */ 0572 const KADateTime end = endDateTime(); 0573 const int count1 = rrule1->durationTo(end.qDateTime()) 0574 - (rrule1->recursOn(mRecurrence.startDate(), mRecurrence.startDateTime().timeZone()) ? 0 : 1); 0575 if (count1 > 0) 0576 rrule1->setDuration(count1); 0577 else 0578 rrule1->setEndDt(mRecurrence.startDateTime()); 0579 const int count2 = rrule2->durationTo(end.qDateTime()) 0580 - (rrule2->recursOn(mRecurrence.startDate(), mRecurrence.startDateTime().timeZone()) ? 0 : 1); 0581 if (count2 > 0) 0582 rrule2->setDuration(count2); 0583 else 0584 rrule2->setEndDt(mRecurrence.startDateTime()); 0585 } 0586 } 0587 } 0588 recur.addRRule(rrule2); 0589 break; 0590 } 0591 default: 0592 break; 0593 } 0594 } 0595 0596 KADateTime KARecurrence::startDateTime() const 0597 { 0598 return KADateTime(d->mRecurrence.startDateTime()); 0599 } 0600 0601 QDate KARecurrence::startDate() const 0602 { 0603 return d->mRecurrence.startDate(); 0604 } 0605 0606 void KARecurrence::setStartDateTime(const KADateTime& dt, bool dateOnly) 0607 { 0608 d->mRecurrence.setStartDateTime(msecs0(dt), dateOnly); 0609 if (dateOnly) 0610 d->mRecurrence.setAllDay(true); 0611 } 0612 0613 /****************************************************************************** 0614 * Return the date/time of the last recurrence. 0615 */ 0616 KADateTime KARecurrence::endDateTime() const 0617 { 0618 return d->endDateTime(); 0619 } 0620 0621 KADateTime KARecurrence::Private::endDateTime() const 0622 { 0623 if (mFeb29Type == Feb29_None || mRecurrence.duration() <= 1) 0624 { 0625 /* Either it doesn't have any special February 29th treatment, 0626 * it's infinite (count = -1), the end date is specified 0627 * (count = 0), or it ends on the start date (count = 1). 0628 * So just use the normal KCal end date calculation. 0629 */ 0630 return KADateTime(mRecurrence.endDateTime()); 0631 } 0632 0633 /* Create a temporary recurrence rule to find the end date. 0634 * In a standard KCal recurrence, the 29th February only occurs once every 0635 * 4 years. So shift the temporary recurrence date to the 28th to ensure 0636 * that it occurs every year, thus giving the correct occurrence count. 0637 */ 0638 auto rrule = new RecurrenceRule(); 0639 rrule->setRecurrenceType(RecurrenceRule::rYearly); 0640 KADateTime dt(mRecurrence.startDateTime()); 0641 QDate da = dt.date(); 0642 switch (da.day()) 0643 { 0644 case 29: 0645 // The start date is definitely a recurrence date, so shift 0646 // start date to the temporary recurrence date of the 28th 0647 da.setDate(da.year(), da.month(), 28); 0648 break; 0649 case 28: 0650 if (da.month() != 2 || mFeb29Type != Feb29_Feb28 || QDate::isLeapYear(da.year())) 0651 { 0652 // Start date is not a recurrence date, so shift it to 27th 0653 da.setDate(da.year(), da.month(), 27); 0654 } 0655 break; 0656 case 1: 0657 if (da.month() == 3 && mFeb29Type == Feb29_Mar1 && !QDate::isLeapYear(da.year())) 0658 { 0659 // Start date is a March 1st recurrence date, so shift 0660 // start date to the temporary recurrence date of the 28th 0661 da.setDate(da.year(), 2, 28); 0662 } 0663 break; 0664 default: 0665 break; 0666 } 0667 dt.setDate(da); 0668 rrule->setStartDt(dt.qDateTime()); 0669 rrule->setAllDay(mRecurrence.allDay()); 0670 rrule->setFrequency(mRecurrence.frequency()); 0671 rrule->setDuration(mRecurrence.duration()); 0672 QList<int> ds; 0673 ds.append(28); 0674 rrule->setByMonthDays(ds); 0675 rrule->setByMonths(mRecurrence.defaultRRuleConst()->byMonths()); 0676 dt = KADateTime(rrule->endDt()); 0677 delete rrule; 0678 0679 // We've found the end date for a recurrence on the 28th. Unless that date 0680 // is a real February 28th recurrence, adjust to the actual recurrence date. 0681 if (mFeb29Type == Feb29_Feb28 && dt.date().month() == 2 && !QDate::isLeapYear(dt.date().year())) 0682 return dt; 0683 return dt.addDays(1); 0684 } 0685 0686 /****************************************************************************** 0687 * Return the date of the last recurrence. 0688 */ 0689 QDate KARecurrence::endDate() const 0690 { 0691 KADateTime end = endDateTime(); 0692 return end.isValid() ? end.date() : QDate(); 0693 } 0694 0695 void KARecurrence::setEndDate(const QDate& endDate) 0696 { 0697 d->mRecurrence.setEndDate(endDate); 0698 } 0699 0700 void KARecurrence::setEndDateTime(const KADateTime& endDateTime) 0701 { 0702 d->mRecurrence.setEndDateTime(msecs0(endDateTime)); 0703 } 0704 0705 bool KARecurrence::allDay() const 0706 { 0707 return d->mRecurrence.allDay(); 0708 } 0709 0710 void KARecurrence::setRecurReadOnly(bool readOnly) 0711 { 0712 d->mRecurrence.setRecurReadOnly(readOnly); 0713 } 0714 0715 bool KARecurrence::recurReadOnly() const 0716 { 0717 return d->mRecurrence.recurReadOnly(); 0718 } 0719 0720 bool KARecurrence::recurs() const 0721 { 0722 return d->mRecurrence.recurs(); 0723 } 0724 0725 QBitArray KARecurrence::days() const 0726 { 0727 return d->mRecurrence.days(); 0728 } 0729 0730 QList<RecurrenceRule::WDayPos> KARecurrence::monthPositions() const 0731 { 0732 return d->mRecurrence.monthPositions(); 0733 } 0734 0735 QList<int> KARecurrence::monthDays() const 0736 { 0737 return d->mRecurrence.monthDays(); 0738 } 0739 0740 QList<int> KARecurrence::yearDays() const 0741 { 0742 return d->mRecurrence.yearDays(); 0743 } 0744 0745 QList<int> KARecurrence::yearDates() const 0746 { 0747 return d->mRecurrence.yearDates(); 0748 } 0749 0750 QList<int> KARecurrence::yearMonths() const 0751 { 0752 return d->mRecurrence.yearMonths(); 0753 } 0754 0755 QList<RecurrenceRule::WDayPos> KARecurrence::yearPositions() const 0756 { 0757 return d->mRecurrence.yearPositions(); 0758 } 0759 0760 void KARecurrence::addWeeklyDays(const QBitArray& days) 0761 { 0762 d->mRecurrence.addWeeklyDays(days); 0763 } 0764 0765 void KARecurrence::addYearlyDay(int day) 0766 { 0767 d->mRecurrence.addYearlyDay(day); 0768 } 0769 0770 void KARecurrence::addYearlyDate(int date) 0771 { 0772 d->mRecurrence.addYearlyDate(date); 0773 } 0774 0775 void KARecurrence::addYearlyMonth(short month) 0776 { 0777 d->mRecurrence.addYearlyMonth(month); 0778 } 0779 0780 void KARecurrence::addYearlyPos(short pos, const QBitArray& days) 0781 { 0782 d->mRecurrence.addYearlyPos(pos, days); 0783 } 0784 0785 void KARecurrence::addMonthlyPos(short pos, const QBitArray& days) 0786 { 0787 d->mRecurrence.addMonthlyPos(pos, days); 0788 } 0789 0790 void KARecurrence::addMonthlyPos(short pos, ushort day) 0791 { 0792 d->mRecurrence.addMonthlyPos(pos, day); 0793 } 0794 0795 void KARecurrence::addMonthlyDate(short day) 0796 { 0797 d->mRecurrence.addMonthlyDate(day); 0798 } 0799 0800 /****************************************************************************** 0801 * Get the next time the recurrence occurs, strictly after a specified time. 0802 */ 0803 KADateTime KARecurrence::getNextDateTime(const KADateTime& preDateTime) const 0804 { 0805 switch (type()) 0806 { 0807 case ANNUAL_DATE: 0808 case ANNUAL_POS: 0809 { 0810 Recurrence recur; 0811 writeRecurrence(recur); 0812 return KADateTime(recur.getNextDateTime(msecs0(preDateTime))); 0813 } 0814 default: 0815 return KADateTime(d->mRecurrence.getNextDateTime(msecs0(preDateTime))); 0816 } 0817 } 0818 0819 /****************************************************************************** 0820 * Get the previous time the recurrence occurred, strictly before a specified time. 0821 */ 0822 KADateTime KARecurrence::getPreviousDateTime(const KADateTime& afterDateTime) const 0823 { 0824 switch (type()) 0825 { 0826 case ANNUAL_DATE: 0827 case ANNUAL_POS: 0828 { 0829 Recurrence recur; 0830 writeRecurrence(recur); 0831 return KADateTime(recur.getPreviousDateTime(msecs0(afterDateTime))); 0832 } 0833 default: 0834 return KADateTime(d->mRecurrence.getPreviousDateTime(msecs0(afterDateTime))); 0835 } 0836 } 0837 0838 /****************************************************************************** 0839 * Return whether the event will recur on the specified date. 0840 * The start date only returns true if it matches the recurrence rules. 0841 */ 0842 bool KARecurrence::recursOn(const QDate& dt, const KADateTime::Spec& timeSpec) const 0843 { 0844 if (!d->mRecurrence.recursOn(dt, Private::toTimeZone(timeSpec))) 0845 return false; 0846 if (dt != d->mRecurrence.startDate()) 0847 return true; 0848 // We know now that it isn't in EXDATES or EXRULES, 0849 // so we just need to check if it's in RDATES or RRULES 0850 if (d->mRecurrence.rDates().contains(dt)) 0851 return true; 0852 const RecurrenceRule::List rulelist = d->mRecurrence.rRules(); 0853 for (const RecurrenceRule* rule : rulelist) 0854 { 0855 if (rule->recursOn(dt, Private::toTimeZone(timeSpec))) 0856 return true; 0857 } 0858 const auto dtlist = d->mRecurrence.rDateTimes(); 0859 for (const QDateTime& dtime : dtlist) 0860 { 0861 if (dtime.date() == dt) 0862 return true; 0863 } 0864 return false; 0865 } 0866 0867 bool KARecurrence::recursAt(const KADateTime& dt) const 0868 { 0869 return d->mRecurrence.recursAt(msecs0(dt)); 0870 } 0871 0872 TimeList KARecurrence::recurTimesOn(const QDate& date, const KADateTime::Spec& timeSpec) const 0873 { 0874 return d->mRecurrence.recurTimesOn(date, Private::toTimeZone(timeSpec)); 0875 } 0876 0877 DateTimeList KARecurrence::timesInInterval(const KADateTime& start, const KADateTime& end) const 0878 { 0879 const auto l = d->mRecurrence.timesInInterval(msecs0(start), msecs0(end)); 0880 DateTimeList rv; 0881 rv.reserve(l.size()); 0882 for (const auto& qdt : l) 0883 rv << qdt; 0884 return rv; 0885 } 0886 0887 int KARecurrence::frequency() const 0888 { 0889 return d->mRecurrence.frequency(); 0890 } 0891 0892 void KARecurrence::setFrequency(int freq) 0893 { 0894 d->mRecurrence.setFrequency(freq); 0895 } 0896 0897 int KARecurrence::duration() const 0898 { 0899 return d->mRecurrence.duration(); 0900 } 0901 0902 void KARecurrence::setDuration(int duration) 0903 { 0904 d->mRecurrence.setDuration(duration); 0905 } 0906 0907 int KARecurrence::durationTo(const KADateTime& dt) const 0908 { 0909 return d->mRecurrence.durationTo(msecs0(dt)); 0910 } 0911 0912 int KARecurrence::durationTo(const QDate& date) const 0913 { 0914 return d->mRecurrence.durationTo(date); 0915 } 0916 0917 /****************************************************************************** 0918 * Find the duration of two RRULEs combined. 0919 * Use the shorter of the two if they differ. 0920 */ 0921 int KARecurrence::Private::combineDurations(const RecurrenceRule* rrule1, const RecurrenceRule* rrule2, QDate& end) const 0922 { 0923 int count1 = rrule1->duration(); 0924 int count2 = rrule2->duration(); 0925 if (count1 == -1 && count2 == -1) 0926 return -1; 0927 0928 // One of the RRULEs may not recur at all if the recurrence count is small. 0929 // In this case, its end date will have been set to the start date. 0930 if (count1 && !count2 && rrule2->endDt().date() == mRecurrence.startDateTime().date()) 0931 return count1; 0932 if (count2 && !count1 && rrule1->endDt().date() == mRecurrence.startDateTime().date()) 0933 return count2; 0934 0935 /* The duration counts will be different even for RRULEs of the same length, 0936 * because the first RRULE only actually occurs every 4 years. So we need to 0937 * compare the end dates. 0938 */ 0939 if (!count1 || !count2) 0940 count1 = count2 = 0; 0941 // Get the two rules sorted by end date. 0942 KADateTime end1(rrule1->endDt()); 0943 KADateTime end2(rrule2->endDt()); 0944 if (end1.date() == end2.date()) 0945 { 0946 end = end1.date(); 0947 return count1 + count2; 0948 } 0949 const RecurrenceRule* rr1; // earlier end date 0950 const RecurrenceRule* rr2; // later end date 0951 if (end2.isValid() 0952 && (!end1.isValid() || end1.date() > end2.date())) 0953 { 0954 // Swap the two rules to make rr1 have the earlier end date 0955 rr1 = rrule2; 0956 rr2 = rrule1; 0957 const KADateTime e = end1; 0958 end1 = end2; 0959 end2 = e; 0960 } 0961 else 0962 { 0963 rr1 = rrule1; 0964 rr2 = rrule2; 0965 } 0966 0967 // Get the date of the next occurrence after the end of the earlier ending rule 0968 RecurrenceRule rr(*rr1); 0969 rr.setDuration(-1); 0970 KADateTime next1(rr.getNextDate(end1.qDateTime())); 0971 next1.setDateOnly(true); 0972 if (!next1.isValid()) 0973 end = end1.date(); 0974 else 0975 { 0976 if (end2.isValid() && next1 > end2) 0977 { 0978 // The next occurrence after the end of the earlier ending rule 0979 // is later than the end of the later ending rule. So simply use 0980 // the end date of the later rule. 0981 end = end2.date(); 0982 return count1 + count2; 0983 } 0984 const QDate prev2 = rr2->getPreviousDate(next1.qDateTime()).date(); 0985 end = (prev2 > end1.date()) ? prev2 : end1.date(); 0986 } 0987 if (count2) 0988 count2 = rr2->durationTo(end); 0989 return count1 + count2; 0990 } 0991 0992 /****************************************************************************** 0993 * Return the longest interval between recurrences. 0994 * Reply = 0 if it never recurs. 0995 */ 0996 Duration KARecurrence::longestInterval() const 0997 { 0998 const int freq = d->mRecurrence.frequency(); 0999 switch (type()) 1000 { 1001 case MINUTELY: 1002 return {freq * 60, Duration::Seconds}; 1003 1004 case DAILY: 1005 { 1006 const QList<RecurrenceRule::WDayPos> days = d->mRecurrence.defaultRRuleConst()->byDays(); 1007 if (days.isEmpty()) 1008 return {freq, Duration::Days}; 1009 1010 // After applying the frequency, the specified days of the week 1011 // further restrict when the recurrence occurs. 1012 // So the maximum interval may be greater than the frequency. 1013 bool ds[7] = { false, false, false, false, false, false, false }; 1014 for (const RecurrenceRule::WDayPos& day : days) 1015 { 1016 if (day.pos() == 0) 1017 ds[day.day() - 1] = true; 1018 } 1019 if (freq % 7) 1020 { 1021 // It will recur on every day of the week in some week or other 1022 // (except for those days which are excluded). 1023 int first = -1; 1024 int last = -1; 1025 int maxgap = 1; 1026 for (int i = 0; i < freq * 7; i += freq) 1027 { 1028 if (ds[i % 7]) 1029 { 1030 if (first < 0) 1031 first = i; 1032 else if (i - last > maxgap) 1033 maxgap = i - last; 1034 last = i; 1035 } 1036 } 1037 const int wrap = freq * 7 - last + first; 1038 if (wrap > maxgap) 1039 maxgap = wrap; 1040 return {maxgap, Duration::Days}; 1041 } 1042 else 1043 { 1044 // It will recur on the same day of the week every time. 1045 // Ensure that the day is a day which is not excluded. 1046 if (ds[d->mRecurrence.startDate().dayOfWeek() - 1]) 1047 return {freq, Duration::Days}; 1048 break; 1049 } 1050 } 1051 case WEEKLY: 1052 { 1053 // Find which days of the week it recurs on, and if on more than 1054 // one, reduce the maximum interval accordingly. 1055 const QBitArray ds = d->mRecurrence.days(); 1056 int first = -1; 1057 int last = -1; 1058 int maxgap = 1; 1059 // Use the user's definition of the week, starting at the 1060 // day of the week specified by the user's locale. 1061 const int weekStart = QLocale().firstDayOfWeek() - 1; // zero-based 1062 for (int i = 0; i < 7; ++i) 1063 { 1064 // Get the standard Qt day-of-week number (zero-based) 1065 // for the day-of-week number in the user's locale. 1066 if (ds.testBit((i + weekStart) % 7)) 1067 { 1068 if (first < 0) 1069 first = i; 1070 else if (i - last > maxgap) 1071 maxgap = i - last; 1072 last = i; 1073 } 1074 } 1075 if (first < 0) 1076 break; // no days recur 1077 const int span = last - first; 1078 if (freq > 1) 1079 return {freq * 7 - span, Duration::Days}; 1080 if (7 - span > maxgap) 1081 return {7 - span, Duration::Days}; 1082 return {maxgap, Duration::Days}; 1083 } 1084 case MONTHLY_DAY: 1085 case MONTHLY_POS: 1086 return {freq * 31, Duration::Days}; 1087 1088 case ANNUAL_DATE: 1089 case ANNUAL_POS: 1090 { 1091 // Find which months of the year it recurs on, and if on more than 1092 // one, reduce the maximum interval accordingly. 1093 const QList<int> months = d->mRecurrence.yearMonths(); // month list is sorted 1094 if (months.isEmpty()) 1095 break; // no months recur 1096 if (months.count() == 1) 1097 return {freq * 365, Duration::Days}; 1098 int first = -1; 1099 int last = -1; 1100 int maxgap = 0; 1101 for (const int month : months) 1102 { 1103 if (first < 0) 1104 first = month; 1105 else 1106 { 1107 const int span = QDate(2001, last, 1).daysTo(QDate(2001, month, 1)); 1108 if (span > maxgap) 1109 maxgap = span; 1110 } 1111 last = month; 1112 } 1113 const int span = QDate(2001, first, 1).daysTo(QDate(2001, last, 1)); 1114 if (freq > 1) 1115 return {freq * 365 - span, Duration::Days}; 1116 if (365 - span > maxgap) 1117 return {365 - span, Duration::Days}; 1118 return {maxgap, Duration::Days}; 1119 } 1120 default: 1121 break; 1122 } 1123 return 0; 1124 } 1125 1126 /****************************************************************************** 1127 * Return the interval between recurrences, if the interval between successive 1128 * occurrences does not vary. 1129 * Reply = 0 if recurrence does not occur at fixed intervals. 1130 */ 1131 Duration KARecurrence::regularInterval() const 1132 { 1133 int freq = d->mRecurrence.frequency(); 1134 switch (type()) 1135 { 1136 case MINUTELY: 1137 return {freq * 60, Duration::Seconds}; 1138 case DAILY: 1139 { 1140 const QList<RecurrenceRule::WDayPos> days = d->mRecurrence.defaultRRuleConst()->byDays(); 1141 if (days.isEmpty()) 1142 return {freq, Duration::Days}; 1143 // After applying the frequency, the specified days of the week 1144 // further restrict when the recurrence occurs. 1145 // Find which days occur, and count the number of days which occur. 1146 bool ds[7] = { false, false, false, false, false, false, false }; 1147 for (const RecurrenceRule::WDayPos& day : days) 1148 { 1149 if (day.pos() == 0) 1150 ds[day.day() - 1] = true; 1151 } 1152 if (!(freq % 7)) 1153 { 1154 // It will recur on the same day of the week every time. 1155 // Check whether that day is in the list of included days. 1156 if (ds[d->mRecurrence.startDate().dayOfWeek() - 1]) 1157 return {freq, Duration::Days}; 1158 break; 1159 } 1160 int n = 0; // number of days which occur 1161 for (int i = 0; i < 7; ++i) 1162 { 1163 if (ds[i]) 1164 ++n; 1165 } 1166 if (n == 7) 1167 return {freq, Duration::Days}; // every day is included 1168 if (n == 1) 1169 return {freq * 7, Duration::Days}; // only one day of the week is included 1170 break; 1171 } 1172 case WEEKLY: 1173 { 1174 const QList<RecurrenceRule::WDayPos> days = d->mRecurrence.defaultRRuleConst()->byDays(); 1175 if (days.isEmpty()) 1176 return {freq * 7, Duration::Days}; 1177 // The specified days of the week occur every week in which the 1178 // recurrence occurs. 1179 // Find which days occur, and count the number of days which occur. 1180 bool ds[7] = { false, false, false, false, false, false, false }; 1181 for (const RecurrenceRule::WDayPos& day : days) 1182 { 1183 if (day.pos() == 0) 1184 ds[day.day() - 1] = true; 1185 } 1186 int n = 0; // number of days which occur 1187 for (int i = 0; i < 7; ++i) 1188 { 1189 if (ds[i]) 1190 ++n; 1191 } 1192 if (n == 7) 1193 { 1194 if (freq == 1) 1195 return {freq, Duration::Days}; // every day is included 1196 break; 1197 } 1198 if (n == 1) 1199 return {freq * 7, Duration::Days}; // only one day of the week is included 1200 break; 1201 } 1202 default: 1203 break; 1204 } 1205 return 0; 1206 } 1207 1208 DateTimeList KARecurrence::exDateTimes() const 1209 { 1210 return d->mRecurrence.exDateTimes(); 1211 } 1212 1213 DateList KARecurrence::exDates() const 1214 { 1215 return d->mRecurrence.exDates(); 1216 } 1217 1218 void KARecurrence::setExDateTimes(const DateTimeList& exdates) 1219 { 1220 d->mRecurrence.setExDateTimes(exdates); 1221 } 1222 1223 void KARecurrence::setExDates(const DateList& exdates) 1224 { 1225 d->mRecurrence.setExDates(exdates); 1226 } 1227 1228 void KARecurrence::addExDateTime(const KADateTime& exdate) 1229 { 1230 d->mRecurrence.addExDateTime(msecs0(exdate)); 1231 } 1232 1233 void KARecurrence::addExDate(const QDate& exdate) 1234 { 1235 d->mRecurrence.addExDate(exdate); 1236 } 1237 1238 void KARecurrence::shiftTimes(const QTimeZone& oldSpec, const QTimeZone& newSpec) 1239 { 1240 d->mRecurrence.shiftTimes(oldSpec, newSpec); 1241 } 1242 1243 RecurrenceRule* KARecurrence::defaultRRuleConst() const 1244 { 1245 return d->mRecurrence.defaultRRuleConst(); 1246 } 1247 1248 /****************************************************************************** 1249 * Return the recurrence's period type. 1250 */ 1251 KARecurrence::Type KARecurrence::type() const 1252 { 1253 if (d->mCachedType == -1) 1254 d->mCachedType = type(d->mRecurrence.defaultRRuleConst()); 1255 return static_cast<Type>(d->mCachedType); 1256 } 1257 1258 /****************************************************************************** 1259 * Return the recurrence rule type. 1260 */ 1261 KARecurrence::Type KARecurrence::type(const KCalendarCore::RecurrenceRule* rrule) 1262 { 1263 switch (Recurrence::recurrenceType(rrule)) 1264 { 1265 case Recurrence::rMinutely: return MINUTELY; 1266 case Recurrence::rDaily: return DAILY; 1267 case Recurrence::rWeekly: return WEEKLY; 1268 case Recurrence::rMonthlyDay: return MONTHLY_DAY; 1269 case Recurrence::rMonthlyPos: return MONTHLY_POS; 1270 case Recurrence::rYearlyMonth: return ANNUAL_DATE; 1271 case Recurrence::rYearlyPos: return ANNUAL_POS; 1272 default: 1273 if (dailyType(rrule)) 1274 return DAILY; 1275 return NO_RECUR; 1276 } 1277 } 1278 1279 /****************************************************************************** 1280 * Check if the rule is a daily rule with or without BYDAYS specified. 1281 */ 1282 bool KARecurrence::dailyType(const RecurrenceRule* rrule) 1283 { 1284 if (rrule->recurrenceType() != RecurrenceRule::rDaily 1285 || !rrule->bySeconds().isEmpty() 1286 || !rrule->byMinutes().isEmpty() 1287 || !rrule->byHours().isEmpty() 1288 || !rrule->byWeekNumbers().isEmpty() 1289 || !rrule->byMonthDays().isEmpty() 1290 || !rrule->byMonths().isEmpty() 1291 || !rrule->bySetPos().isEmpty() 1292 || !rrule->byYearDays().isEmpty()) 1293 return false; 1294 const QList<RecurrenceRule::WDayPos> days = rrule->byDays(); 1295 if (days.isEmpty()) 1296 return true; 1297 // Check that all the positions are zero (i.e. every time) 1298 bool found = false; 1299 for (const RecurrenceRule::WDayPos& day : days) 1300 { 1301 if (day.pos() != 0) 1302 return false; 1303 found = true; 1304 } 1305 return found; 1306 } 1307 1308 } // namespace KAlarmCal 1309 1310 namespace 1311 { 1312 1313 /****************************************************************************** 1314 * Return QDateTime with milliseconds part of time set to 0. 1315 * This is used to ensure that times don't have random milliseconds values, and 1316 * also to get round a minor bug in KRecurrence which doesn't return correct 1317 * milliseconds values for sub-daily recurrences. 1318 */ 1319 QDateTime msecs0(const KAlarmCal::KADateTime& kdt) 1320 { 1321 QDateTime qdt = kdt.qDateTime(); 1322 const QTime t = qdt.time(); 1323 qdt.setTime(QTime(t.hour(), t.minute(), t.second())); 1324 return qdt; 1325 } 1326 1327 } 1328 1329 // vim: et sw=4: