Skip to content
This repository has been archived by the owner on Nov 11, 2021. It is now read-only.

Commit

Permalink
[kcalcore] Properly handle exception datetimes for ClockTime events. …
Browse files Browse the repository at this point in the history
…Contributes to JB#43708

If a recurring event is specified as a ClockTime event, or if an
exception to the event is specified in ClockTime specification,
we need to apply the exception semantics specially.

That is: we should not perform a toLocalZone() conversion before
checking if the exception datetime matches, but just directly
compare the dates and times as-is.
  • Loading branch information
Chris Adams committed Nov 14, 2019
1 parent f8153fe commit 281ddaf
Show file tree
Hide file tree
Showing 7 changed files with 173 additions and 7 deletions.
20 changes: 20 additions & 0 deletions kcalcore/kdedate/KDateTime
Expand Up @@ -844,6 +844,26 @@ class KDECORE_EXPORT KDateTime //krazy:exclude=dpointer (implicitly shared)
*/
KDateTime toTimeSpec(const KDateTime &dt) const;

/**
* Returns the time in the specified time specification.
*
* If the instance is in clock time, no conversion to
* local time is performed, but instead the given
* time spec is directly applied to the time as-is.
*
* If the spec is clock time, no conversion to
* local time is performed, but instead the instance's
* time will be returned directly with clock time spec.
*
* If neither the instance nor the spec are clock time,
* then this function will return a new time which has
* been converted as via toTimeSpec().
*
* @param spec new time specification
* @return converted time
*/
KDateTime asTimeSpec(const Spec &spec) const;

/**
* Converts the time to a UTC time, measured in seconds since 00:00:00 UTC
* 1st January 1970 (as returned by time(2)).
Expand Down
11 changes: 11 additions & 0 deletions kcalcore/kdedate/kdatetime.cpp
Expand Up @@ -1020,6 +1020,17 @@ KDateTime KDateTime::toTimeSpec(const Spec &spec) const
return KDateTime(d->toUtc(), spec);
}

KDateTime KDateTime::asTimeSpec(const Spec &spec) const
{
if (spec.type() == KDateTime::ClockTime) {
return KDateTime(date(), time(), KDateTime::ClockTime);
} else if (isClockTime()) {
return KDateTime(date(), time(), spec);
} else {
return toTimeSpec(spec);
}
}

uint KDateTime::toTime_t() const
{
QDateTime qdt = d->toUtc();
Expand Down
20 changes: 20 additions & 0 deletions kcalcore/kdedate/kdatetime.h
Expand Up @@ -844,6 +844,26 @@ class KDECORE_EXPORT KDateTime //krazy:exclude=dpointer (implicitly shared)
*/
KDateTime toTimeSpec(const KDateTime &dt) const;

/**
* Returns the time in the specified time specification.
*
* If the instance is in clock time, no conversion to
* local time is performed, but instead the given
* time spec is directly applied to the time as-is.
*
* If the spec is clock time, no conversion to
* local time is performed, but instead the instance's
* time will be returned directly with clock time spec.
*
* If neither the instance nor the spec are clock time,
* then this function will return a new time which has
* been converted as via toTimeSpec().
*
* @param spec new time specification
* @return converted time
*/
KDateTime asTimeSpec(const Spec &spec) const;

