diff --git a/autotests/testdateserialization.cpp b/autotests/testdateserialization.cpp index f691d8b0e..3ceea1a8d 100644 --- a/autotests/testdateserialization.cpp +++ b/autotests/testdateserialization.cpp @@ -1,83 +1,91 @@ /* * SPDX-FileCopyrightText: 2020 Glen Ditchfield * * SPDX-License-Identifier: LGPL-3.0-or-later */ #include "testdateserialization.h" #include "icalformat.h" #include "memorycalendar.h" #include #include QTEST_MAIN(TestDateSerialization) using namespace KCalendarCore; // Check that serialization and deserialization of a minimal recurring todo // preserves the start and due dates of the todo and its first occurrence. // See bug 345498. void TestDateSerialization::testNewRecurringTodo() { +#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) + QDateTime startDate = QDate(2015, 3, 24).startOfDay(); +#else QDateTime startDate { QDate(2015, 3, 24) }; +#endif QDateTime dueDate { startDate.addDays(1) }; Todo::Ptr todo(new Todo); todo->setDtStart(startDate); todo->setDtDue(dueDate, true); todo->setAllDay(true); todo->recurrence()->setMonthly(1); MemoryCalendar::Ptr cal { new MemoryCalendar(QTimeZone::utc()) }; cal->addIncidence(todo); ICalFormat format; const QString result = format.toString(cal, QString()); Incidence::Ptr i = format.fromString(result); QVERIFY(i); QVERIFY(i->type() == IncidenceBase::IncidenceType::TypeTodo); Todo::Ptr newTodo = i.staticCast(); QCOMPARE(newTodo->dtStart(true), startDate); QCOMPARE(newTodo->dtStart(false), startDate); QCOMPARE(newTodo->dtDue(true), dueDate); QCOMPARE(newTodo->dtDue(false), dueDate); } // Check that serialization and deserialization of a minimal recurring todo // that has been completed once preserves the start and due dates of the todo // and correctly calculates the start and due dates of the next occurrence. // See bug 345565. void TestDateSerialization::testTodoCompletedOnce() { +#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) + QDateTime startDate = QDate::currentDate().startOfDay(); +#else QDateTime startDate { QDate::currentDate() }; +#endif QDateTime dueDate { startDate.addDays(1) }; Todo::Ptr todo(new Todo); todo->setDtStart(startDate); todo->setDtDue(dueDate, true); todo->setAllDay(true); todo->recurrence()->setMonthly(1); MemoryCalendar::Ptr cal { new MemoryCalendar(QTimeZone::utc()) }; cal->addIncidence(todo); ICalFormat format; QString result = format.toString(cal, QString()); Incidence::Ptr i = format.fromString(result); QVERIFY(i); QVERIFY(i->type() == IncidenceBase::IncidenceType::TypeTodo); todo = i.staticCast(); todo->setCompleted(dueDate); cal = MemoryCalendar::Ptr {new MemoryCalendar(QTimeZone::utc()) }; cal->addIncidence(todo); result = format.toString(cal, QString()); QCOMPARE(todo->dtStart(true), startDate); QCOMPARE(todo->dtStart(false), startDate.addMonths(1)); QCOMPARE(todo->dtDue(true), dueDate); QCOMPARE(todo->dtDue(false), dueDate.addMonths(1)); } diff --git a/autotests/testfreebusyperiod.cpp b/autotests/testfreebusyperiod.cpp index e12802e9f..b686efb9c 100644 --- a/autotests/testfreebusyperiod.cpp +++ b/autotests/testfreebusyperiod.cpp @@ -1,104 +1,108 @@ /* This file is part of the kcalcore library. SPDX-FileCopyrightText: 2010 Casey Link SPDX-FileCopyrightText: 2009-2010 Klaralvdalens Datakonsult AB, a KDAB Group company SPDX-License-Identifier: LGPL-2.0-or-later */ #include "testfreebusyperiod.h" #include "freebusyperiod.h" #include QTEST_MAIN(FreeBusyPeriodTest) using namespace KCalendarCore; void FreeBusyPeriodTest::testValidity() { const QDateTime p1DateTime(QDate(2006, 8, 30), QTime(7, 0, 0), Qt::UTC); FreeBusyPeriod p1(p1DateTime, Duration(60)); QString summary = QStringLiteral("I can haz summary?"); QString location = QStringLiteral("The Moon"); p1.setSummary(summary); p1.setLocation(location); QVERIFY(p1.hasDuration()); QCOMPARE(p1.duration().asSeconds(), 60); QVERIFY(p1.start() == QDateTime(QDate(2006, 8, 30), QTime(7, 0, 0), Qt::UTC)); QCOMPARE(p1.summary(), summary); QCOMPARE(p1.location(), location); } void FreeBusyPeriodTest::testAssign() { const QDateTime p1DateTime(QDate(2006, 8, 30), QTime(7, 0, 0), Qt::UTC); FreeBusyPeriod p1(p1DateTime, Duration(60)); FreeBusyPeriod p2; QString summary = QStringLiteral("I can haz summary?"); QString location = QStringLiteral("The Moon"); p1.setSummary(summary); p1.setLocation(location); p2 = p1; QVERIFY(p2.hasDuration()); QVERIFY(p2.duration().asSeconds() == 60); QVERIFY(p2.start() == QDateTime(QDate(2006, 8, 30), QTime(7, 0, 0), Qt::UTC)); QCOMPARE(p1.summary(), summary); QCOMPARE(p1.location(), location); } void FreeBusyPeriodTest::testDataStreamOut() { const QDateTime p1DateTime(QDate(2006, 8, 30), QTime(7, 0, 0), Qt::UTC); FreeBusyPeriod p1(p1DateTime, Duration(60)); p1.setSummary(QStringLiteral("I can haz summary?")); p1.setLocation(QStringLiteral("The Moon")); QByteArray byteArray; QDataStream out_stream(&byteArray, QIODevice::WriteOnly); out_stream << p1; QDataStream in_stream(&byteArray, QIODevice::ReadOnly); Period p2; Period periodParent = static_cast(p1); in_stream >> p2; QVERIFY(periodParent == p2); QString summary; in_stream >> summary; QCOMPARE(summary, p1.summary()); QString location; in_stream >> location; QCOMPARE(location, p1.location()); } void FreeBusyPeriodTest::testDataStreamIn() { +#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) + const QDateTime p1DateTime = QDate(2006, 8, 30).startOfDay(); +#else const QDateTime p1DateTime(QDate(2006, 8, 30)); +#endif const Duration duration(24 * 60 * 60); FreeBusyPeriod p1(p1DateTime, duration); p1.setSummary(QStringLiteral("I can haz summary?")); p1.setLocation(QStringLiteral("The Moon")); QByteArray byteArray; QDataStream out_stream(&byteArray, QIODevice::WriteOnly); out_stream << p1; QDataStream in_stream(&byteArray, QIODevice::ReadOnly); FreeBusyPeriod p2; in_stream >> p2; QCOMPARE(p2, p1); } diff --git a/autotests/testicalformat.cpp b/autotests/testicalformat.cpp index d2912f433..740ee4eee 100644 --- a/autotests/testicalformat.cpp +++ b/autotests/testicalformat.cpp @@ -1,159 +1,163 @@ /* This file is part of the kcalcore library. SPDX-FileCopyrightText: 2010 Klarälvdalens Datakonsult AB, a KDAB Group company SPDX-FileContributor: Sergio Martins SPDX-License-Identifier: LGPL-2.0-or-later */ #include "testicalformat.h" #include "event.h" #include "icalformat.h" #include "memorycalendar.h" #include #include #include QTEST_MAIN(ICalFormatTest) using namespace KCalendarCore; void ICalFormatTest::testCharsets() { ICalFormat format; const QDate currentDate = QDate::currentDate(); Event::Ptr event = Event::Ptr(new Event()); event->setUid(QStringLiteral("12345")); event->setDtStart(QDateTime(currentDate, {})); event->setDtEnd(QDateTime(currentDate.addDays(1), {})); event->setAllDay(true); // ü const QChar latin1_umlaut[] = { 0xFC, QLatin1Char('\0') }; event->setSummary(QString(latin1_umlaut)); // Test if toString( Incidence ) didn't mess charsets const QString serialized = format.toString(event.staticCast()); const QChar utf_umlaut[] = { 0xC3, 0XBC, QLatin1Char('\0') }; QVERIFY(serialized.toUtf8().contains(QString(utf_umlaut).toLatin1().constData())); QVERIFY(!serialized.toUtf8().contains(QString(latin1_umlaut).toLatin1().constData())); QVERIFY(serialized.toLatin1().contains(QString(latin1_umlaut).toLatin1().constData())); QVERIFY(!serialized.toLatin1().contains(QString(utf_umlaut).toLatin1().constData())); // test fromString( QString ) const QString serializedCalendar = QLatin1String("BEGIN:VCALENDAR\nPRODID:-//K Desktop Environment//NONSGML libkcal 3.2//EN\nVERSION:2.0\n") +serialized +QLatin1String("\nEND:VCALENDAR"); Incidence::Ptr event2 = format.fromString(serializedCalendar); QVERIFY(event->summary() == event2->summary()); QVERIFY(event2->summary().toUtf8() == QByteArray(QString(utf_umlaut).toLatin1().constData())); // test save() MemoryCalendar::Ptr calendar(new MemoryCalendar(QTimeZone::utc())); calendar->addIncidence(event); QVERIFY(format.save(calendar, QLatin1String("hommer.ics"))); // Make sure hommer.ics is in UTF-8 QFile file(QStringLiteral("hommer.ics")); QVERIFY(file.open(QIODevice::ReadOnly | QIODevice::Text)); const QByteArray bytesFromFile = file.readAll(); QVERIFY(bytesFromFile.contains(QString(utf_umlaut).toLatin1().constData())); QVERIFY(!bytesFromFile.contains(QString(latin1_umlaut).toLatin1().constData())); file.close(); // Test load: MemoryCalendar::Ptr calendar2(new MemoryCalendar(QTimeZone::utc())); QVERIFY(format.load(calendar2, QLatin1String("hommer.ics"))); QVERIFY(calendar2->incidences().count() == 1); // qDebug() << format.toString( event.staticCast() ); // qDebug() << format.toString( calendar2->incidences().at(0) ); Event::Ptr loadedEvent = calendar2->incidences().at(0).staticCast(); QVERIFY(loadedEvent->summary().toUtf8() == QByteArray(QString(utf_umlaut).toLatin1().constData())); QVERIFY(*loadedEvent == *event); // Test fromRawString() MemoryCalendar::Ptr calendar3(new MemoryCalendar(QTimeZone::utc())); QVERIFY(format.fromRawString(calendar3, bytesFromFile)); QVERIFY(calendar3->incidences().count() == 1); QVERIFY(*calendar3->incidences().at(0) == *event); QFile::remove(QStringLiteral("hommer.ics")); } void ICalFormatTest::testVolatileProperties() { // Volatile properties are not written to the serialized data ICalFormat format; const QDate currentDate = QDate::currentDate(); Event::Ptr event = Event::Ptr(new Event()); event->setUid(QStringLiteral("12345")); event->setDtStart(QDateTime(currentDate, {})); event->setDtEnd(QDateTime(currentDate.addDays(1), {})); event->setAllDay(true); event->setCustomProperty("VOLATILE", "FOO", QStringLiteral("BAR")); QString string = format.toICalString(event); Incidence::Ptr incidence = format.fromString(string); QCOMPARE(incidence->uid(), QStringLiteral("12345")); QVERIFY(incidence->customProperties().isEmpty()); } void ICalFormatTest::testCuType() { ICalFormat format; const QDate currentDate = QDate::currentDate(); Event::Ptr event(new Event()); event->setUid(QStringLiteral("12345")); event->setDtStart(QDateTime(currentDate, {})); event->setDtEnd(QDateTime(currentDate.addDays(1), {})); event->setAllDay(true); Attendee attendee(QStringLiteral("fred"), QStringLiteral("fred@flintstone.com")); attendee.setCuType(Attendee::Resource); event->addAttendee(attendee); const QString serialized = format.toString(event.staticCast()); // test fromString(QString) const QString serializedCalendar = QLatin1String("BEGIN:VCALENDAR\nPRODID:-//K Desktop Environment//NONSGML libkcal 3.2//EN\nVERSION:2.0\n") +serialized +QLatin1String("\nEND:VCALENDAR"); Incidence::Ptr event2 = format.fromString(serializedCalendar); QVERIFY(event2->attendeeCount() == 1); Attendee attendee2 = event2->attendees()[0]; QVERIFY(attendee2.cuType() == attendee.cuType()); QVERIFY(attendee2.name() == attendee.name()); QVERIFY(attendee2.email() == attendee.email()); } void ICalFormatTest::testAlarm() { ICalFormat format; Event::Ptr event(new Event); +#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) + event->setDtStart(QDate(2017, 03, 24).startOfDay()); +#else event->setDtStart(QDateTime(QDate(2017, 03, 24))); +#endif Alarm::Ptr alarm = event->newAlarm(); alarm->setType(Alarm::Display); alarm->setStartOffset(Duration(0)); const QString serialized = QLatin1String("BEGIN:VCALENDAR\nPRODID:-//K Desktop Environment//NONSGML libkcal 3.2//EN\nVERSION:2.0\n") + format.toString(event.staticCast()) + QLatin1String("\nEND:VCALENDAR"); Incidence::Ptr event2 = format.fromString(serialized); Alarm::Ptr alarm2 = event2->alarms()[0]; QCOMPARE(*alarm, *alarm2); } diff --git a/autotests/testtimesininterval.cpp b/autotests/testtimesininterval.cpp index acb558a35..ba0acfb76 100644 --- a/autotests/testtimesininterval.cpp +++ b/autotests/testtimesininterval.cpp @@ -1,244 +1,256 @@ /* This file is part of the kcalcore library. SPDX-FileCopyrightText: 2010 Klarälvdalens Datakonsult AB, a KDAB Group company SPDX-FileContributor: Sergio Martins SPDX-License-Identifier: LGPL-2.0-or-later */ #include "testtimesininterval.h" #include "event.h" #include #include QTEST_MAIN(TimesInIntervalTest) using namespace KCalendarCore; void TimesInIntervalTest::test() { const QDateTime currentDate(QDate::currentDate(), {}); Event *event = new Event(); event->setDtStart(currentDate); event->setDtEnd(currentDate.addDays(1)); event->setAllDay(true); event->setSummary(QStringLiteral("Event1 Summary")); event->recurrence()->setDaily(1); //------------------------------------------------------------------------------------------------ // Just to warm up QVERIFY(event->recurs()); QVERIFY(event->recursAt(currentDate)); //------------------------------------------------------------------------------------------------ // Daily recurrence that never stops. // Should return numDaysInInterval+1 occurrences const int numDaysInInterval = 7; QDateTime start(currentDate); QDateTime end(start.addDays(numDaysInInterval)); start.setTime(QTime(0, 0, 0)); end.setTime(QTime(23, 59, 59)); auto dateList = event->recurrence()->timesInInterval(start, end); QVERIFY(dateList.count() == numDaysInInterval + 1); //------------------------------------------------------------------------------------------------ // start == end == first day of the recurrence, should only return 1 occurrence end = start; end.setTime(QTime(23, 59, 59)); dateList = event->recurrence()->timesInInterval(start, end); QVERIFY(dateList.count() == 1); //------------------------------------------------------------------------------------------------ // Test daily recurrence that only lasts X days const int recurrenceDuration = 3; event->recurrence()->setDuration(recurrenceDuration); end = start.addDays(100); dateList = event->recurrence()->timesInInterval(start, end); QVERIFY(dateList.count() == recurrenceDuration); //------------------------------------------------------------------------------------------------ // Test daily recurrence that only lasts X days, and give start == end == last day of // recurrence. Previous versions of kcal had a bug and didn't return an occurrence start = start.addDays(recurrenceDuration - 1); end = start; start.setTime(QTime(0, 0, 0)); end.setTime(QTime(23, 59, 59)); dateList = event->recurrence()->timesInInterval(start, end); QVERIFY(dateList.count() == 1); //------------------------------------------------------------------------------------------------ } //Test that interval start and end are inclusive void TimesInIntervalTest::testSubDailyRecurrenceIntervalInclusive() { const QDateTime start(QDate(2013, 03, 10), QTime(10, 0, 0), Qt::UTC); const QDateTime end(QDate(2013, 03, 10), QTime(11, 0, 0), Qt::UTC); KCalendarCore::Event::Ptr event(new KCalendarCore::Event()); event->setUid(QStringLiteral("event")); event->setDtStart(start); event->recurrence()->setHourly(1); event->recurrence()->setDuration(2); QList expectedEventOccurrences; expectedEventOccurrences << start << start.addSecs(60 * 60); const auto timesInInterval = event->recurrence()->timesInInterval(start, end); // qDebug() << "timesInInterval " << timesInInterval; for (const auto &dt : timesInInterval) { // qDebug() << dt; QCOMPARE(expectedEventOccurrences.removeAll(dt), 1); } QCOMPARE(expectedEventOccurrences.size(), 0); } //Test that the recurrence dtStart is used for calculation and not the interval start date void TimesInIntervalTest::testSubDailyRecurrence2() { const QDateTime start(QDate(2013, 03, 10), QTime(10, 2, 3), Qt::UTC); const QDateTime end(QDate(2013, 03, 10), QTime(13, 4, 5), Qt::UTC); KCalendarCore::Event::Ptr event(new KCalendarCore::Event()); event->setUid(QStringLiteral("event")); event->setDtStart(start); event->recurrence()->setHourly(1); event->recurrence()->setDuration(2); QList expectedEventOccurrences; expectedEventOccurrences << start << start.addSecs(60 * 60); const auto timesInInterval = event->recurrence()->timesInInterval(start.addSecs(-20), end.addSecs(20)); // qDebug() << "timesInInterval " << timesInInterval; for (const auto &dt : timesInInterval) { // qDebug() << dt; QCOMPARE(expectedEventOccurrences.removeAll(dt), 1); } QCOMPARE(expectedEventOccurrences.size(), 0); } void TimesInIntervalTest::testSubDailyRecurrenceIntervalLimits() { const QDateTime start(QDate(2013, 03, 10), QTime(10, 2, 3), Qt::UTC); const QDateTime end(QDate(2013, 03, 10), QTime(12, 2, 3), Qt::UTC); KCalendarCore::Event::Ptr event(new KCalendarCore::Event()); event->setUid(QStringLiteral("event")); event->setDtStart(start); event->recurrence()->setHourly(1); event->recurrence()->setDuration(3); QList expectedEventOccurrences; expectedEventOccurrences << start.addSecs(60 * 60); const auto timesInInterval = event->recurrence()->timesInInterval(start.addSecs(1), end.addSecs(-1)); // qDebug() << "timesInInterval " << timesInInterval; for (const auto &dt : timesInInterval) { // qDebug() << dt; QCOMPARE(expectedEventOccurrences.removeAll(dt), 1); } QCOMPARE(expectedEventOccurrences.size(), 0); } void TimesInIntervalTest::testLocalTimeHandlingNonAllDay() { // Create an event which occurs every weekday of every week, // starting from Friday the 11th of October, from 12 pm until 1 pm, clock time, // and lasts for two weeks, with three exception datetimes, // (only two of which will apply). QTimeZone anotherZone(QTimeZone::systemTimeZoneId().contains("Toronto") ? QTimeZone(QByteArray("Pacific/Midway")) : QTimeZone(QByteArray("America/Toronto"))); Event event; event.setAllDay(false); event.setDtStart(QDateTime(QDate(2019, 10, 11), QTime(12, 0), Qt::LocalTime)); RecurrenceRule * const rule = new RecurrenceRule(); rule->setRecurrenceType(RecurrenceRule::rDaily); rule->setStartDt(event.dtStart()); rule->setFrequency(1); rule->setDuration(14); rule->setByDays(QList() << RecurrenceRule::WDayPos(0, 1) // Monday << RecurrenceRule::WDayPos(0, 2) // Tuesday << RecurrenceRule::WDayPos(0, 3) // Wednesday << RecurrenceRule::WDayPos(0, 4) // Thursday << RecurrenceRule::WDayPos(0, 5)); // Friday Recurrence *recurrence = event.recurrence(); recurrence->addRRule(rule); // 12 o'clock in local time, will apply. recurrence->addExDateTime(QDateTime(QDate(2019, 10, 15), QTime(12, 0), Qt::LocalTime)); // 12 o'clock in another time zone, will not apply. recurrence->addExDateTime(QDateTime(QDate(2019, 10, 17), QTime(12, 0), anotherZone)); // The time in another time zone, corresponding to 12 o'clock in the system time zone, will apply. recurrence->addExDateTime(QDateTime(QDate(2019, 10, 24), QTime(12, 00), QTimeZone::systemTimeZone()).toTimeZone(anotherZone)); // Expand the events and within a wide interval const DateTimeList timesInInterval = recurrence->timesInInterval(QDateTime(QDate(2019, 10, 05), QTime(0, 0)), QDateTime(QDate(2019, 10, 25), QTime(23, 59))); // ensure that the expansion does not include weekend days, // nor either of the exception date times. const QList expectedDays { 11, 14, 16, 17, 18, 21, 22, 23, 25 }; for (int day : expectedDays) { QVERIFY(timesInInterval.contains(QDateTime(QDate(2019, 10, day), QTime(12, 0), Qt::LocalTime))); } QCOMPARE(timesInInterval.size(), expectedDays.size()); } void TimesInIntervalTest::testLocalTimeHandlingAllDay() { // Create an event which occurs every weekday of every week, // starting from Friday the 11th of October, and lasts for two weeks, // with four exception datetimes (only three of which will apply). QTimeZone anotherZone(QTimeZone::systemTimeZoneId().contains("Toronto") ? QTimeZone(QByteArray("Pacific/Midway")) : QTimeZone(QByteArray("America/Toronto"))); Event event; event.setAllDay(true); +#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) + event.setDtStart(QDate(2019, 10, 11).startOfDay()); +#else event.setDtStart(QDateTime(QDate(2019, 10, 11))); +#endif RecurrenceRule * const rule = new RecurrenceRule(); rule->setRecurrenceType(RecurrenceRule::rDaily); rule->setStartDt(event.dtStart()); rule->setFrequency(1); rule->setDuration(14); rule->setByDays(QList() << RecurrenceRule::WDayPos(0, 1) // Monday << RecurrenceRule::WDayPos(0, 2) // Tuesday << RecurrenceRule::WDayPos(0, 3) // Wednesday << RecurrenceRule::WDayPos(0, 4) // Thursday << RecurrenceRule::WDayPos(0, 5)); // Friday Recurrence *recurrence = event.recurrence(); recurrence->addRRule(rule); // A simple date, will apply. recurrence->addExDate(QDate(2019, 10, 14)); // A date only local time, will apply. +#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) + recurrence->addExDateTime(QDate(2019, 10, 15).startOfDay()); +#else recurrence->addExDateTime(QDateTime(QDate(2019, 10, 15))); +#endif // A date time starting at 00:00 in another zone, will not apply. recurrence->addExDateTime(QDateTime(QDate(2019, 10, 17), QTime(), anotherZone)); // A date time starting at 00:00 in the system time zone, will apply. recurrence->addExDateTime(QDateTime(QDate(2019, 10, 24), QTime(), QTimeZone::systemTimeZone())); // Expand the events and within a wide interval const DateTimeList timesInInterval = recurrence->timesInInterval(QDateTime(QDate(2019, 10, 05), QTime(0, 0)), QDateTime(QDate(2019, 10, 25), QTime(23, 59))); // ensure that the expansion does not include weekend days, // nor either of the exception date times. const QList expectedDays { 11, 16, 17, 18, 21, 22, 23, 25 }; for (int day : expectedDays) { +#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) + QVERIFY(timesInInterval.contains(QDate(2019, 10, day).startOfDay())); +#else QVERIFY(timesInInterval.contains(QDateTime(QDate(2019, 10, day)))); +#endif } QCOMPARE(timesInInterval.size(), expectedDays.size()); } diff --git a/src/customproperties.cpp b/src/customproperties.cpp index 93f1da0a9..79af49963 100644 --- a/src/customproperties.cpp +++ b/src/customproperties.cpp @@ -1,256 +1,257 @@ /* This file is part of the kcalcore library. SPDX-FileCopyrightText: 2002, 2006, 2010 David Jarvie SPDX-License-Identifier: LGPL-2.0-or-later */ /** @file This file is part of the API for handling calendar data and defines the CustomProperties class. @brief A class to manage custom calendar properties. @author David Jarvie \ */ #include "customproperties.h" #include #include "kcalendarcore_debug.h" using namespace KCalendarCore; //@cond PRIVATE static bool checkName(const QByteArray &name); class Q_DECL_HIDDEN CustomProperties::Private { public: bool operator==(const Private &other) const; QMap mProperties; // custom calendar properties QMap mPropertyParameters; // Volatile properties are not written back to the serialized format and are not compared in operator== // They are only used for runtime purposes and are not part of the payload. QMap mVolatileProperties; bool isVolatileProperty(const QString &name) const { return name.startsWith(QLatin1String("X-KDE-VOLATILE")); } }; bool CustomProperties::Private::operator==(const CustomProperties::Private &other) const { if (mProperties.count() != other.mProperties.count()) { // qCDebug(KCALCORE_LOG) << "Property count is different:" << mProperties << other.mProperties; return false; } for (QMap::ConstIterator it = mProperties.begin(); it != mProperties.end(); ++it) { QMap::ConstIterator itOther = other.mProperties.find(it.key()); if (itOther == other.mProperties.end() || itOther.value() != it.value()) { return false; } } for (QMap::ConstIterator it = mPropertyParameters.begin(); it != mPropertyParameters.end(); ++it) { QMap::ConstIterator itOther = other.mPropertyParameters.find(it.key()); if (itOther == other.mPropertyParameters.end() || itOther.value() != it.value()) { return false; } } return true; } //@endcond CustomProperties::CustomProperties() : d(new Private) { } CustomProperties::CustomProperties(const CustomProperties &cp) : d(new Private(*cp.d)) { } CustomProperties &CustomProperties::operator=(const CustomProperties &other) { // check for self assignment if (&other == this) { return *this; } *d = *other.d; return *this; } CustomProperties::~CustomProperties() { delete d; } bool CustomProperties::operator==(const CustomProperties &other) const { return *d == *other.d; } void CustomProperties::setCustomProperty(const QByteArray &app, const QByteArray &key, const QString &value) { if (value.isNull() || key.isEmpty() || app.isEmpty()) { return; } QByteArray property = "X-KDE-" + app + '-' + key; if (!checkName(property)) { return; } customPropertyUpdate(); if (d->isVolatileProperty(QLatin1String(property))) { d->mVolatileProperties[property] = value; } else { d->mProperties[property] = value; } customPropertyUpdated(); } void CustomProperties::removeCustomProperty(const QByteArray &app, const QByteArray &key) { removeNonKDECustomProperty(QByteArray("X-KDE-" + app + '-' + key)); } QString CustomProperties::customProperty(const QByteArray &app, const QByteArray &key) const { return nonKDECustomProperty(QByteArray("X-KDE-" + app + '-' + key)); } QByteArray CustomProperties::customPropertyName(const QByteArray &app, const QByteArray &key) { QByteArray property("X-KDE-" + app + '-' + key); if (!checkName(property)) { return QByteArray(); } return property; } void CustomProperties::setNonKDECustomProperty(const QByteArray &name, const QString &value, const QString ¶meters) { if (value.isNull() || !checkName(name)) { return; } customPropertyUpdate(); d->mProperties[name] = value; d->mPropertyParameters[name] = parameters; customPropertyUpdated(); } void CustomProperties::removeNonKDECustomProperty(const QByteArray &name) { if (d->mProperties.contains(name)) { customPropertyUpdate(); d->mProperties.remove(name); d->mPropertyParameters.remove(name); customPropertyUpdated(); } else if (d->mVolatileProperties.contains(name)) { customPropertyUpdate(); d->mVolatileProperties.remove(name); customPropertyUpdated(); } } QString CustomProperties::nonKDECustomProperty(const QByteArray &name) const { return d->isVolatileProperty(QLatin1String(name)) ? d->mVolatileProperties.value(name) : d->mProperties.value(name); } QString CustomProperties::nonKDECustomPropertyParameters(const QByteArray &name) const { return d->mPropertyParameters.value(name); } void CustomProperties::setCustomProperties(const QMap &properties) { bool changed = false; for (QMap::ConstIterator it = properties.begin(); it != properties.end(); ++it) { // Validate the property name and convert any null string to empty string if (checkName(it.key())) { if (d->isVolatileProperty(QLatin1String(it.key()))) { d->mVolatileProperties[it.key()] = it.value().isNull() ? QLatin1String("") : it.value(); } else { d->mProperties[it.key()] = it.value().isNull() ? QLatin1String("") : it.value(); } if (!changed) { customPropertyUpdate(); } changed = true; } } if (changed) { customPropertyUpdated(); } } QMap CustomProperties::customProperties() const { - QMap result; - result.unite(d->mProperties); - result.unite(d->mVolatileProperties); + QMap result = d->mProperties; + for (auto it = d->mVolatileProperties.begin(), end = d->mVolatileProperties.end(); it != end; ++it) { + result.insert(it.key(), it.value()); + } return result; } void CustomProperties::customPropertyUpdate() { } void CustomProperties::customPropertyUpdated() { } //@cond PRIVATE bool checkName(const QByteArray &name) { // Check that the property name starts with 'X-' and contains // only the permitted characters const char *n = name.constData(); int len = name.length(); if (len < 2 || n[0] != 'X' || n[1] != '-') { return false; } for (int i = 2; i < len; ++i) { char ch = n[i]; if ((ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9') || ch == '-') { continue; } return false; // invalid character found } return true; } //@endcond QDataStream &KCalendarCore::operator<<(QDataStream &stream, const KCalendarCore::CustomProperties &properties) { return stream << properties.d->mProperties << properties.d->mPropertyParameters; } QDataStream &KCalendarCore::operator>>(QDataStream &stream, KCalendarCore::CustomProperties &properties) { properties.d->mVolatileProperties.clear(); return stream >> properties.d->mProperties >> properties.d->mPropertyParameters; } diff --git a/src/recurrence.cpp b/src/recurrence.cpp index a685a534f..2be967c76 100644 --- a/src/recurrence.cpp +++ b/src/recurrence.cpp @@ -1,1546 +1,1549 @@ /* This file is part of kcalcore library. SPDX-FileCopyrightText: 1998 Preston Brown SPDX-FileCopyrightText: 2001 Cornelius Schumacher SPDX-FileCopyrightText: 2002, 2006 David Jarvie SPDX-FileCopyrightText: 2005 Reinhold Kainhofer SPDX-License-Identifier: LGPL-2.0-or-later */ #include "recurrence.h" #include "utils_p.h" #include "recurrencehelper_p.h" #include "kcalendarcore_debug.h" #include #include #include #include using namespace KCalendarCore; //@cond PRIVATE class Q_DECL_HIDDEN KCalendarCore::Recurrence::Private { public: Private() : mCachedType(rMax), mAllDay(false), mRecurReadOnly(false) { } Private(const Private &p) : mRDateTimes(p.mRDateTimes), mRDates(p.mRDates), mExDateTimes(p.mExDateTimes), mExDates(p.mExDates), mStartDateTime(p.mStartDateTime), mCachedType(p.mCachedType), mAllDay(p.mAllDay), mRecurReadOnly(p.mRecurReadOnly) { } bool operator==(const Private &p) const; RecurrenceRule::List mExRules; RecurrenceRule::List mRRules; QList mRDateTimes; DateList mRDates; QList mExDateTimes; DateList mExDates; QDateTime mStartDateTime; // date/time of first recurrence QList mObservers; // Cache the type of the recurrence with the old system (e.g. MonthlyPos) mutable ushort mCachedType; bool mAllDay = false; // the recurrence has no time, just a date bool mRecurReadOnly = false; }; bool Recurrence::Private::operator==(const Recurrence::Private &p) const { // qCDebug(KCALCORE_LOG) << mStartDateTime << p.mStartDateTime; if ((mStartDateTime != p.mStartDateTime && (mStartDateTime.isValid() || p.mStartDateTime.isValid())) || mAllDay != p.mAllDay || mRecurReadOnly != p.mRecurReadOnly || mExDates != p.mExDates || mExDateTimes != p.mExDateTimes || mRDates != p.mRDates || mRDateTimes != p.mRDateTimes) { return false; } // Compare the rrules, exrules! Assume they have the same order... This only // matters if we have more than one rule (which shouldn't be the default anyway) int i; int end = mRRules.count(); if (end != p.mRRules.count()) { return false; } for (i = 0; i < end; ++i) { if (*mRRules[i] != *p.mRRules[i]) { return false; } } end = mExRules.count(); if (end != p.mExRules.count()) { return false; } for (i = 0; i < end; ++i) { if (*mExRules[i] != *p.mExRules[i]) { return false; } } return true; } //@endcond Recurrence::Recurrence() : d(new KCalendarCore::Recurrence::Private()) { } Recurrence::Recurrence(const Recurrence &r) : RecurrenceRule::RuleObserver(), d(new KCalendarCore::Recurrence::Private(*r.d)) { int i, end; d->mRRules.reserve(r.d->mRRules.count()); for (i = 0, end = r.d->mRRules.count(); i < end; ++i) { RecurrenceRule *rule = new RecurrenceRule(*r.d->mRRules[i]); d->mRRules.append(rule); rule->addObserver(this); } d->mExRules.reserve(r.d->mExRules.count()); for (i = 0, end = r.d->mExRules.count(); i < end; ++i) { RecurrenceRule *rule = new RecurrenceRule(*r.d->mExRules[i]); d->mExRules.append(rule); rule->addObserver(this); } } Recurrence::~Recurrence() { qDeleteAll(d->mExRules); qDeleteAll(d->mRRules); delete d; } bool Recurrence::operator==(const Recurrence &recurrence) const { return *d == *recurrence.d; } #if KCALENDARCORE_BUILD_DEPRECATED_SINCE(5, 64) Recurrence &Recurrence::operator=(const Recurrence &recurrence) { // check for self assignment if (&recurrence == this) { return *this; } // ### this copies the pointers in mExRules and mRRules eventually resulting in a double free! // fortunately however this function is unused, we just can't remove it just yet, due to ABI guarantees +QT_WARNING_PUSH +QT_WARNING_DISABLE_GCC("-Wdeprecated-copy") *d = *recurrence.d; +QT_WARNING_POP return *this; } #endif void Recurrence::addObserver(RecurrenceObserver *observer) { if (!d->mObservers.contains(observer)) { d->mObservers.append(observer); } } void Recurrence::removeObserver(RecurrenceObserver *observer) { d->mObservers.removeAll(observer); } QDateTime Recurrence::startDateTime() const { return d->mStartDateTime; } bool Recurrence::allDay() const { return d->mAllDay; } void Recurrence::setAllDay(bool allDay) { if (d->mRecurReadOnly || allDay == d->mAllDay) { return; } d->mAllDay = allDay; for (int i = 0, end = d->mRRules.count(); i < end; ++i) { d->mRRules[i]->setAllDay(allDay); } for (int i = 0, end = d->mExRules.count(); i < end; ++i) { d->mExRules[i]->setAllDay(allDay); } updated(); } RecurrenceRule *Recurrence::defaultRRule(bool create) const { if (d->mRRules.isEmpty()) { if (!create || d->mRecurReadOnly) { return nullptr; } RecurrenceRule *rrule = new RecurrenceRule(); rrule->setStartDt(startDateTime()); const_cast(this)->addRRule(rrule); return rrule; } else { return d->mRRules[0]; } } RecurrenceRule *Recurrence::defaultRRuleConst() const { return d->mRRules.isEmpty() ? nullptr : d->mRRules[0]; } void Recurrence::updated() { // recurrenceType() re-calculates the type if it's rMax d->mCachedType = rMax; for (int i = 0, end = d->mObservers.count(); i < end; ++i) { if (d->mObservers[i]) { d->mObservers[i]->recurrenceUpdated(this); } } } bool Recurrence::recurs() const { return !d->mRRules.isEmpty() || !d->mRDates.isEmpty() || !d->mRDateTimes.isEmpty(); } ushort Recurrence::recurrenceType() const { if (d->mCachedType == rMax) { d->mCachedType = recurrenceType(defaultRRuleConst()); } return d->mCachedType; } ushort Recurrence::recurrenceType(const RecurrenceRule *rrule) { if (!rrule) { return rNone; } RecurrenceRule::PeriodType type = rrule->recurrenceType(); // BYSETPOS, BYWEEKNUMBER and BYSECOND were not supported in old versions if (!rrule->bySetPos().isEmpty() || !rrule->bySeconds().isEmpty() || !rrule->byWeekNumbers().isEmpty()) { return rOther; } // It wasn't possible to set BYMINUTES, BYHOUR etc. by the old code. So if // it's set, it's none of the old types if (!rrule->byMinutes().isEmpty() || !rrule->byHours().isEmpty()) { return rOther; } // Possible combinations were: // BYDAY: with WEEKLY, MONTHLY, YEARLY // BYMONTHDAY: with MONTHLY, YEARLY // BYMONTH: with YEARLY // BYYEARDAY: with YEARLY if ((!rrule->byYearDays().isEmpty() && type != RecurrenceRule::rYearly) || (!rrule->byMonths().isEmpty() && type != RecurrenceRule::rYearly)) { return rOther; } if (!rrule->byDays().isEmpty()) { if (type != RecurrenceRule::rYearly && type != RecurrenceRule::rMonthly && type != RecurrenceRule::rWeekly) { return rOther; } } switch (type) { case RecurrenceRule::rNone: return rNone; case RecurrenceRule::rMinutely: return rMinutely; case RecurrenceRule::rHourly: return rHourly; case RecurrenceRule::rDaily: return rDaily; case RecurrenceRule::rWeekly: return rWeekly; case RecurrenceRule::rMonthly: { if (rrule->byDays().isEmpty()) { return rMonthlyDay; } else if (rrule->byMonthDays().isEmpty()) { return rMonthlyPos; } else { return rOther; // both position and date specified } } case RecurrenceRule::rYearly: { // Possible combinations: // rYearlyMonth: [BYMONTH &] BYMONTHDAY // rYearlyDay: BYYEARDAY // rYearlyPos: [BYMONTH &] BYDAY if (!rrule->byDays().isEmpty()) { // can only by rYearlyPos if (rrule->byMonthDays().isEmpty() && rrule->byYearDays().isEmpty()) { return rYearlyPos; } else { return rOther; } } else if (!rrule->byYearDays().isEmpty()) { // Can only be rYearlyDay if (rrule->byMonths().isEmpty() && rrule->byMonthDays().isEmpty()) { return rYearlyDay; } else { return rOther; } } else { return rYearlyMonth; } } default: return rOther; } } bool Recurrence::recursOn(const QDate &qd, const QTimeZone &timeZone) const { // Don't waste time if date is before the start of the recurrence if (QDateTime(qd, QTime(23, 59, 59), timeZone) < d->mStartDateTime) { return false; } // First handle dates. Exrules override if (std::binary_search(d->mExDates.constBegin(), d->mExDates.constEnd(), qd)) { return false; } int i, end; // For all-day events a matching exrule excludes the whole day // since exclusions take precedence over inclusions, we know it can't occur on that day. if (allDay()) { for (i = 0, end = d->mExRules.count(); i < end; ++i) { if (d->mExRules[i]->recursOn(qd, timeZone)) { return false; } } } if (std::binary_search(d->mRDates.constBegin(), d->mRDates.constEnd(), qd)) { return true; } // Check if it might recur today at all. bool recurs = (startDate() == qd); for (i = 0, end = d->mRDateTimes.count(); i < end && !recurs; ++i) { recurs = (d->mRDateTimes[i].toTimeZone(timeZone).date() == qd); } for (i = 0, end = d->mRRules.count(); i < end && !recurs; ++i) { recurs = d->mRRules[i]->recursOn(qd, timeZone); } // If the event wouldn't recur at all, simply return false, don't check ex* if (!recurs) { return false; } // Check if there are any times for this day excluded, either by exdate or exrule: bool exon = false; for (i = 0, end = d->mExDateTimes.count(); i < end && !exon; ++i) { exon = (d->mExDateTimes[i].toTimeZone(timeZone).date() == qd); } if (!allDay()) { // we have already checked all-day times above for (i = 0, end = d->mExRules.count(); i < end && !exon; ++i) { exon = d->mExRules[i]->recursOn(qd, timeZone); } } if (!exon) { // Simple case, nothing on that day excluded, return the value from before return recurs; } else { // Harder part: I don't think there is any way other than to calculate the // whole list of items for that day. //TODO: consider whether it would be more efficient to call // Rule::recurTimesOn() instead of Rule::recursOn() from the start TimeList timesForDay(recurTimesOn(qd, timeZone)); return !timesForDay.isEmpty(); } } bool Recurrence::recursAt(const QDateTime &dt) const { // Convert to recurrence's time zone for date comparisons, and for more efficient time comparisons const auto dtrecur = dt.toTimeZone(d->mStartDateTime.timeZone()); // if it's excluded anyway, don't bother to check if it recurs at all. if (std::binary_search(d->mExDateTimes.constBegin(), d->mExDateTimes.constEnd(), dtrecur) || std::binary_search(d->mExDates.constBegin(), d->mExDates.constEnd(), dtrecur.date())) { return false; } int i, end; for (i = 0, end = d->mExRules.count(); i < end; ++i) { if (d->mExRules[i]->recursAt(dtrecur)) { return false; } } // Check explicit recurrences, then rrules. if (startDateTime() == dtrecur || std::binary_search(d->mRDateTimes.constBegin(), d->mRDateTimes.constEnd(), dtrecur)) { return true; } for (i = 0, end = d->mRRules.count(); i < end; ++i) { if (d->mRRules[i]->recursAt(dtrecur)) { return true; } } return false; } /** Calculates the cumulative end of the whole recurrence (rdates and rrules). If any rrule is infinite, or the recurrence doesn't have any rrules or rdates, an invalid date is returned. */ QDateTime Recurrence::endDateTime() const { QList dts; dts << startDateTime(); if (!d->mRDates.isEmpty()) { dts << QDateTime(d->mRDates.last(), QTime(0, 0, 0), d->mStartDateTime.timeZone()); } if (!d->mRDateTimes.isEmpty()) { dts << d->mRDateTimes.last(); } for (int i = 0, end = d->mRRules.count(); i < end; ++i) { auto rl = d->mRRules[i]->endDt(); // if any of the rules is infinite, the whole recurrence is if (!rl.isValid()) { return QDateTime(); } dts << rl; } sortAndRemoveDuplicates(dts); return dts.isEmpty() ? QDateTime() : dts.last(); } /** Calculates the cumulative end of the whole recurrence (rdates and rrules). If any rrule is infinite, or the recurrence doesn't have any rrules or rdates, an invalid date is returned. */ QDate Recurrence::endDate() const { QDateTime end(endDateTime()); return end.isValid() ? end.date() : QDate(); } void Recurrence::setEndDate(const QDate &date) { QDateTime dt(date, d->mStartDateTime.time(), d->mStartDateTime.timeZone()); if (allDay()) { dt.setTime(QTime(23, 59, 59)); } setEndDateTime(dt); } void Recurrence::setEndDateTime(const QDateTime &dateTime) { if (d->mRecurReadOnly) { return; } RecurrenceRule *rrule = defaultRRule(true); if (!rrule) { return; } // If the recurrence rule has a duration, and we're trying to set an invalid end date, // we have to skip setting it to avoid setting the field dirty. // The end date is already invalid since the duration is set and end date/duration // are mutually exclusive. // We can't use inequality check below, because endDt() also returns a valid date // for a duration (it is calculated from the duration). if (rrule->duration() > 0 && !dateTime.isValid()) { return; } if (dateTime != rrule->endDt()) { rrule->setEndDt(dateTime); updated(); } } int Recurrence::duration() const { RecurrenceRule *rrule = defaultRRuleConst(); return rrule ? rrule->duration() : 0; } int Recurrence::durationTo(const QDateTime &datetime) const { // Emulate old behavior: This is just an interface to the first rule! RecurrenceRule *rrule = defaultRRuleConst(); return rrule ? rrule->durationTo(datetime) : 0; } int Recurrence::durationTo(const QDate &date) const { return durationTo(QDateTime(date, QTime(23, 59, 59), d->mStartDateTime.timeZone())); } void Recurrence::setDuration(int duration) { if (d->mRecurReadOnly) { return; } RecurrenceRule *rrule = defaultRRule(true); if (!rrule) { return; } if (duration != rrule->duration()) { rrule->setDuration(duration); updated(); } } void Recurrence::shiftTimes(const QTimeZone &oldTz, const QTimeZone &newTz) { if (d->mRecurReadOnly) { return; } d->mStartDateTime = d->mStartDateTime.toTimeZone(oldTz); d->mStartDateTime.setTimeZone(newTz); int i, end; for (i = 0, end = d->mRDateTimes.count(); i < end; ++i) { d->mRDateTimes[i] = d->mRDateTimes[i].toTimeZone(oldTz); d->mRDateTimes[i].setTimeZone(newTz); } for (i = 0, end = d->mExDateTimes.count(); i < end; ++i) { d->mExDateTimes[i] = d->mExDateTimes[i].toTimeZone(oldTz); d->mExDateTimes[i].setTimeZone(newTz); } for (i = 0, end = d->mRRules.count(); i < end; ++i) { d->mRRules[i]->shiftTimes(oldTz, newTz); } for (i = 0, end = d->mExRules.count(); i < end; ++i) { d->mExRules[i]->shiftTimes(oldTz, newTz); } } void Recurrence::unsetRecurs() { if (d->mRecurReadOnly) { return; } qDeleteAll(d->mRRules); d->mRRules.clear(); updated(); } void Recurrence::clear() { if (d->mRecurReadOnly) { return; } qDeleteAll(d->mRRules); d->mRRules.clear(); qDeleteAll(d->mExRules); d->mExRules.clear(); d->mRDates.clear(); d->mRDateTimes.clear(); d->mExDates.clear(); d->mExDateTimes.clear(); d->mCachedType = rMax; updated(); } void Recurrence::setRecurReadOnly(bool readOnly) { d->mRecurReadOnly = readOnly; } bool Recurrence::recurReadOnly() const { return d->mRecurReadOnly; } QDate Recurrence::startDate() const { return d->mStartDateTime.date(); } void Recurrence::setStartDateTime(const QDateTime &start, bool isAllDay) { if (d->mRecurReadOnly) { return; } d->mStartDateTime = start; setAllDay(isAllDay); // set all RRULEs and EXRULEs int i, end; for (i = 0, end = d->mRRules.count(); i < end; ++i) { d->mRRules[i]->setStartDt(start); } for (i = 0, end = d->mExRules.count(); i < end; ++i) { d->mExRules[i]->setStartDt(start); } updated(); } int Recurrence::frequency() const { RecurrenceRule *rrule = defaultRRuleConst(); return rrule ? rrule->frequency() : 0; } // Emulate the old behaviour. Make this methods just an interface to the // first rrule void Recurrence::setFrequency(int freq) { if (d->mRecurReadOnly || freq <= 0) { return; } RecurrenceRule *rrule = defaultRRule(true); if (rrule) { rrule->setFrequency(freq); } updated(); } // WEEKLY int Recurrence::weekStart() const { RecurrenceRule *rrule = defaultRRuleConst(); return rrule ? rrule->weekStart() : 1; } // Emulate the old behavior QBitArray Recurrence::days() const { QBitArray days(7); days.fill(0); RecurrenceRule *rrule = defaultRRuleConst(); if (rrule) { const QList &bydays = rrule->byDays(); for (int i = 0; i < bydays.size(); ++i) { if (bydays.at(i).pos() == 0) { days.setBit(bydays.at(i).day() - 1); } } } return days; } // MONTHLY // Emulate the old behavior QList Recurrence::monthDays() const { RecurrenceRule *rrule = defaultRRuleConst(); if (rrule) { return rrule->byMonthDays(); } else { return QList(); } } // Emulate the old behavior QList Recurrence::monthPositions() const { RecurrenceRule *rrule = defaultRRuleConst(); return rrule ? rrule->byDays() : QList(); } // YEARLY QList Recurrence::yearDays() const { RecurrenceRule *rrule = defaultRRuleConst(); return rrule ? rrule->byYearDays() : QList(); } QList Recurrence::yearDates() const { return monthDays(); } QList Recurrence::yearMonths() const { RecurrenceRule *rrule = defaultRRuleConst(); return rrule ? rrule->byMonths() : QList(); } QList Recurrence::yearPositions() const { return monthPositions(); } RecurrenceRule *Recurrence::setNewRecurrenceType(RecurrenceRule::PeriodType type, int freq) { if (d->mRecurReadOnly || freq <= 0) { return nullptr; } // Ignore the call if nothing has change if (defaultRRuleConst() && defaultRRuleConst()->recurrenceType() == type && frequency() == freq) { return nullptr; } qDeleteAll(d->mRRules); d->mRRules.clear(); updated(); RecurrenceRule *rrule = defaultRRule(true); if (!rrule) { return nullptr; } rrule->setRecurrenceType(type); rrule->setFrequency(freq); rrule->setDuration(-1); return rrule; } void Recurrence::setMinutely(int _rFreq) { if (setNewRecurrenceType(RecurrenceRule::rMinutely, _rFreq)) { updated(); } } void Recurrence::setHourly(int _rFreq) { if (setNewRecurrenceType(RecurrenceRule::rHourly, _rFreq)) { updated(); } } void Recurrence::setDaily(int _rFreq) { if (setNewRecurrenceType(RecurrenceRule::rDaily, _rFreq)) { updated(); } } void Recurrence::setWeekly(int freq, int weekStart) { RecurrenceRule *rrule = setNewRecurrenceType(RecurrenceRule::rWeekly, freq); if (!rrule) { return; } rrule->setWeekStart(weekStart); updated(); } void Recurrence::setWeekly(int freq, const QBitArray &days, int weekStart) { setWeekly(freq, weekStart); addMonthlyPos(0, days); } void Recurrence::addWeeklyDays(const QBitArray &days) { addMonthlyPos(0, days); } void Recurrence::setMonthly(int freq) { if (setNewRecurrenceType(RecurrenceRule::rMonthly, freq)) { updated(); } } void Recurrence::addMonthlyPos(short pos, const QBitArray &days) { // Allow 53 for yearly! if (d->mRecurReadOnly || pos > 53 || pos < -53) { return; } RecurrenceRule *rrule = defaultRRule(false); if (!rrule) { return; } bool changed = false; QList positions = rrule->byDays(); for (int i = 0; i < 7; ++i) { if (days.testBit(i)) { RecurrenceRule::WDayPos p(pos, i + 1); if (!positions.contains(p)) { changed = true; positions.append(p); } } } if (changed) { rrule->setByDays(positions); updated(); } } void Recurrence::addMonthlyPos(short pos, ushort day) { // Allow 53 for yearly! if (d->mRecurReadOnly || pos > 53 || pos < -53) { return; } RecurrenceRule *rrule = defaultRRule(false); if (!rrule) { return; } QList positions = rrule->byDays(); RecurrenceRule::WDayPos p(pos, day); if (!positions.contains(p)) { positions.append(p); setMonthlyPos(positions); } } void Recurrence::setMonthlyPos(const QList &monthlyDays) { if (d->mRecurReadOnly) { return; } RecurrenceRule *rrule = defaultRRule(true); if (!rrule) { return; } //TODO: sort lists // the position inside the list has no meaning, so sort the list before testing if it changed if (monthlyDays != rrule->byDays()) { rrule->setByDays(monthlyDays); updated(); } } void Recurrence::addMonthlyDate(short day) { if (d->mRecurReadOnly || day > 31 || day < -31) { return; } RecurrenceRule *rrule = defaultRRule(true); if (!rrule) { return; } QList monthDays = rrule->byMonthDays(); if (!monthDays.contains(day)) { monthDays.append(day); setMonthlyDate(monthDays); } } void Recurrence::setMonthlyDate(const QList< int > &monthlyDays) { if (d->mRecurReadOnly) { return; } RecurrenceRule *rrule = defaultRRule(true); if (!rrule) { return; } QList mD(monthlyDays); QList rbD(rrule->byMonthDays()); sortAndRemoveDuplicates(mD); sortAndRemoveDuplicates(rbD); if (mD != rbD) { rrule->setByMonthDays(monthlyDays); updated(); } } void Recurrence::setYearly(int freq) { if (setNewRecurrenceType(RecurrenceRule::rYearly, freq)) { updated(); } } // Daynumber within year void Recurrence::addYearlyDay(int day) { RecurrenceRule *rrule = defaultRRule(false); // It must already exist! if (!rrule) { return; } QList days = rrule->byYearDays(); if (!days.contains(day)) { days << day; setYearlyDay(days); } } void Recurrence::setYearlyDay(const QList &days) { RecurrenceRule *rrule = defaultRRule(false); // It must already exist! if (!rrule) { return; } QList d(days); QList bYD(rrule->byYearDays()); sortAndRemoveDuplicates(d); sortAndRemoveDuplicates(bYD); if (d != bYD) { rrule->setByYearDays(days); updated(); } } // day part of date within year void Recurrence::addYearlyDate(int day) { addMonthlyDate(day); } void Recurrence::setYearlyDate(const QList &dates) { setMonthlyDate(dates); } // day part of date within year, given as position (n-th weekday) void Recurrence::addYearlyPos(short pos, const QBitArray &days) { addMonthlyPos(pos, days); } void Recurrence::setYearlyPos(const QList &days) { setMonthlyPos(days); } // month part of date within year void Recurrence::addYearlyMonth(short month) { if (d->mRecurReadOnly || month < 1 || month > 12) { return; } RecurrenceRule *rrule = defaultRRule(false); if (!rrule) { return; } QList months = rrule->byMonths(); if (!months.contains(month)) { months << month; setYearlyMonth(months); } } void Recurrence::setYearlyMonth(const QList &months) { if (d->mRecurReadOnly) { return; } RecurrenceRule *rrule = defaultRRule(false); if (!rrule) { return; } QList m(months); QList bM(rrule->byMonths()); sortAndRemoveDuplicates(m); sortAndRemoveDuplicates(bM); if (m != bM) { rrule->setByMonths(months); updated(); } } TimeList Recurrence::recurTimesOn(const QDate &date, const QTimeZone &timeZone) const { // qCDebug(KCALCORE_LOG) << "recurTimesOn(" << date << ")"; int i, end; TimeList times; // The whole day is excepted if (std::binary_search(d->mExDates.constBegin(), d->mExDates.constEnd(), date)) { return times; } // EXRULE takes precedence over RDATE entries, so for all-day events, // a matching excule also excludes the whole day automatically if (allDay()) { for (i = 0, end = d->mExRules.count(); i < end; ++i) { if (d->mExRules[i]->recursOn(date, timeZone)) { return times; } } } QDateTime dt = startDateTime().toTimeZone(timeZone); if (dt.date() == date) { times << dt.time(); } bool foundDate = false; for (i = 0, end = d->mRDateTimes.count(); i < end; ++i) { dt = d->mRDateTimes[i].toTimeZone(timeZone); if (dt.date() == date) { times << dt.time(); foundDate = true; } else if (foundDate) { break; // <= Assume that the rdatetime list is sorted } } for (i = 0, end = d->mRRules.count(); i < end; ++i) { times += d->mRRules[i]->recurTimesOn(date, timeZone); } sortAndRemoveDuplicates(times); foundDate = false; TimeList extimes; for (i = 0, end = d->mExDateTimes.count(); i < end; ++i) { dt = d->mExDateTimes[i].toTimeZone(timeZone); if (dt.date() == date) { extimes << dt.time(); foundDate = true; } else if (foundDate) { break; } } if (!allDay()) { // we have already checked all-day times above for (i = 0, end = d->mExRules.count(); i < end; ++i) { extimes += d->mExRules[i]->recurTimesOn(date, timeZone); } } sortAndRemoveDuplicates(extimes); inplaceSetDifference(times, extimes); return times; } QList Recurrence::timesInInterval(const QDateTime &start, const QDateTime &end) const { int i, count; QList times; for (i = 0, count = d->mRRules.count(); i < count; ++i) { times += d->mRRules[i]->timesInInterval(start, end); } // add rdatetimes that fit in the interval for (i = 0, count = d->mRDateTimes.count(); i < count; ++i) { if (d->mRDateTimes[i] >= start && d->mRDateTimes[i] <= end) { times += d->mRDateTimes[i]; } } // add rdates that fit in the interval QDateTime kdt = d->mStartDateTime; for (i = 0, count = d->mRDates.count(); i < count; ++i) { kdt.setDate(d->mRDates[i]); if (kdt >= start && kdt <= end) { times += kdt; } } // Recurrence::timesInInterval(...) doesn't explicitly add mStartDateTime to the list // of times to be returned. It calls mRRules[i]->timesInInterval(...) which include // mStartDateTime. // So, If we have rdates/rdatetimes but don't have any rrule we must explicitly // add mStartDateTime to the list, otherwise we won't see the first occurrence. if ((!d->mRDates.isEmpty() || !d->mRDateTimes.isEmpty()) && d->mRRules.isEmpty() && start <= d->mStartDateTime && end >= d->mStartDateTime) { times += d->mStartDateTime; } sortAndRemoveDuplicates(times); // Remove excluded times int idt = 0; int enddt = times.count(); for (i = 0, count = d->mExDates.count(); i < count && idt < enddt; ++i) { while (idt < enddt && times[idt].date() < d->mExDates[i]) { ++idt; } while (idt < enddt && times[idt].date() == d->mExDates[i]) { times.removeAt(idt); --enddt; } } QList extimes; for (i = 0, count = d->mExRules.count(); i < count; ++i) { extimes += d->mExRules[i]->timesInInterval(start, end); } extimes += d->mExDateTimes; sortAndRemoveDuplicates(extimes); inplaceSetDifference(times, extimes); return times; } QDateTime Recurrence::getNextDateTime(const QDateTime &preDateTime) const { QDateTime nextDT = preDateTime; // prevent infinite loops, e.g. when an exrule extinguishes an rrule (e.g. // the exrule is identical to the rrule). If an occurrence is found, break // out of the loop by returning that QDateTime // TODO_Recurrence: Is a loop counter of 1000 really okay? I mean for secondly // recurrence, an exdate might exclude more than 1000 intervals! int loop = 0; while (loop < 1000) { // Outline of the algo: // 1) Find the next date/time after preDateTime when the event could recur // 1.0) Add the start date if it's after preDateTime // 1.1) Use the next occurrence from the explicit RDATE lists // 1.2) Add the next recurrence for each of the RRULEs // 2) Take the earliest recurrence of these = QDateTime nextDT // 3) If that date/time is not excluded, either explicitly by an EXDATE or // by an EXRULE, return nextDT as the next date/time of the recurrence // 4) If it's excluded, start all at 1), but starting at nextDT (instead // of preDateTime). Loop at most 1000 times. ++loop; // First, get the next recurrence from the RDate lists QList dates; if (nextDT < startDateTime()) { dates << startDateTime(); } // Assume that the rdatetime list is sorted const auto it = std::upper_bound(d->mRDateTimes.constBegin(), d->mRDateTimes.constEnd(), nextDT); if (it != d->mRDateTimes.constEnd()) { dates << *it; } QDateTime kdt(startDateTime()); for (const auto &date : qAsConst(d->mRDates)) { kdt.setDate(date); if (kdt > nextDT) { dates << kdt; break; } } // Add the next occurrences from all RRULEs. for (const auto &rule : qAsConst(d->mRRules)) { QDateTime dt = rule->getNextDate(nextDT); if (dt.isValid()) { dates << dt; } } // Take the first of these (all others can't be used later on) sortAndRemoveDuplicates(dates); if (dates.isEmpty()) { return QDateTime(); } nextDT = dates.first(); // Check if that date/time is excluded explicitly or by an exrule: if (!std::binary_search(d->mExDates.constBegin(), d->mExDates.constEnd(), nextDT.date()) && !std::binary_search(d->mExDateTimes.constBegin(), d->mExDateTimes.constEnd(), nextDT)) { bool allowed = true; for (const auto &rule : qAsConst(d->mExRules)) { allowed = allowed && !rule->recursAt(nextDT); } if (allowed) { return nextDT; } } } // Couldn't find a valid occurrences in 1000 loops, something is wrong! return QDateTime(); } QDateTime Recurrence::getPreviousDateTime(const QDateTime &afterDateTime) const { QDateTime prevDT = afterDateTime; // prevent infinite loops, e.g. when an exrule extinguishes an rrule (e.g. // the exrule is identical to the rrule). If an occurrence is found, break // out of the loop by returning that QDateTime int loop = 0; while (loop < 1000) { // Outline of the algo: // 1) Find the next date/time after preDateTime when the event could recur // 1.1) Use the next occurrence from the explicit RDATE lists // 1.2) Add the next recurrence for each of the RRULEs // 2) Take the earliest recurrence of these = QDateTime nextDT // 3) If that date/time is not excluded, either explicitly by an EXDATE or // by an EXRULE, return nextDT as the next date/time of the recurrence // 4) If it's excluded, start all at 1), but starting at nextDT (instead // of preDateTime). Loop at most 1000 times. ++loop; // First, get the next recurrence from the RDate lists QList dates; if (prevDT > startDateTime()) { dates << startDateTime(); } const auto it = strictLowerBound(d->mRDateTimes.constBegin(), d->mRDateTimes.constEnd(), prevDT); if (it != d->mRDateTimes.constEnd()) { dates << *it; } QDateTime kdt(startDateTime()); for (const auto &date : qAsConst(d->mRDates)) { kdt.setDate(date); if (kdt < prevDT) { dates << kdt; break; } } // Add the previous occurrences from all RRULEs. for (const auto &rule : qAsConst(d->mRRules)) { QDateTime dt = rule->getPreviousDate(prevDT); if (dt.isValid()) { dates << dt; } } // Take the last of these (all others can't be used later on) sortAndRemoveDuplicates(dates); if (dates.isEmpty()) { return QDateTime(); } prevDT = dates.last(); // Check if that date/time is excluded explicitly or by an exrule: if (!std::binary_search(d->mExDates.constBegin(), d->mExDates.constEnd(), prevDT.date()) && !std::binary_search(d->mExDateTimes.constBegin(), d->mExDateTimes.constEnd(), prevDT)) { bool allowed = true; for (const auto &rule : qAsConst(d->mExRules)) { allowed = allowed && !rule->recursAt(prevDT); } if (allowed) { return prevDT; } } } // Couldn't find a valid occurrences in 1000 loops, something is wrong! return QDateTime(); } /***************************** PROTECTED FUNCTIONS ***************************/ RecurrenceRule::List Recurrence::rRules() const { return d->mRRules; } void Recurrence::addRRule(RecurrenceRule *rrule) { if (d->mRecurReadOnly || !rrule) { return; } rrule->setAllDay(d->mAllDay); d->mRRules.append(rrule); rrule->addObserver(this); updated(); } void Recurrence::removeRRule(RecurrenceRule *rrule) { if (d->mRecurReadOnly) { return; } d->mRRules.removeAll(rrule); rrule->removeObserver(this); updated(); } void Recurrence::deleteRRule(RecurrenceRule *rrule) { if (d->mRecurReadOnly) { return; } d->mRRules.removeAll(rrule); delete rrule; updated(); } RecurrenceRule::List Recurrence::exRules() const { return d->mExRules; } void Recurrence::addExRule(RecurrenceRule *exrule) { if (d->mRecurReadOnly || !exrule) { return; } exrule->setAllDay(d->mAllDay); d->mExRules.append(exrule); exrule->addObserver(this); updated(); } void Recurrence::removeExRule(RecurrenceRule *exrule) { if (d->mRecurReadOnly) { return; } d->mExRules.removeAll(exrule); exrule->removeObserver(this); updated(); } void Recurrence::deleteExRule(RecurrenceRule *exrule) { if (d->mRecurReadOnly) { return; } d->mExRules.removeAll(exrule); delete exrule; updated(); } QList Recurrence::rDateTimes() const { return d->mRDateTimes; } void Recurrence::setRDateTimes(const QList &rdates) { if (d->mRecurReadOnly) { return; } d->mRDateTimes = rdates; sortAndRemoveDuplicates(d->mRDateTimes); updated(); } void Recurrence::addRDateTime(const QDateTime &rdate) { if (d->mRecurReadOnly) { return; } setInsert(d->mRDateTimes, rdate); updated(); } DateList Recurrence::rDates() const { return d->mRDates; } void Recurrence::setRDates(const DateList &rdates) { if (d->mRecurReadOnly) { return; } d->mRDates = rdates; sortAndRemoveDuplicates(d->mRDates); updated(); } void Recurrence::addRDate(const QDate &rdate) { if (d->mRecurReadOnly) { return; } setInsert(d->mRDates, rdate); updated(); } QList Recurrence::exDateTimes() const { return d->mExDateTimes; } void Recurrence::setExDateTimes(const QList &exdates) { if (d->mRecurReadOnly) { return; } d->mExDateTimes = exdates; sortAndRemoveDuplicates(d->mExDateTimes); } void Recurrence::addExDateTime(const QDateTime &exdate) { if (d->mRecurReadOnly) { return; } setInsert(d->mExDateTimes, exdate); updated(); } DateList Recurrence::exDates() const { return d->mExDates; } void Recurrence::setExDates(const DateList &exdates) { if (d->mRecurReadOnly) { return; } DateList l = exdates; sortAndRemoveDuplicates(l); if (d->mExDates != l) { d->mExDates = l; updated(); } } void Recurrence::addExDate(const QDate &exdate) { if (d->mRecurReadOnly) { return; } setInsert(d->mExDates, exdate); updated(); } void Recurrence::recurrenceChanged(RecurrenceRule *) { updated(); } // %%%%%%%%%%%%%%%%%% end:Recurrencerule %%%%%%%%%%%%%%%%%% void Recurrence::dump() const { int i; int count = d->mRRules.count(); qCDebug(KCALCORE_LOG) << " -)" << count << "RRULEs:"; for (i = 0; i < count; ++i) { qCDebug(KCALCORE_LOG) << " -) RecurrenceRule: "; d->mRRules[i]->dump(); } count = d->mExRules.count(); qCDebug(KCALCORE_LOG) << " -)" << count << "EXRULEs:"; for (i = 0; i < count; ++i) { qCDebug(KCALCORE_LOG) << " -) ExceptionRule :"; d->mExRules[i]->dump(); } count = d->mRDates.count(); qCDebug(KCALCORE_LOG) << " -)" << count << "Recurrence Dates:"; for (i = 0; i < count; ++i) { qCDebug(KCALCORE_LOG) << " " << d->mRDates[i]; } count = d->mRDateTimes.count(); qCDebug(KCALCORE_LOG) << " -)" << count << "Recurrence Date/Times:"; for (i = 0; i < count; ++i) { qCDebug(KCALCORE_LOG) << " " << d->mRDateTimes[i]; } count = d->mExDates.count(); qCDebug(KCALCORE_LOG) << " -)" << count << "Exceptions Dates:"; for (i = 0; i < count; ++i) { qCDebug(KCALCORE_LOG) << " " << d->mExDates[i]; } count = d->mExDateTimes.count(); qCDebug(KCALCORE_LOG) << " -)" << count << "Exception Date/Times:"; for (i = 0; i < count; ++i) { qCDebug(KCALCORE_LOG) << " " << d->mExDateTimes[i]; } } Recurrence::RecurrenceObserver::~RecurrenceObserver() { } KCALENDARCORE_EXPORT QDataStream &KCalendarCore::operator<<(QDataStream &out, KCalendarCore::Recurrence *r) { if (!r) { return out; } serializeQDateTimeList(out, r->d->mRDateTimes); serializeQDateTimeList(out, r->d->mExDateTimes); out << r->d->mRDates; serializeQDateTimeAsKDateTime(out, r->d->mStartDateTime); out << r->d->mCachedType << r->d->mAllDay << r->d->mRecurReadOnly << r->d->mExDates << r->d->mExRules.count() << r->d->mRRules.count(); for (RecurrenceRule *rule : qAsConst(r->d->mExRules)) { out << rule; } for (RecurrenceRule *rule : qAsConst(r->d->mRRules)) { out << rule; } return out; } KCALENDARCORE_EXPORT QDataStream &KCalendarCore::operator>>(QDataStream &in, KCalendarCore::Recurrence *r) { if (!r) { return in; } int rruleCount, exruleCount; deserializeQDateTimeList(in, r->d->mRDateTimes); deserializeQDateTimeList(in, r->d->mExDateTimes); in >> r->d->mRDates; deserializeKDateTimeAsQDateTime(in, r->d->mStartDateTime); in >> r->d->mCachedType >> r->d->mAllDay >> r->d->mRecurReadOnly >> r->d->mExDates >> exruleCount >> rruleCount; r->d->mExRules.clear(); r->d->mRRules.clear(); for (int i = 0; i < exruleCount; ++i) { RecurrenceRule *rule = new RecurrenceRule(); rule->addObserver(r); in >> rule; r->d->mExRules.append(rule); } for (int i = 0; i < rruleCount; ++i) { RecurrenceRule *rule = new RecurrenceRule(); rule->addObserver(r); in >> rule; r->d->mRRules.append(rule); } return in; }