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: