karecurrence.cpp 41.7 KB
Newer Older
1
2
/*
 *  karecurrence.cpp  -  recurrence with special yearly February 29th handling
David Jarvie's avatar
David Jarvie committed
3
4
 *  This file is part of kalarmcal library, which provides access to KAlarm
 *  calendar data.
5
 *  SPDX-FileCopyrightText: 2005-2019 David Jarvie <djarvie@kde.org>
6
 *
7
 *  SPDX-License-Identifier: LGPL-2.0-or-later
8
9
10
11
 */

#include "karecurrence.h"

Laurent Montel's avatar
Laurent Montel committed
12
13
#include <KCalendarCore/Recurrence>
#include <KCalendarCore/ICalFormat>
Laurent Montel's avatar
Laurent Montel committed
14

Laurent Montel's avatar
Laurent Montel committed
15
#include "kalarmcal_debug.h"
16

Laurent Montel's avatar
Laurent Montel committed
17
#include <QDate>
David Jarvie's avatar
David Jarvie committed
18
#include <QLocale>
19

20
using namespace KCalendarCore;
21
namespace KAlarmCal
22
23
24
25
{

class Recurrence_p : public Recurrence
{
Laurent Montel's avatar
Laurent Montel committed
26
27
28
29
30
public:
    using Recurrence::setNewRecurrenceType;
    Recurrence_p() : Recurrence() {}
    Recurrence_p(const Recurrence &r) : Recurrence(r) {}
    Recurrence_p(const Recurrence_p &r) : Recurrence(r) {}
31
    Recurrence_p& operator=(const Recurrence_p &r) = delete;
32
33
};

34
class Q_DECL_HIDDEN KARecurrence::Private
35
{
Laurent Montel's avatar
Laurent Montel committed
36
public:
Laurent Montel's avatar
Laurent Montel committed
37
    Private() {}
Laurent Montel's avatar
Laurent Montel committed
38
    explicit Private(const Recurrence &r)
Laurent Montel's avatar
Laurent Montel committed
39
        : mRecurrence(r) {}
Laurent Montel's avatar
Laurent Montel committed
40
41
42
43
44
45
    void clear()
    {
        mRecurrence.clear();
        mFeb29Type  = Feb29_None;
        mCachedType = -1;
    }
David Jarvie's avatar
David Jarvie committed
46
47
    bool set(Type, int freq, int count, int f29, const KADateTime &start, const KADateTime &end);
    bool init(RecurrenceRule::PeriodType, int freq, int count, int feb29Type, const KADateTime &start, const KADateTime &end);
Laurent Montel's avatar
Laurent Montel committed
48
49
    void fix();
    void writeRecurrence(const KARecurrence *q, Recurrence &recur) const;
David Jarvie's avatar
David Jarvie committed
50
    KADateTime endDateTime() const;
Laurent Montel's avatar
Laurent Montel committed
51
    int  combineDurations(const RecurrenceRule *, const RecurrenceRule *, QDate &end) const;
David Jarvie's avatar
David Jarvie committed
52
    static QTimeZone toTimeZone(const KADateTime::Spec &spec);
Laurent Montel's avatar
Laurent Montel committed
53
54
55

    static Feb29Type mDefaultFeb29;
    Recurrence_p     mRecurrence;
Laurent Montel's avatar
Laurent Montel committed
56
57
    Feb29Type        mFeb29Type = Feb29_None;    // yearly recurrence on Feb 29th (leap years) / Mar 1st (non-leap years)
    mutable int      mCachedType = -1;
58
59
};

David Jarvie's avatar
David Jarvie committed
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
QTimeZone KARecurrence::Private::toTimeZone(const KADateTime::Spec &spec)
{
    switch (spec.type()) {
        case KADateTime::LocalZone:
            return QTimeZone::systemTimeZone();
        case KADateTime::UTC:
            return QTimeZone::utc();
        case KADateTime::TimeZone:
	    return spec.timeZone();
        case KADateTime::OffsetFromUTC:
            return QTimeZone(spec.utcOffset());
        case KADateTime::Invalid:
        default:
            return QTimeZone();
    }
}

77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
/*=============================================================================
= Class KARecurrence
= The purpose of this class is to represent the restricted range of recurrence
= types which are handled by KAlarm, and to translate between these and the
= libkcal Recurrence class. In particular, it handles yearly recurrences on
= 29th February specially:
=
= KARecurrence allows annual 29th February recurrences to fall on 28th
= February or 1st March, or not at all, in non-leap years. It allows such
= 29th February recurrences to be combined with the 29th of other months in
= a simple way, represented simply as the 29th of multiple months including
= February. For storage in the libkcal calendar, the 29th day of the month
= recurrence for other months is combined with a last-day-of-February or a
= 60th-day-of-the-year recurrence rule, thereby conforming to RFC2445.
=============================================================================*/

KARecurrence::Feb29Type KARecurrence::Private::mDefaultFeb29 = KARecurrence::Feb29_None;

KARecurrence::KARecurrence()
    : d(new Private)
{ }

99
KARecurrence::KARecurrence(const KCalendarCore::Recurrence &r)
100
101
102
103
104
    : d(new Private(r))
{
    fix();
}

Laurent Montel's avatar
Laurent Montel committed
105
KARecurrence::KARecurrence(const KARecurrence &r)
106
107
108
109
110
111
112
113
    : d(new Private(*r.d))
{ }

KARecurrence::~KARecurrence()
{
    delete d;
}

Laurent Montel's avatar
Laurent Montel committed
114
bool KARecurrence::operator==(const KARecurrence &r) const
115
116
{
    return d->mRecurrence == r.d->mRecurrence
David Jarvie's avatar
David Jarvie committed
117
       &&  d->mFeb29Type == r.d->mFeb29Type;
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
}

KARecurrence::Feb29Type KARecurrence::feb29Type() const
{
    return d->mFeb29Type;
}

KARecurrence::Feb29Type KARecurrence::defaultFeb29Type()
{
    return Private::mDefaultFeb29;
}

void KARecurrence::setDefaultFeb29Type(Feb29Type t)
{
    Private::mDefaultFeb29 = t;
}

/******************************************************************************
* Set up a KARecurrence from recurrence parameters, using the start date to
* determine the recurrence day/month as appropriate.
* Only a restricted subset of recurrence types is allowed.
* Reply = true if successful.
*/
David Jarvie's avatar
David Jarvie committed
141
bool KARecurrence::set(Type t, int freq, int count, const KADateTime &start, const KADateTime &end)
142
143
144
145
{
    return d->set(t, freq, count, -1, start, end);
}

David Jarvie's avatar
David Jarvie committed
146
bool KARecurrence::set(Type t, int freq, int count, const KADateTime &start, const KADateTime &end, Feb29Type f29)
147
148
149
150
{
    return d->set(t, freq, count, f29, start, end);
}

David Jarvie's avatar
David Jarvie committed
151
bool KARecurrence::Private::set(Type recurType, int freq, int count, int f29, const KADateTime &start, const KADateTime &end)
152
153
154
{
    mCachedType = -1;
    RecurrenceRule::PeriodType rrtype;
Laurent Montel's avatar
Laurent Montel committed
155
    switch (recurType) {
Laurent Montel's avatar
Laurent Montel committed
156
157
158
159
160
161
162
163
    case MINUTELY:    rrtype = RecurrenceRule::rMinutely;  break;
    case DAILY:       rrtype = RecurrenceRule::rDaily;  break;
    case WEEKLY:      rrtype = RecurrenceRule::rWeekly;  break;
    case MONTHLY_DAY: rrtype = RecurrenceRule::rMonthly;  break;
    case ANNUAL_DATE: rrtype = RecurrenceRule::rYearly;  break;
    case NO_RECUR:    rrtype = RecurrenceRule::rNone;  break;
    default:
        return false;
164
    }
Laurent Montel's avatar
Laurent Montel committed
165
    if (!init(rrtype, freq, count, f29, start, end)) {
166
        return false;
Laurent Montel's avatar
Laurent Montel committed
167
168
    }
    switch (recurType) {
Laurent Montel's avatar
Laurent Montel committed
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
    case WEEKLY: {
        QBitArray days(7);
        days.setBit(start.date().dayOfWeek() - 1);
        mRecurrence.addWeeklyDays(days);
        break;
    }
    case MONTHLY_DAY:
        mRecurrence.addMonthlyDate(start.date().day());
        break;
    case ANNUAL_DATE:
        mRecurrence.addYearlyDate(start.date().day());
        mRecurrence.addYearlyMonth(start.date().month());
        break;
    default:
        break;
184
185
186
187
188
189
190
191
    }
    return true;
}

/******************************************************************************
* Initialise a KARecurrence from recurrence parameters.
* Reply = true if successful.
*/
192
bool KARecurrence::init(KCalendarCore::RecurrenceRule::PeriodType t, int freq, int count,
David Jarvie's avatar
David Jarvie committed
193
                        const KADateTime &start, const KADateTime &end)
194
195
196
197
{
    return d->init(t, freq, count, -1, start, end);
}

198
bool KARecurrence::init(RecurrenceRule::PeriodType t, int freq, int count,
David Jarvie's avatar
David Jarvie committed
199
                        const KADateTime &start, const KADateTime &end, Feb29Type f29)
200
201
202
203
{
    return d->init(t, freq, count, f29, start, end);
}

204
bool KARecurrence::Private::init(RecurrenceRule::PeriodType recurType, int freq, int count,
David Jarvie's avatar
David Jarvie committed
205
                                 int f29, const KADateTime &start, const KADateTime &end)
206
207
{
    clear();
208
    const Feb29Type feb29Type = (f29 == -1) ? mDefaultFeb29 : static_cast<Feb29Type>(f29);
Laurent Montel's avatar
Laurent Montel committed
209
    if (count < -1) {
210
        return false;
Laurent Montel's avatar
Laurent Montel committed
211
    }
212
    const bool dateOnly = start.isDateOnly();
Laurent Montel's avatar
Laurent Montel committed
213
214
215
216
217
    if (!count  && ((!dateOnly && !end.isValid())
                    || (dateOnly && !end.date().isValid()))) {
        return false;
    }
    switch (recurType) {
Laurent Montel's avatar
Laurent Montel committed
218
219
220
221
222
223
224
225
226
227
    case RecurrenceRule::rMinutely:
    case RecurrenceRule::rDaily:
    case RecurrenceRule::rWeekly:
    case RecurrenceRule::rMonthly:
    case RecurrenceRule::rYearly:
        break;
    case RecurrenceRule::rNone:
        return true;
    default:
        return false;
228
229
    }
    mRecurrence.setNewRecurrenceType(recurType, freq);
Laurent Montel's avatar
Laurent Montel committed
230
    if (count) {
231
        mRecurrence.setDuration(count);
Laurent Montel's avatar
Laurent Montel committed
232
    } else if (dateOnly) {
233
        mRecurrence.setEndDate(end.date());
Laurent Montel's avatar
Laurent Montel committed
234
    } else {
David Jarvie's avatar
David Jarvie committed
235
        mRecurrence.setEndDateTime(end.qDateTime());
Laurent Montel's avatar
Laurent Montel committed
236
    }
David Jarvie's avatar
David Jarvie committed
237
    KADateTime startdt = start;
238
    if (recurType == RecurrenceRule::rYearly
David Jarvie's avatar
David Jarvie committed
239
    && (feb29Type == Feb29_Feb28  ||  feb29Type == Feb29_Mar1)) {
240
241
        int year = startdt.date().year();
        if (!QDate::isLeapYear(year)
David Jarvie's avatar
David Jarvie committed
242
        &&  startdt.date().dayOfYear() == (feb29Type == Feb29_Mar1 ? 60 : 59)) {
243
244
245
246
247
248
249
250
251
252
253
254
            /* The event start date is February 28th or March 1st, but it
             * is a recurrence on February 29th (recurring on February 28th
             * or March 1st in non-leap years). Adjust the start date to
             * be on February 29th in the last previous leap year.
             * This is necessary because KARecurrence represents all types
             * of 29th February recurrences by a simple 29th February.
             */
            while (!QDate::isLeapYear(--year)) ;
            startdt.setDate(QDate(year, 2, 29));
        }
        mFeb29Type = feb29Type;
    }
David Jarvie's avatar
David Jarvie committed
255
    mRecurrence.setStartDateTime(startdt.qDateTime(), dateOnly);   // sets recurrence all-day if date-only
256
257
258
259
260
261
    return true;
}

/******************************************************************************
* Initialise the recurrence from an iCalendar RRULE string.
*/
Laurent Montel's avatar
Laurent Montel committed
262
bool KARecurrence::set(const QString &icalRRULE)
263
{
264
    static const QString RRULE = QStringLiteral("RRULE:");
265
    d->clear();
Laurent Montel's avatar
Laurent Montel committed
266
    if (icalRRULE.isEmpty()) {
267
        return true;
Laurent Montel's avatar
Laurent Montel committed
268
    }
269
270
    ICalFormat format;
    if (!format.fromString(d->mRecurrence.defaultRRule(true),
Laurent Montel's avatar
Laurent Montel committed
271
                           (icalRRULE.startsWith(RRULE) ? icalRRULE.mid(RRULE.length()) : icalRRULE))) {
272
        return false;
Laurent Montel's avatar
Laurent Montel committed
273
    }
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
    fix();
    return true;
}

void KARecurrence::clear()
{
    d->clear();
}

/******************************************************************************
* Must be called after presetting with a KCal::Recurrence, to convert the
* recurrence to KARecurrence types:
* - Convert hourly recurrences to minutely.
* - Remove all but the first day in yearly date recurrences.
* - Check for yearly recurrences falling on February 29th and adjust them as
*   necessary. A 29th of the month rule can be combined with either a 60th day
*   of the year rule or a last day of February rule.
*/
void KARecurrence::fix()
{
    d->fix();
}

void KARecurrence::Private::fix()
{
    mCachedType = -1;
    mFeb29Type = Feb29_None;
    int convert = 0;
    int days[2] = { 0, 0 };
Laurent Montel's avatar
Laurent Montel committed
303
    RecurrenceRule *rrules[2];
304
    const RecurrenceRule::List rrulelist = mRecurrence.rRules();
305
    int rri = 0;
306
    const int rrend = rrulelist.count();
Laurent Montel's avatar
Laurent Montel committed
307
308
    for (int i = 0;  i < 2  &&  rri < rrend;  ++i, ++rri) {
        RecurrenceRule *rrule = rrulelist[rri];
309
310
        rrules[i] = rrule;
        bool stop = true;
311
        switch (mRecurrence.recurrenceType(rrule)) {
Laurent Montel's avatar
Laurent Montel committed
312
313
314
315
        case Recurrence::rHourly:
            // Convert an hourly recurrence to a minutely one
            rrule->setRecurrenceType(RecurrenceRule::rMinutely);
            rrule->setFrequency(rrule->frequency() * 60);
David Jarvie's avatar
David Jarvie committed
316
            // fall through to rMinutely
Laurent Montel's avatar
Laurent Montel committed
317
            Q_FALLTHROUGH();
Laurent Montel's avatar
Laurent Montel committed
318
319
320
321
322
323
324
325
326
327
328
329
330
        case Recurrence::rMinutely:
        case Recurrence::rDaily:
        case Recurrence::rWeekly:
        case Recurrence::rMonthlyDay:
        case Recurrence::rMonthlyPos:
        case Recurrence::rYearlyPos:
            if (!convert) {
                ++rri;    // remove all rules except the first
            }
            break;
        case Recurrence::rOther:
            if (dailyType(rrule)) {
                // it's a daily rule with BYDAYS
Laurent Montel's avatar
Laurent Montel committed
331
                if (!convert) {
332
333
                    ++rri;    // remove all rules except the first
                }
Laurent Montel's avatar
Laurent Montel committed
334
335
336
337
338
339
340
341
            }
            break;
        case Recurrence::rYearlyDay: {
            // Ensure that the yearly day number is 60 (i.e. Feb 29th/Mar 1st)
            if (convert) {
                // This is the second rule.
                // Ensure that it can be combined with the first one.
                if (days[0] != 29
David Jarvie's avatar
David Jarvie committed
342
343
                ||  rrule->frequency() != rrules[0]->frequency()
                ||  rrule->startDt()   != rrules[0]->startDt()) {
Laurent Montel's avatar
Laurent Montel committed
344
                    break;
Laurent Montel's avatar
Laurent Montel committed
345
                }
Laurent Montel's avatar
Laurent Montel committed
346
347
348
349
350
351
            }
            const QList<int> ds = rrule->byYearDays();
            if (!ds.isEmpty()  &&  ds.first() == 60) {
                ++convert;    // this rule needs to be converted
                days[i] = 60;
                stop = false;
352
                break;
Laurent Montel's avatar
Laurent Montel committed
353
354
355
356
357
358
359
            }
            break;     // not day 60, so remove this rule
        }
        case Recurrence::rYearlyMonth: {
            QList<int> ds = rrule->byMonthDays();
            if (!ds.isEmpty()) {
                int day = ds.first();
Laurent Montel's avatar
Laurent Montel committed
360
                if (convert) {
361
362
                    // This is the second rule.
                    // Ensure that it can be combined with the first one.
Laurent Montel's avatar
Laurent Montel committed
363
                    if (day == days[0]  || (day == -1 && days[0] == 60)
David Jarvie's avatar
David Jarvie committed
364
365
                    ||  rrule->frequency() != rrules[0]->frequency()
                    ||  rrule->startDt()   != rrules[0]->startDt()) {
366
                        break;
Laurent Montel's avatar
Laurent Montel committed
367
368
                    }
                }
Laurent Montel's avatar
Laurent Montel committed
369
370
371
372
                if (ds.count() > 1) {
                    ds.clear();   // remove all but the first day
                    ds.append(day);
                    rrule->setByMonthDays(ds);
373
                }
Laurent Montel's avatar
Laurent Montel committed
374
375
376
377
378
                if (day == -1) {
                    // Last day of the month - only combine if it's February
                    const QList<int> months = rrule->byMonths();
                    if (months.count() != 1  ||  months.first() != 2) {
                        day = 0;
379
380
                    }
                }
Laurent Montel's avatar
Laurent Montel committed
381
382
383
384
385
                if (day == 29  ||  day == -1) {
                    ++convert;    // this rule may need to be converted
                    days[i] = day;
                    stop = false;
                    break;
386
                }
387
            }
Laurent Montel's avatar
Laurent Montel committed
388
389
390
391
392
393
394
            if (!convert) {
                ++rri;
            }
            break;
        }
        default:
            break;
Laurent Montel's avatar
Laurent Montel committed
395
396
397
398
        }
        if (stop) {
            break;
        }
399
400
401
    }

    // Remove surplus rules
Laurent Montel's avatar
Laurent Montel committed
402
    for (;  rri < rrend;  ++rri) {
403
        mRecurrence.deleteRRule(rrulelist[rri]);
Laurent Montel's avatar
Laurent Montel committed
404
    }
405
406
407
408

    QDate end;
    int count;
    QList<int> months;
Laurent Montel's avatar
Laurent Montel committed
409
    if (convert == 2) {
410
411
412
        // There are two yearly recurrence rules to combine into a February 29th recurrence.
        // Combine the two recurrence rules into a single rYearlyMonth rule falling on Feb 29th.
        // Find the duration of the two RRULEs combined, using the shorter of the two if they differ.
Laurent Montel's avatar
Laurent Montel committed
413
        if (days[0] != 29) {
414
            // Swap the two rules so that the 29th rule is the first
Laurent Montel's avatar
Laurent Montel committed
415
            RecurrenceRule *rr = rrules[0];
416
417
            rrules[0] = rrules[1];    // the 29th rule
            rrules[1] = rr;
418
            const int d = days[0];
419
420
421
422
423
            days[0] = days[1];
            days[1] = d;        // the non-29th day
        }
        // If February is included in the 29th rule, remove it to avoid duplication
        months = rrules[0]->byMonths();
Laurent Montel's avatar
Laurent Montel committed
424
        if (months.removeAll(2)) {
425
            rrules[0]->setByMonths(months);
Laurent Montel's avatar
Laurent Montel committed
426
        }
427
428
429

        count = combineDurations(rrules[0], rrules[1], end);
        mFeb29Type = (days[1] == 60) ? Feb29_Mar1 : Feb29_Feb28;
Laurent Montel's avatar
Laurent Montel committed
430
    } else if (convert == 1  &&  days[0] == 60) {
431
432
433
        // There is a single 60th day of the year rule.
        // Convert it to a February 29th recurrence.
        count = mRecurrence.duration();
Laurent Montel's avatar
Laurent Montel committed
434
        if (!count) {
435
            end = mRecurrence.endDate();
Laurent Montel's avatar
Laurent Montel committed
436
        }
437
        mFeb29Type = Feb29_Mar1;
Laurent Montel's avatar
Laurent Montel committed
438
    } else {
439
        return;
Laurent Montel's avatar
Laurent Montel committed
440
    }
441
442
443

    // Create the new February 29th recurrence
    mRecurrence.setNewRecurrenceType(RecurrenceRule::rYearly, mRecurrence.frequency());
Laurent Montel's avatar
Laurent Montel committed
444
    RecurrenceRule *rrule = mRecurrence.defaultRRule();
445
446
447
448
449
    months.append(2);
    rrule->setByMonths(months);
    QList<int> ds;
    ds.append(29);
    rrule->setByMonthDays(ds);
Laurent Montel's avatar
Laurent Montel committed
450
    if (count) {
451
        mRecurrence.setDuration(count);
Laurent Montel's avatar
Laurent Montel committed
452
    } else {
453
        mRecurrence.setEndDate(end);
Laurent Montel's avatar
Laurent Montel committed
454
    }
455
456
457
458
459
460
}

/******************************************************************************
* Initialise a KCal::Recurrence to be the same as this instance.
* Additional recurrence rules are created as necessary if it recurs on Feb 29th.
*/
461
void KARecurrence::writeRecurrence(KCalendarCore::Recurrence &recur) const
462
463
464
465
{
    d->writeRecurrence(this, recur);
}

Laurent Montel's avatar
Laurent Montel committed
466
void KARecurrence::Private::writeRecurrence(const KARecurrence *q, Recurrence &recur) const
467
468
{
    recur.clear();
469
    recur.setStartDateTime(mRecurrence.startDateTime(), q->allDay());
470
471
    recur.setExDates(mRecurrence.exDates());
    recur.setExDateTimes(mRecurrence.exDateTimes());
Laurent Montel's avatar
Laurent Montel committed
472
473
    const RecurrenceRule *rrule = mRecurrence.defaultRRuleConst();
    if (!rrule) {
474
        return;
Laurent Montel's avatar
Laurent Montel committed
475
    }
476
477
    int freq  = mRecurrence.frequency();
    int count = mRecurrence.duration();
Laurent Montel's avatar
Laurent Montel committed
478
479
    static_cast<Recurrence_p *>(&recur)->setNewRecurrenceType(rrule->recurrenceType(), freq);
    if (count) {
480
        recur.setDuration(count);
Laurent Montel's avatar
Laurent Montel committed
481
    } else {
David Jarvie's avatar
David Jarvie committed
482
        recur.setEndDateTime(endDateTime().qDateTime());
Laurent Montel's avatar
Laurent Montel committed
483
484
    }
    switch (q->type()) {
Laurent Montel's avatar
Laurent Montel committed
485
486
    case DAILY:
        if (rrule->byDays().isEmpty()) {
487
            break;
Laurent Montel's avatar
Laurent Montel committed
488
        }
Laurent Montel's avatar
Laurent Montel committed
489
        // fall through to rWeekly
Laurent Montel's avatar
Laurent Montel committed
490
        Q_FALLTHROUGH();
Laurent Montel's avatar
Laurent Montel committed
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
    case WEEKLY:
    case MONTHLY_POS:
        recur.defaultRRule(true)->setByDays(rrule->byDays());
        break;
    case MONTHLY_DAY:
        recur.defaultRRule(true)->setByMonthDays(rrule->byMonthDays());
        break;
    case ANNUAL_POS:
        recur.defaultRRule(true)->setByMonths(rrule->byMonths());
        recur.defaultRRule()->setByDays(rrule->byDays());
        break;
    case ANNUAL_DATE: {
        QList<int>     months = rrule->byMonths();
        const QList<int> days = mRecurrence.monthDays();
        const bool special = (mFeb29Type != Feb29_None  &&  !days.isEmpty()
                              &&  days.first() == 29  &&  months.removeAll(2));
        RecurrenceRule *rrule1 = recur.defaultRRule();
        rrule1->setByMonths(months);
        rrule1->setByMonthDays(days);
        if (!special) {
511
            break;
Laurent Montel's avatar
Laurent Montel committed
512
        }
513

Laurent Montel's avatar
Laurent Montel committed
514
515
        // It recurs on the 29th February.
        // Create an additional 60th day of the year, or last day of February, rule.
516
        auto rrule2 = new RecurrenceRule();
Laurent Montel's avatar
Laurent Montel committed
517
518
519
520
521
        rrule2->setRecurrenceType(RecurrenceRule::rYearly);
        rrule2->setFrequency(freq);
        rrule2->setStartDt(mRecurrence.startDateTime());
        rrule2->setAllDay(mRecurrence.allDay());
        if (!count) {
David Jarvie's avatar
David Jarvie committed
522
            rrule2->setEndDt(endDateTime().qDateTime());
Laurent Montel's avatar
Laurent Montel committed
523
524
525
526
527
528
529
530
531
532
533
534
535
        }
        if (mFeb29Type == Feb29_Mar1) {
            QList<int> ds;
            ds.append(60);
            rrule2->setByYearDays(ds);
        } else {
            QList<int> ds;
            ds.append(-1);
            rrule2->setByMonthDays(ds);
            QList<int> ms;
            ms.append(2);
            rrule2->setByMonths(ms);
        }
536

Laurent Montel's avatar
Laurent Montel committed
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
        if (months.isEmpty()) {
            // Only February recurs.
            // Replace the RRULE and keep the recurrence count the same.
            if (count) {
                rrule2->setDuration(count);
            }
            recur.unsetRecurs();
        } else {
            // Months other than February also recur on the 29th.
            // Remove February from the list and add a separate RRULE for February.
            if (count) {
                rrule1->setDuration(-1);
                rrule2->setDuration(-1);
                if (count > 0) {
                    /* Adjust counts in the two rules to keep the correct occurrence total.
                     * Note that durationTo() always includes the start date. Since for an
                     * individual RRULE the start date may not actually be included, we need
                     * to decrement the count if the start date doesn't actually recur in
                     * this RRULE.
                     * Note that if the count is small, one of the rules may not recur at
                     * all. In that case, retain it so that the February 29th characteristic
                     * is not lost should the user later change the recurrence count.
                     */
David Jarvie's avatar
David Jarvie committed
560
561
                    const KADateTime end = endDateTime();
                    const int count1 = rrule1->durationTo(end.qDateTime())
562
                                       - (rrule1->recursOn(mRecurrence.startDate(), mRecurrence.startDateTime().timeZone()) ? 0 : 1);
Laurent Montel's avatar
Laurent Montel committed
563
564
565
566
567
                    if (count1 > 0) {
                        rrule1->setDuration(count1);
                    } else {
                        rrule1->setEndDt(mRecurrence.startDateTime());
                    }
David Jarvie's avatar
David Jarvie committed
568
                    const int count2 = rrule2->durationTo(end.qDateTime())
569
                                       - (rrule2->recursOn(mRecurrence.startDate(), mRecurrence.startDateTime().timeZone()) ? 0 : 1);
Laurent Montel's avatar
Laurent Montel committed
570
571
572
573
                    if (count2 > 0) {
                        rrule2->setDuration(count2);
                    } else {
                        rrule2->setEndDt(mRecurrence.startDateTime());
574
575
576
577
                    }
                }
            }
        }
Laurent Montel's avatar
Laurent Montel committed
578
579
580
581
582
        recur.addRRule(rrule2);
        break;
    }
    default:
        break;
583
584
585
    }
}

David Jarvie's avatar
David Jarvie committed
586
KADateTime KARecurrence::startDateTime() const
587
{
David Jarvie's avatar
David Jarvie committed
588
    return KADateTime(d->mRecurrence.startDateTime());
589
590
591
592
593
594
595
}

QDate KARecurrence::startDate() const
{
    return d->mRecurrence.startDate();
}

David Jarvie's avatar
David Jarvie committed
596
void KARecurrence::setStartDateTime(const KADateTime &dt, bool dateOnly)
597
{
David Jarvie's avatar
David Jarvie committed
598
    d->mRecurrence.setStartDateTime(dt.qDateTime(), dateOnly);
Laurent Montel's avatar
Laurent Montel committed
599
    if (dateOnly) {
600
        d->mRecurrence.setAllDay(true);
Laurent Montel's avatar
Laurent Montel committed
601
    }
602
603
604
605
606
}

/******************************************************************************
* Return the date/time of the last recurrence.
*/
David Jarvie's avatar
David Jarvie committed
607
KADateTime KARecurrence::endDateTime() const
608
609
610
611
{
    return d->endDateTime();
}

David Jarvie's avatar
David Jarvie committed
612
KADateTime KARecurrence::Private::endDateTime() const
613
{
Laurent Montel's avatar
Laurent Montel committed
614
    if (mFeb29Type == Feb29_None  ||  mRecurrence.duration() <= 1) {
615
616
617
618
619
        /* Either it doesn't have any special February 29th treatment,
         * it's infinite (count = -1), the end date is specified
         * (count = 0), or it ends on the start date (count = 1).
         * So just use the normal KCal end date calculation.
         */
David Jarvie's avatar
David Jarvie committed
620
        return KADateTime(mRecurrence.endDateTime());
621
622
623
624
625
626
627
    }

    /* Create a temporary recurrence rule to find the end date.
     * In a standard KCal recurrence, the 29th February only occurs once every
     * 4 years. So shift the temporary recurrence date to the 28th to ensure
     * that it occurs every year, thus giving the correct occurrence count.
     */
628
    auto rrule = new RecurrenceRule();
629
    rrule->setRecurrenceType(RecurrenceRule::rYearly);
David Jarvie's avatar
David Jarvie committed
630
    KADateTime dt = KADateTime(mRecurrence.startDateTime());
631
    QDate da = dt.date();
Laurent Montel's avatar
Laurent Montel committed
632
    switch (da.day()) {
Laurent Montel's avatar
Laurent Montel committed
633
634
635
    case 29:
        // The start date is definitely a recurrence date, so shift
        // start date to the temporary recurrence date of the 28th
David Jarvie's avatar
David Jarvie committed
636
        da.setDate(da.year(), da.month(), 28);
Laurent Montel's avatar
Laurent Montel committed
637
638
639
640
        break;
    case 28:
        if (da.month() != 2  ||  mFeb29Type != Feb29_Feb28  ||  QDate::isLeapYear(da.year())) {
            // Start date is not a recurrence date, so shift it to 27th
David Jarvie's avatar
David Jarvie committed
641
            da.setDate(da.year(), da.month(), 27);
Laurent Montel's avatar
Laurent Montel committed
642
643
644
645
646
        }
        break;
    case 1:
        if (da.month() == 3  &&  mFeb29Type == Feb29_Mar1  &&  !QDate::isLeapYear(da.year())) {
            // Start date is a March 1st recurrence date, so shift
647
            // start date to the temporary recurrence date of the 28th
David Jarvie's avatar
David Jarvie committed
648
            da.setDate(da.year(), 2, 28);
Laurent Montel's avatar
Laurent Montel committed
649
650
651
652
        }
        break;
    default:
        break;
653
654
    }
    dt.setDate(da);
David Jarvie's avatar
David Jarvie committed
655
    rrule->setStartDt(dt.qDateTime());
656
657
658
659
660
661
662
    rrule->setAllDay(mRecurrence.allDay());
    rrule->setFrequency(mRecurrence.frequency());
    rrule->setDuration(mRecurrence.duration());
    QList<int> ds;
    ds.append(28);
    rrule->setByMonthDays(ds);
    rrule->setByMonths(mRecurrence.defaultRRuleConst()->byMonths());
David Jarvie's avatar
David Jarvie committed
663
    dt = KADateTime(rrule->endDt());
664
665
666
667
    delete rrule;

    // We've found the end date for a recurrence on the 28th. Unless that date
    // is a real February 28th recurrence, adjust to the actual recurrence date.
Laurent Montel's avatar
Laurent Montel committed
668
    if (mFeb29Type == Feb29_Feb28  &&  dt.date().month() == 2  &&  !QDate::isLeapYear(dt.date().year())) {
669
        return dt;
Laurent Montel's avatar
Laurent Montel committed
670
    }
671
672
673
674
675
676
677
678
    return dt.addDays(1);
}

/******************************************************************************
* Return the date of the last recurrence.
*/
QDate KARecurrence::endDate() const
{
David Jarvie's avatar
David Jarvie committed
679
    KADateTime end = endDateTime();
680
681
682
    return end.isValid() ? end.date() : QDate();
}

Laurent Montel's avatar
Laurent Montel committed
683
void KARecurrence::setEndDate(const QDate &endDate)
684
685
686
687
{
    d->mRecurrence.setEndDate(endDate);
}

David Jarvie's avatar
David Jarvie committed
688
void KARecurrence::setEndDateTime(const KADateTime &endDateTime)
689
{
David Jarvie's avatar
David Jarvie committed
690
    d->mRecurrence.setEndDateTime(endDateTime.qDateTime());
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
}

bool KARecurrence::allDay() const
{
    return d->mRecurrence.allDay();
}

void KARecurrence::setRecurReadOnly(bool readOnly)
{
    d->mRecurrence.setRecurReadOnly(readOnly);
}

bool KARecurrence::recurReadOnly() const
{
    return d->mRecurrence.recurReadOnly();
}

bool KARecurrence::recurs() const
{
    return d->mRecurrence.recurs();
}

QBitArray KARecurrence::days() const
{
    return d->mRecurrence.days();
}

QList<RecurrenceRule::WDayPos> KARecurrence::monthPositions() const
{
    return d->mRecurrence.monthPositions();
}

QList<int> KARecurrence::monthDays() const
{
    return d->mRecurrence.monthDays();
}

QList<int> KARecurrence::yearDays() const
{
    return d->mRecurrence.yearDays();
}

QList<int> KARecurrence::yearDates() const
{
    return d->mRecurrence.yearDates();
}

QList<int> KARecurrence::yearMonths() const
{
    return d->mRecurrence.yearMonths();
}

QList<RecurrenceRule::WDayPos> KARecurrence::yearPositions() const
{
    return d->mRecurrence.yearPositions();
}

Laurent Montel's avatar
Laurent Montel committed
748
void KARecurrence::addWeeklyDays(const QBitArray &days)
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
{
    d->mRecurrence.addWeeklyDays(days);
}

void KARecurrence::addYearlyDay(int day)
{
    d->mRecurrence.addYearlyDay(day);
}

void KARecurrence::addYearlyDate(int date)
{
    d->mRecurrence.addYearlyDate(date);
}

void KARecurrence::addYearlyMonth(short month)
{
    d->mRecurrence.addYearlyMonth(month);
}

Laurent Montel's avatar
Laurent Montel committed
768
void KARecurrence::addYearlyPos(short pos, const QBitArray &days)
769
770
771
772
{
    d->mRecurrence.addYearlyPos(pos, days);
}

Laurent Montel's avatar
Laurent Montel committed
773
void KARecurrence::addMonthlyPos(short pos, const QBitArray &days)
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
{
    d->mRecurrence.addMonthlyPos(pos, days);
}

void KARecurrence::addMonthlyPos(short pos, ushort day)
{
    d->mRecurrence.addMonthlyPos(pos, day);
}

void KARecurrence::addMonthlyDate(short day)
{
    d->mRecurrence.addMonthlyDate(day);
}

/******************************************************************************
* Get the next time the recurrence occurs, strictly after a specified time.
*/
David Jarvie's avatar
David Jarvie committed
791
KADateTime KARecurrence::getNextDateTime(const KADateTime &preDateTime) const
Laurent Montel's avatar
Laurent Montel committed
792
793
{
    switch (type()) {
Laurent Montel's avatar
Laurent Montel committed
794
795
796
797
    case ANNUAL_DATE:
    case ANNUAL_POS: {
        Recurrence recur;
        writeRecurrence(recur);
David Jarvie's avatar
David Jarvie committed
798
        return KADateTime(recur.getNextDateTime(preDateTime.qDateTime()));
Laurent Montel's avatar
Laurent Montel committed
799
800
    }
    default:
David Jarvie's avatar
David Jarvie committed
801
        return KADateTime(d->mRecurrence.getNextDateTime(preDateTime.qDateTime()));
802
803
804
805
806
807
    }
}

/******************************************************************************
* Get the previous time the recurrence occurred, strictly before a specified time.
*/
David Jarvie's avatar
David Jarvie committed
808
KADateTime KARecurrence::getPreviousDateTime(const KADateTime &afterDateTime) const
Laurent Montel's avatar
Laurent Montel committed
809
810
{
    switch (type()) {
Laurent Montel's avatar
Laurent Montel committed
811
812
813
814
    case ANNUAL_DATE:
    case ANNUAL_POS: {
        Recurrence recur;
        writeRecurrence(recur);
David Jarvie's avatar
David Jarvie committed
815
        return KADateTime(recur.getPreviousDateTime(afterDateTime.qDateTime()));
Laurent Montel's avatar
Laurent Montel committed
816
817
    }
    default:
David Jarvie's avatar
David Jarvie committed
818
        return KADateTime(d->mRecurrence.getPreviousDateTime(afterDateTime.qDateTime()));
819
820
821
822
823
824
825
    }
}

/******************************************************************************
* Return whether the event will recur on the specified date.
* The start date only returns true if it matches the recurrence rules.
*/
David Jarvie's avatar
David Jarvie committed
826
bool KARecurrence::recursOn(const QDate &dt, const KADateTime::Spec &timeSpec) const
827
{
David Jarvie's avatar
David Jarvie committed
828
    if (!d->mRecurrence.recursOn(dt, Private::toTimeZone(timeSpec))) {
829
        return false;
Laurent Montel's avatar
Laurent Montel committed
830
831
    }
    if (dt != d->mRecurrence.startDate()) {
832
        return true;
Laurent Montel's avatar
Laurent Montel committed
833
    }
834
835
    // We know now that it isn't in EXDATES or EXRULES,
    // so we just need to check if it's in RDATES or RRULES
Laurent Montel's avatar
Laurent Montel committed
836
    if (d->mRecurrence.rDates().contains(dt)) {
837
        return true;
Laurent Montel's avatar
Laurent Montel committed
838
    }
839
    const RecurrenceRule::List rulelist = d->mRecurrence.rRules();
David Jarvie's avatar
David Jarvie committed
840
841
    for (const RecurrenceRule *rule : rulelist)
        if (rule->recursOn(dt, Private::toTimeZone(timeSpec))) {
842
            return true;
Laurent Montel's avatar
Laurent Montel committed
843
        }
844
    const auto dtlist = d->mRecurrence.rDateTimes();
David Jarvie's avatar
David Jarvie committed
845
846
    for (const QDateTime &dtime : dtlist)
        if (dtime.date() == dt) {
847
            return true;
Laurent Montel's avatar
Laurent Montel committed
848
        }
849
850
851
    return false;
}

David Jarvie's avatar
David Jarvie committed
852
bool KARecurrence::recursAt(const KADateTime &dt) const
853
{
David Jarvie's avatar
David Jarvie committed
854
    return d->mRecurrence.recursAt(dt.qDateTime());
855
856
}

David Jarvie's avatar
David Jarvie committed
857
TimeList KARecurrence::recurTimesOn(const QDate &date, const KADateTime::Spec &timeSpec) const
858
{
David Jarvie's avatar
David Jarvie committed
859
    return d->mRecurrence.recurTimesOn(date, Private::toTimeZone(timeSpec));
860
861
}

David Jarvie's avatar
David Jarvie committed
862
DateTimeList KARecurrence::timesInInterval(const KADateTime &start, const KADateTime &end) const
863
{
David Jarvie's avatar
David Jarvie committed
864
    const auto l = d->mRecurrence.timesInInterval(start.qDateTime(), end.qDateTime());
865
866
867
    DateTimeList rv;
    rv.reserve(l.size());
    for (const auto &qdt : l) {
Daniel Vrátil's avatar
Daniel Vrátil committed
868
        rv << qdt;
869
870
    }
    return rv;
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
}

int KARecurrence::frequency() const
{
    return d->mRecurrence.frequency();
}

void KARecurrence::setFrequency(int freq)
{
    d->mRecurrence.setFrequency(freq);
}

int KARecurrence::duration() const
{
    return d->mRecurrence.duration();
}

void KARecurrence::setDuration(int duration)
{
    d->mRecurrence.setDuration(duration);
}

David Jarvie's avatar
David Jarvie committed
893
int KARecurrence::durationTo(const KADateTime &dt) const
894
{
David Jarvie's avatar
David Jarvie committed
895
    return d->mRecurrence.durationTo(dt.qDateTime());
896
897
}

Laurent Montel's avatar
Laurent Montel committed
898
int KARecurrence::durationTo(const QDate &date) const
899
900
901
902
903
904
905
906
{
    return d->mRecurrence.durationTo(date);
}

/******************************************************************************
* Find the duration of two RRULEs combined.
* Use the shorter of the two if they differ.
*/
Laurent Montel's avatar
Laurent Montel committed
907
int KARecurrence::Private::combineDurations(const RecurrenceRule *rrule1, const RecurrenceRule *rrule2, QDate &end) const
908
909
910
{
    int count1 = rrule1->duration();
    int count2 = rrule2->duration();
Laurent Montel's avatar
Laurent Montel committed
911
    if (count1 == -1  &&  count2 == -1) {
912
        return -1;
Laurent Montel's avatar
Laurent Montel committed
913
    }
914
915
916

    // One of the RRULEs may not recur at all if the recurrence count is small.
    // In this case, its end date will have been set to the start date.
Laurent Montel's avatar
Laurent Montel committed
917
    if (count1  &&  !count2  &&  rrule2->endDt().date() == mRecurrence.startDateTime().date()) {
918
        return count1;
Laurent Montel's avatar
Laurent Montel committed
919
920
    }
    if (count2  &&  !count1  &&  rrule1->endDt().date() == mRecurrence.startDateTime().date()) {
921
        return count2;
Laurent Montel's avatar
Laurent Montel committed
922
    }
923
924
925
926
927

    /* The duration counts will be different even for RRULEs of the same length,
     * because the first RRULE only actually occurs every 4 years. So we need to
     * compare the end dates.
     */
Laurent Montel's avatar
Laurent Montel committed
928
    if (!count1  ||  !count2) {
929
        count1 = count2 = 0;
Laurent Montel's avatar
Laurent Montel committed
930
    }
931
    // Get the two rules sorted by end date.
David Jarvie's avatar
David Jarvie committed
932
933
    KADateTime end1(rrule1->endDt());
    KADateTime end2(rrule2->endDt());
Laurent Montel's avatar
Laurent Montel committed
934
    if (end1.date() == end2.date()) {
935
936
937
        end = end1.date();
        return count1 + count2;
    }
Laurent Montel's avatar
Laurent Montel committed
938
939
    const RecurrenceRule *rr1;    // earlier end date
    const RecurrenceRule *rr2;    // later end date
940
    if (end2.isValid()
David Jarvie's avatar
David Jarvie committed
941
    &&  (!end1.isValid()  ||  end1.date() > end2.date())) {
942
943
944
        // Swap the two rules to make rr1 have the earlier end date
        rr1 = rrule2;
        rr2 = rrule1;
David Jarvie's avatar
David Jarvie committed
945
        const KADateTime e = end1;
946
947
        end1 = end2;
        end2 = e;
Laurent Montel's avatar
Laurent Montel committed
948
    } else {
949
950
951
952
953
954
955
        rr1 = rrule1;
        rr2 = rrule2;
    }

    // Get the date of the next occurrence after the end of the earlier ending rule
    RecurrenceRule rr(*rr1);
    rr.setDuration(-1);
David Jarvie's avatar
David Jarvie committed
956
    KADateTime next1(rr.getNextDate(end1.qDateTime()));
957
    next1.setDateOnly(true);
Laurent Montel's avatar
Laurent Montel committed
958
    if (!next1.isValid()) {
959
        end = end1.date();
Laurent Montel's avatar
Laurent Montel committed
960
961
    } else {
        if (end2.isValid()  &&  next1 > end2) {
962
963
964
965
966
967
            // The next occurrence after the end of the earlier ending rule
            // is later than the end of the later ending rule. So simply use
            // the end date of the later rule.
            end = end2.date();
            return count1 + count2;
        }
David Jarvie's avatar
David Jarvie committed
968
        const QDate prev2 = rr2->getPreviousDate(next1.qDateTime()).date();
969
970
        end = (prev2 > end1.date()) ? prev2 : end1.date();
    }
Laurent Montel's avatar
Laurent Montel committed
971
    if (count2) {
972
        count2 = rr2->durationTo(end);
Laurent Montel's avatar
Laurent Montel committed
973
    }
974
975
976
977
978
979
980
981
982
    return count1 + count2;
}

/******************************************************************************
* Return the longest interval between recurrences.
* Reply = 0 if it never recurs.
*/
Duration KARecurrence::longestInterval() const
{
983
    const int freq = d->mRecurrence.frequency();
Laurent Montel's avatar
Laurent Montel committed
984
    switch (type()) {
Laurent Montel's avatar
Laurent Montel committed
985
986
    case MINUTELY:
        return Duration(freq * 60, Duration::Seconds);
Laurent Montel's avatar
Laurent Montel committed
987

Laurent Montel's avatar
Laurent Montel committed
988
989
990
991
992
    case DAILY: {
        const QList<RecurrenceRule::WDayPos> days = d->mRecurrence.defaultRRuleConst()->byDays();
        if (days.isEmpty()) {
            return Duration(freq, Duration::Days);
        }
993

Laurent Montel's avatar
Laurent Montel committed
994
995
996
997
        // After applying the frequency, the specified days of the week
        // further restrict when the recurrence occurs.
        // So the maximum interval may be greater than the frequency.
        bool ds[7] = { false, false, false, false, false, false, false };
David Jarvie's avatar
David Jarvie committed
998
999
1000
        for (const RecurrenceRule::WDayPos &day : days)
            if (day.pos() == 0) {
                ds[day.day() - 1] = true;