/**
* Converts the time to a UTC time, measured in seconds since 00:00:00 UTC
* 1st January 1970 (as returned by time(2)).
Expand Down
25 changes: 20 additions & 5 deletions kcalcore/recurrence.cpp
Expand Up @@ -1005,11 +1005,26 @@ DateTimeList Recurrence::timesInInterval( const KDateTime &start, const KDateTim
extimes += d->mExDateTimes;
extimes.sortUnique();

int st = 0;
for ( i = 0, count = extimes.count(); i < count; ++i ) {
int j = times.removeSorted( extimes[i], st );
if ( j >= 0 ) {
st = j;
bool remove = false;
for ( DateTimeList::iterator it = times.begin(); it != times.end(); ) {
remove = false;
for ( DateTimeList::iterator exIt = extimes.begin(); exIt != extimes.end(); ++exIt) {
const KDateTime occurrenceInExpansionTz = KDateTime(it->date(), it->time(), start.timeSpec());
const KDateTime occurrenceConvertedToExceptionTz = occurrenceInExpansionTz.toTimeSpec(exIt->timeSpec());
if ((!exIt->isClockTime() && !it->isClockTime() && (*exIt == *it || it->toTimeSpec(exIt->timeSpec()) == *exIt))
|| (exIt->isClockTime() && exIt->date() == it->date() && exIt->time() == it->time())
|| (!exIt->isClockTime() && it->isClockTime() // KDateTime operator==() is broken, so do the expansion and comparison manually...
&& ((start.timeSpec() == exIt->timeSpec() && occurrenceInExpansionTz.date() == exIt->date() && occurrenceInExpansionTz.time() == exIt->time())
|| (occurrenceConvertedToExceptionTz.date() == exIt->date() && occurrenceConvertedToExceptionTz.time() == exIt->time())))) {
exIt = extimes.erase(exIt);
remove = true;
break;
}
}
if (remove) {
it = times.erase(it);
} else {
++it;
}
}

Expand Down
4 changes: 2 additions & 2 deletions kcalcore/recurrencerule.cpp
Expand Up @@ -1740,8 +1740,8 @@ KDateTime RecurrenceRule::getNextDate( const KDateTime &preDate ) const
DateTimeList RecurrenceRule::timesInInterval( const KDateTime &dtStart,
const KDateTime &dtEnd ) const
{
KDateTime start = dtStart.toTimeSpec( d->mDateStart.timeSpec() );
KDateTime end = dtEnd.toTimeSpec( d->mDateStart.timeSpec() );
KDateTime start = dtStart.asTimeSpec( d->mDateStart.timeSpec() );
KDateTime end = dtEnd.asTimeSpec( d->mDateStart.timeSpec() );
DateTimeList result;
if ( end < d->mDateStart ) {
return result; // before start of recurrence
Expand Down
98 changes: 98 additions & 0 deletions kcalcore/tests/testtimesininterval.cpp
Expand Up @@ -237,3 +237,101 @@ void TimesInIntervalTest::testWeeklyDayOfWeekRecurrenceDtStart()

QCOMPARE(expectedEventOccurrences.size(), 0);
}

void TimesInIntervalTest::testClockTimeHandlingAllDay()
{
// Create an event which occurs every weekday of every week,
// starting from Friday the 11th of October, and lasts for two weeks,
// with three exception datetimes (only two of which will apply).
KDateTime::Spec expansionSpec(KSystemTimeZones::zone(QStringLiteral("Europe/Helsinki")));
KDateTime::Spec exceptionSpec(KSystemTimeZones::zone(QStringLiteral("America/Toronto")));
KCalCore::Event::Ptr event = KCalCore::Event::Ptr(new KCalCore::Event());
event->startUpdates();
event->setUid("testClockTimeHandlingAllDay");
event->setLocation(QStringLiteral("Test location"));
event->setDescription(QStringLiteral("Test description"));
event->setAllDay(true);
event->setDtStart(KDateTime(QDate(2019, 10, 11), KDateTime::ClockTime));
event->setSummary(QStringLiteral("Test event summary"));
event->setCategories(QStringList() << QStringLiteral("Category One"));

RecurrenceRule * const rule = new RecurrenceRule();
rule->setRecurrenceType(RecurrenceRule::rDaily);
rule->setStartDt(event->dtStart());
rule->setFrequency(1);
rule->setDuration(14);
rule->setByDays(QList<RecurrenceRule::WDayPos>() << 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

event->recurrence()->addRRule(rule);
event->recurrence()->addExDateTime(KDateTime(QDate(2019, 10, 15), KDateTime::ClockTime));
event->recurrence()->addExDateTime(KDateTime(QDate(2019, 10, 17), exceptionSpec)); // this one will not apply.
event->recurrence()->addExDateTime(KDateTime(QDate(2019, 10, 24), expansionSpec)); // this one will apply.
event->endUpdates();

// Expand the events and within a wide interval
const DateTimeList timesInInterval = event->recurrence()->timesInInterval(
KDateTime(QDate(2019, 10, 05), QTime(0, 0), expansionSpec),
KDateTime(QDate(2019, 10, 25), QTime(23, 59), expansionSpec));

// ensure that the expansion does not include weekend days,
// nor either of the exception date times.
const QList<int> expectedDays { 11, 14, 16, 17, 18, 21, 22, 23, 25 };
for (int day : expectedDays) {
QVERIFY(timesInInterval.contains(KDateTime(QDate(2019, 10, day), QTime(0, 0), KDateTime::ClockTime)));
}
QCOMPARE(timesInInterval.size(), expectedDays.size());
}

void TimesInIntervalTest::testClockTimeHandlingNonAllDay()
{
// 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).
KDateTime::Spec expansionSpec(KSystemTimeZones::zone(QStringLiteral("Europe/Helsinki")));
KDateTime::Spec exceptionSpec(KSystemTimeZones::zone(QStringLiteral("America/Toronto")));
KCalCore::Event::Ptr event = KCalCore::Event::Ptr(new KCalCore::Event());
event->startUpdates();
event->setUid("testClockTimeHandlingNonAllDay");
event->setLocation(QStringLiteral("Test location"));
event->setDescription(QStringLiteral("Test description"));
event->setAllDay(false);
event->setDtStart(KDateTime(QDate(2019, 10, 11), QTime(12, 0), KDateTime::ClockTime));
event->setDtEnd(KDateTime(QDate(2019, 10, 11), QTime(13, 0), KDateTime::ClockTime));
event->setSummary(QStringLiteral("Test event summary"));
event->setCategories(QStringList() << QStringLiteral("Category One"));

RecurrenceRule * const rule = new RecurrenceRule();
rule->setRecurrenceType(RecurrenceRule::rDaily);
rule->setStartDt(event->dtStart());
rule->setFrequency(1);
rule->setDuration(14);
rule->setByDays(QList<RecurrenceRule::WDayPos>() << 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

event->recurrence()->addRRule(rule);
event->recurrence()->addExDateTime(KDateTime(QDate(2019, 10, 15), QTime(12, 0), KDateTime::ClockTime));
event->recurrence()->addExDateTime(KDateTime(QDate(2019, 10, 17), QTime(12, 0), exceptionSpec)); // this one will not apply.
event->recurrence()->addExDateTime(KDateTime(QDate(2019, 10, 24), QTime(05, 0), exceptionSpec)); // this one will apply.
event->endUpdates();

// Expand the events and within a wide interval
const DateTimeList timesInInterval = event->recurrence()->timesInInterval(
KDateTime(QDate(2019, 10, 05), QTime(0, 0), expansionSpec),
KDateTime(QDate(2019, 10, 25), QTime(23, 59), expansionSpec));

// ensure that the expansion does not include weekend days,
// nor either of the exception date times.
const QList<int> expectedDays { 11, 14, 16, 17, 18, 21, 22, 23, 25 };
for (int day : expectedDays) {
QVERIFY(timesInInterval.contains(KDateTime(QDate(2019, 10, day), QTime(12, 0), KDateTime::ClockTime)));
}
QCOMPARE(timesInInterval.size(), expectedDays.size());
}
2 changes: 2 additions & 0 deletions kcalcore/tests/testtimesininterval.h
Expand Up @@ -35,6 +35,8 @@ class TimesInIntervalTest : public QObject
void testSubDailyRecurrenceIntervalLimits();
void testDailyRecurrenceDtStart();
void testWeeklyDayOfWeekRecurrenceDtStart();
void testClockTimeHandlingAllDay();
void testClockTimeHandlingNonAllDay();
};

#endif

0 comments on commit 281ddaf

Please sign in to comment.