diff --git a/kcalcore/event.cpp b/kcalcore/event.cpp index 8056b13cd..8f8b81a9a 100644 --- a/kcalcore/event.cpp +++ b/kcalcore/event.cpp @@ -1,376 +1,378 @@ /* This file is part of the kcalcore library. Copyright (c) 2001 Cornelius Schumacher This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ /** @file This file is part of the API for handling calendar data and defines the Event class. @brief This class provides an Event in the sense of RFC2445. @author Cornelius Schumacher \ */ #include "event.h" #include "visitor.h" #include #include using namespace KCalCore; /** Private class that helps to provide binary compatibility between releases. @internal */ //@cond PRIVATE class KCalCore::Event::Private { public: Private() : mHasEndDate(false), mTransparency(Opaque), mMultiDayValid(false), mMultiDay(false) {} Private(const KCalCore::Event::Private &other) : mDtEnd(other.mDtEnd), mHasEndDate(other.mHasEndDate), mTransparency(other.mTransparency), mMultiDayValid(false), mMultiDay(false) {} KDateTime mDtEnd; bool mHasEndDate; Transparency mTransparency; bool mMultiDayValid; bool mMultiDay; }; //@endcond Event::Event() : d(new KCalCore::Event::Private) { } Event::Event(const Event &other) : Incidence(other), d(new KCalCore::Event::Private(*other.d)) { } Event::Event(const Incidence &other) : Incidence(other) , d(new KCalCore::Event::Private) { } Event::~Event() { delete d; } Event *Event::clone() const { return new Event(*this); } IncidenceBase &Event::assign(const IncidenceBase &other) { if (&other != this) { Incidence::assign(other); const Event *e = static_cast(&other); *d = *(e->d); } return *this; } bool Event::equals(const IncidenceBase &event) const { if (!Incidence::equals(event)) { return false; } else { // If they weren't the same type IncidenceBase::equals would had returned false already const Event *e = static_cast(&event); return ((dtEnd() == e->dtEnd()) || (!dtEnd().isValid() && !e->dtEnd().isValid())) && hasEndDate() == e->hasEndDate() && transparency() == e->transparency(); } } Incidence::IncidenceType Event::type() const { return TypeEvent; } QByteArray Event::typeStr() const { return "Event"; } void Event::setDtStart(const KDateTime &dt) { d->mMultiDayValid = false; Incidence::setDtStart(dt); } void Event::setDtEnd(const KDateTime &dtEnd) { if (mReadOnly) { return; } - update(); - d->mDtEnd = dtEnd; - d->mMultiDayValid = false; - d->mHasEndDate = dtEnd.isValid(); - if (d->mHasEndDate) { - setHasDuration(false); + if (d->mDtEnd != dtEnd || d->mHasEndDate != dtEnd.isValid()) { + update(); + d->mDtEnd = dtEnd; + d->mMultiDayValid = false; + d->mHasEndDate = dtEnd.isValid(); + if (d->mHasEndDate) { + setHasDuration(false); + } + setFieldDirty(FieldDtEnd); + updated(); } - setFieldDirty(FieldDtEnd); - updated(); } KDateTime Event::dtEnd() const { if (hasEndDate()) { return d->mDtEnd; } if (hasDuration()) { if (allDay()) { // For all day events, dtEnd is always inclusive KDateTime end = duration().end(dtStart()).addDays(-1); return end >= dtStart() ? end : dtStart(); } else { return duration().end(dtStart()); } } // It is valid for a VEVENT to be without a DTEND. See RFC2445, Sect4.6.1. // Be careful to use Event::dateEnd() as appropriate due to this possibility. return dtStart(); } QDate Event::dateEnd() const { KDateTime end = dtEnd().toTimeSpec(dtStart()); if (allDay()) { return end.date(); } else { return end.addSecs(-1).date(); } } void Event::setHasEndDate(bool b) { d->mHasEndDate = b; setFieldDirty(FieldDtEnd); } bool Event::hasEndDate() const { return d->mHasEndDate; } bool Event::isMultiDay(const KDateTime::Spec &spec) const { // First off, if spec's not valid, we can check for cache if (!spec.isValid() && d->mMultiDayValid) { return d->mMultiDay; } // Not in cache -> do it the hard way KDateTime start, end; if (!spec.isValid()) { start = dtStart(); end = dtEnd(); } else { start = dtStart().toTimeSpec(spec); end = dtEnd().toTimeSpec(spec); } bool multi = (start < end && start.date() != end.date()); // End date is non inclusive // If we have an incidence that duration is one day and ends with a start of a new day // than it is not a multiday event if (multi && end.time() != QTime(0,0,0)) { multi = start.daysTo(end) > 1; } // Update the cache // Also update Cache if spec is invalid /*if (spec.isValid()) { d->mMultiDayValid = true; d->mMultiDay = multi; } */ d->mMultiDayValid = true; d->mMultiDay = multi; return multi; } void Event::shiftTimes(const KDateTime::Spec &oldSpec, const KDateTime::Spec &newSpec) { Incidence::shiftTimes(oldSpec, newSpec); if (hasEndDate()) { d->mDtEnd = d->mDtEnd.toTimeSpec(oldSpec); d->mDtEnd.setTimeSpec(newSpec); } } void Event::setTransparency(Event::Transparency transparency) { if (mReadOnly) { return; } update(); d->mTransparency = transparency; setFieldDirty(FieldTransparency); updated(); } Event::Transparency Event::transparency() const { return d->mTransparency; } void Event::setDuration(const Duration &duration) { setDtEnd(KDateTime()); Incidence::setDuration(duration); } void Event::setAllDay(bool allday) { if (allday != allDay() && !mReadOnly) { setFieldDirty(FieldDtEnd); Incidence::setAllDay(allday); } } bool Event::accept(Visitor &v, IncidenceBase::Ptr incidence) { return v.visit(incidence.staticCast()); } KDateTime Event::dateTime(DateTimeRole role) const { switch (role) { case RoleRecurrenceStart: case RoleAlarmStartOffset: case RoleStartTimeZone: case RoleSort: return dtStart(); case RoleCalendarHashing: return !recurs() && !isMultiDay() ? dtStart() : KDateTime(); case RoleAlarmEndOffset: case RoleEndTimeZone: case RoleEndRecurrenceBase: case RoleEnd: case RoleDisplayEnd: return dtEnd(); case RoleDisplayStart: return dtStart(); case RoleAlarm: if (alarms().isEmpty()) { return KDateTime(); } else { Alarm::Ptr alarm = alarms().first(); return alarm->hasStartOffset() ? dtStart() : dtEnd(); } break; default: return KDateTime(); } } void Event::setDateTime(const KDateTime &dateTime, DateTimeRole role) { switch (role) { case RoleDnD: { const int duration = dtStart().secsTo(dtEnd()); setDtStart(dateTime); setDtEnd(dateTime.addSecs(duration <= 0 ? 3600 : duration)); break; } case RoleEnd: setDtEnd(dateTime); break; default: kDebug() << "Unhandled role" << role; } } void Event::virtual_hook(int id, void *data) { switch (static_cast(id)) { case IncidenceBase::SerializerHook: serialize(*reinterpret_cast(data)); break; case IncidenceBase::DeserializerHook: deserialize(*reinterpret_cast(data)); break; default: Q_ASSERT(false); } } QLatin1String KCalCore::Event::mimeType() const { return Event::eventMimeType(); } QLatin1String Event::eventMimeType() { return QLatin1String("application/x-vnd.akonadi.calendar.event"); } QLatin1String Event::iconName(const KDateTime &) const { return QLatin1String("view-calendar-day"); } void Event::serialize(QDataStream &out) { Incidence::serialize(out); out << d->mDtEnd << d->mHasEndDate << static_cast(d->mTransparency) << d->mMultiDayValid << d->mMultiDay; } void Event::deserialize(QDataStream &in) { Incidence::deserialize(in); in >> d->mDtEnd >> d->mHasEndDate; quint32 transp; in >> transp; d->mTransparency = static_cast(transp); in >> d->mMultiDayValid >> d->mMultiDay; } diff --git a/kcalcore/incidence.cpp b/kcalcore/incidence.cpp index 4ec7deca5..412a3c531 100644 --- a/kcalcore/incidence.cpp +++ b/kcalcore/incidence.cpp @@ -1,1189 +1,1193 @@ /* This file is part of the kcalcore library. Copyright (c) 2001 Cornelius Schumacher Copyright (C) 2003-2004 Reinhold Kainhofer This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ /** @file This file is part of the API for handling calendar data and defines the Incidence class. @brief Provides the class common to non-FreeBusy (Events, To-dos, Journals) calendar components known as incidences. @author Cornelius Schumacher \ @author Reinhold Kainhofer \ */ #include "incidence.h" #include "calformat.h" #include #include #include #include // for Qt::escape() and Qt::mightBeRichText() #include #include using namespace KCalCore; /** Private class that helps to provide binary compatibility between releases. @internal */ //@cond PRIVATE class KCalCore::Incidence::Private { public: Private() : mRevision(0), mDescriptionIsRich(false), mSummaryIsRich(false), mLocationIsRich(false), mRecurrence(0), mStatus(StatusNone), mSecrecy(SecrecyPublic), mPriority(0), mGeoLatitude(INVALID_LATLON), mGeoLongitude(INVALID_LATLON), mHasGeo(false), mThisAndFuture(false), mLocalOnly(false) { } Private(const Private &p) : mCreated(p.mCreated), mRevision(p.mRevision), mDescription(p.mDescription), mDescriptionIsRich(p.mDescriptionIsRich), mSummary(p.mSummary), mSummaryIsRich(p.mSummaryIsRich), mLocation(p.mLocation), mLocationIsRich(p.mLocationIsRich), mCategories(p.mCategories), mRecurrence(0), mResources(p.mResources), mStatus(p.mStatus), mStatusString(p.mStatusString), mSecrecy(p.mSecrecy), mPriority(p.mPriority), mSchedulingID(p.mSchedulingID), mRelatedToUid(p.mRelatedToUid), mGeoLatitude(p.mGeoLatitude), mGeoLongitude(p.mGeoLongitude), mHasGeo(p.mHasGeo), mRecurrenceId(p.mRecurrenceId), mThisAndFuture(p.mThisAndFuture), mLocalOnly(false) { } void clear() { mAlarms.clear(); mAttachments.clear(); delete mRecurrence; mRecurrence = 0; } void init(Incidence *dest, const Incidence &src) { mRevision = src.d->mRevision; mCreated = src.d->mCreated; mDescription = src.d->mDescription; mSummary = src.d->mSummary; mCategories = src.d->mCategories; mRelatedToUid = src.d->mRelatedToUid; mResources = src.d->mResources; mStatusString = src.d->mStatusString; mStatus = src.d->mStatus; mSecrecy = src.d->mSecrecy; mPriority = src.d->mPriority; mLocation = src.d->mLocation; mGeoLatitude = src.d->mGeoLatitude; mGeoLongitude = src.d->mGeoLongitude; mHasGeo = src.d->mHasGeo; mRecurrenceId = src.d->mRecurrenceId; mThisAndFuture = src.d->mThisAndFuture; mLocalOnly = src.d->mLocalOnly; // Alarms and Attachments are stored in ListBase<...>, which is a QValueList<...*>. // We need to really duplicate the objects stored therein, otherwise deleting // i will also delete all attachments from this object (setAutoDelete...) foreach(Alarm::Ptr alarm, src.d->mAlarms) { Alarm::Ptr b(new Alarm(*alarm.data())); b->setParent(dest); mAlarms.append(b); } foreach(Attachment::Ptr attachment, src.d->mAttachments) { Attachment::Ptr a(new Attachment(*attachment)); mAttachments.append(a); } if (src.d->mRecurrence) { mRecurrence = new Recurrence(*(src.d->mRecurrence)); mRecurrence->addObserver(dest); } else { mRecurrence = 0; } } KDateTime mCreated; // creation datetime int mRevision; // revision number QString mDescription; // description string bool mDescriptionIsRich; // description string is richtext. QString mSummary; // summary string bool mSummaryIsRich; // summary string is richtext. QString mLocation; // location string bool mLocationIsRich; // location string is richtext. QStringList mCategories; // category list mutable Recurrence *mRecurrence; // recurrence Attachment::List mAttachments; // attachments list Alarm::List mAlarms; // alarms list QStringList mResources; // resources list (not calendar resources) Status mStatus; // status QString mStatusString; // status string, for custom status Secrecy mSecrecy; // secrecy int mPriority; // priority: 1 = highest, 2 = less, etc. QString mSchedulingID; // ID for scheduling mails QMap mRelatedToUid;// incidence uid this is related to, for each relType float mGeoLatitude; // Specifies latitude in decimal degrees float mGeoLongitude; // Specifies longitude in decimal degrees bool mHasGeo; // if incidence has geo data QHash mTempFiles; // Temporary files for writing attachments to. KDateTime mRecurrenceId; // recurrenceId bool mThisAndFuture; bool mLocalOnly; // allow changes that won't go to the server }; //@endcond Incidence::Incidence() : IncidenceBase(), d(new KCalCore::Incidence::Private) { recreate(); resetDirtyFields(); } Incidence::Incidence(const Incidence &i) : IncidenceBase(i), Recurrence::RecurrenceObserver(), d(new KCalCore::Incidence::Private(*i.d)) { d->init(this, i); resetDirtyFields(); } Incidence::~Incidence() { // Alarm has a raw incidence pointer, so we must set it to 0 // so Alarm doesn't use it after Incidence is destroyed foreach(Alarm::Ptr alarm, d->mAlarms) { alarm->setParent(0); } delete d->mRecurrence; delete d; } //@cond PRIVATE // A string comparison that considers that null and empty are the same static bool stringCompare(const QString &s1, const QString &s2) { return (s1.isEmpty() && s2.isEmpty()) || (s1 == s2); } //@endcond IncidenceBase &Incidence::assign(const IncidenceBase &other) { if (&other != this) { d->clear(); //TODO: should relations be cleared out, as in destructor??? IncidenceBase::assign(other); const Incidence *i = static_cast(&other); d->init(this, *i); } return *this; } bool Incidence::equals(const IncidenceBase &incidence) const { if (!IncidenceBase::equals(incidence)) { return false; } // If they weren't the same type IncidenceBase::equals would had returned false already const Incidence *i2 = static_cast(&incidence); if (alarms().count() != i2->alarms().count()) { return false; } Alarm::List::ConstIterator a1 = alarms().constBegin(); Alarm::List::ConstIterator a1end = alarms().constEnd(); Alarm::List::ConstIterator a2 = i2->alarms().constBegin(); Alarm::List::ConstIterator a2end = i2->alarms().constEnd(); for (; a1 != a1end && a2 != a2end; ++a1, ++a2) { if (**a1 == **a2) { continue; } else { return false; } } if (attachments().count() != i2->attachments().count()) { return false; } Attachment::List::ConstIterator att1 = attachments().constBegin(); const Attachment::List::ConstIterator att1end = attachments().constEnd(); Attachment::List::ConstIterator att2 = i2->attachments().constBegin(); const Attachment::List::ConstIterator att2end = i2->attachments().constEnd(); for (; att1 != att1end && att2 != att2end; ++att1, ++att2) { if (**att1 == **att2) { continue; } else { return false; } } bool recurrenceEqual = (d->mRecurrence == 0 && i2->d->mRecurrence == 0); if (!recurrenceEqual) { recurrence(); // create if doesn't exist i2->recurrence(); // create if doesn't exist recurrenceEqual = d->mRecurrence != 0 && i2->d->mRecurrence != 0 && *d->mRecurrence == *i2->d->mRecurrence; } return recurrenceEqual && created() == i2->created() && stringCompare(description(), i2->description()) && stringCompare(summary(), i2->summary()) && categories() == i2->categories() && stringCompare(relatedTo(), i2->relatedTo()) && resources() == i2->resources() && d->mStatus == i2->d->mStatus && (d->mStatus == StatusNone || stringCompare(d->mStatusString, i2->d->mStatusString)) && secrecy() == i2->secrecy() && priority() == i2->priority() && stringCompare(location(), i2->location()) && stringCompare(schedulingID(), i2->schedulingID()) && recurrenceId() == i2->recurrenceId() && thisAndFuture() == i2->thisAndFuture(); } QString Incidence::instanceIdentifier() const { if (hasRecurrenceId()) { return uid() + recurrenceId().toString(); } return uid(); } void Incidence::recreate() { const KDateTime nowUTC = KDateTime::currentUtcDateTime(); setCreated(nowUTC); setSchedulingID(QString(), CalFormat::createUniqueId()); setRevision(0); setLastModified(nowUTC); } void Incidence::setLastModified(const KDateTime &lm) { if (!d->mLocalOnly) { IncidenceBase::setLastModified(lm); } } void Incidence::setReadOnly(bool readOnly) { IncidenceBase::setReadOnly(readOnly); if (d->mRecurrence) { d->mRecurrence->setRecurReadOnly(readOnly); } } void Incidence::setLocalOnly(bool localOnly) { if (mReadOnly) { return; } d->mLocalOnly = localOnly; } bool Incidence::localOnly() const { return d->mLocalOnly; } void Incidence::setAllDay(bool allDay) { if (mReadOnly) { return; } if (d->mRecurrence) { d->mRecurrence->setAllDay(allDay); } IncidenceBase::setAllDay(allDay); } void Incidence::setCreated(const KDateTime &created) { if (mReadOnly || d->mLocalOnly) { return; } d->mCreated = created.toUtc(); setFieldDirty(FieldCreated); // FIXME: Shouldn't we call updated for the creation date, too? // updated(); } KDateTime Incidence::created() const { return d->mCreated; } void Incidence::setRevision(int rev) { if (mReadOnly || d->mLocalOnly) { return; } update(); d->mRevision = rev; setFieldDirty(FieldRevision); updated(); } int Incidence::revision() const { return d->mRevision; } void Incidence::setDtStart(const KDateTime &dt) { - if (d->mRecurrence) { + IncidenceBase::setDtStart(dt); + if ((dirtyFields().contains(FieldDtStart)) && d->mRecurrence) { d->mRecurrence->setStartDateTime(dt); } - IncidenceBase::setDtStart(dt); } void Incidence::shiftTimes(const KDateTime::Spec &oldSpec, const KDateTime::Spec &newSpec) { IncidenceBase::shiftTimes(oldSpec, newSpec); if (d->mRecurrence) { d->mRecurrence->shiftTimes(oldSpec, newSpec); } for (int i = 0, end = d->mAlarms.count(); i < end; ++i) { d->mAlarms[i]->shiftTimes(oldSpec, newSpec); } } void Incidence::setDescription(const QString &description, bool isRich) { if (mReadOnly) { return; } update(); d->mDescription = description; d->mDescriptionIsRich = isRich; setFieldDirty(FieldDescription); updated(); } void Incidence::setDescription(const QString &description) { setDescription(description, Qt::mightBeRichText(description)); } QString Incidence::description() const { return d->mDescription; } QString Incidence::richDescription() const { if (descriptionIsRich()) { return d->mDescription; } else { return Qt::escape(d->mDescription).replace(QLatin1Char('\n'), QLatin1String("
")); } } bool Incidence::descriptionIsRich() const { return d->mDescriptionIsRich; } void Incidence::setSummary(const QString &summary, bool isRich) { if (mReadOnly) { return; } - update(); - d->mSummary = summary; - d->mSummaryIsRich = isRich; - setFieldDirty(FieldSummary); - updated(); + if (d->mSummary != summary || d->mSummaryIsRich != isRich) { + update(); + d->mSummary = summary; + d->mSummaryIsRich = isRich; + setFieldDirty(FieldSummary); + updated(); + } } void Incidence::setSummary(const QString &summary) { setSummary(summary, Qt::mightBeRichText(summary)); } QString Incidence::summary() const { return d->mSummary; } QString Incidence::richSummary() const { if (summaryIsRich()) { return d->mSummary; } else { return Qt::escape(d->mSummary).replace(QLatin1Char('\n'), QLatin1String("
")); } } bool Incidence::summaryIsRich() const { return d->mSummaryIsRich; } void Incidence::setCategories(const QStringList &categories) { if (mReadOnly) { return; } update(); d->mCategories = categories; updated(); } void Incidence::setCategories(const QString &catStr) { if (mReadOnly) { return; } update(); setFieldDirty(FieldCategories); d->mCategories.clear(); if (catStr.isEmpty()) { updated(); return; } d->mCategories = catStr.split(QLatin1Char(',')); QStringList::Iterator it; for (it = d->mCategories.begin(); it != d->mCategories.end(); ++it) { *it = (*it).trimmed(); } updated(); } QStringList Incidence::categories() const { return d->mCategories; } QString Incidence::categoriesStr() const { return d->mCategories.join(QLatin1String(",")); } void Incidence::setRelatedTo(const QString &relatedToUid, RelType relType) { // TODO: RFC says that an incidence can have more than one related-to field // even for the same relType. if (d->mRelatedToUid[relType] != relatedToUid) { update(); d->mRelatedToUid[relType] = relatedToUid; setFieldDirty(FieldRelatedTo); updated(); } } QString Incidence::relatedTo(RelType relType) const { return d->mRelatedToUid.value(relType); } // %%%%%%%%%%%% Recurrence-related methods %%%%%%%%%%%%%%%%%%%% Recurrence *Incidence::recurrence() const { if (!d->mRecurrence) { d->mRecurrence = new Recurrence(); d->mRecurrence->setStartDateTime(dateTime(RoleRecurrenceStart)); d->mRecurrence->setAllDay(allDay()); d->mRecurrence->setRecurReadOnly(mReadOnly); d->mRecurrence->addObserver(const_cast(this)); } return d->mRecurrence; } void Incidence::clearRecurrence() { delete d->mRecurrence; d->mRecurrence = 0; } ushort Incidence::recurrenceType() const { if (d->mRecurrence) { return d->mRecurrence->recurrenceType(); } else { return Recurrence::rNone; } } bool Incidence::recurs() const { if (d->mRecurrence) { return d->mRecurrence->recurs(); } else { return false; } } bool Incidence::recursOn(const QDate &date, const KDateTime::Spec &timeSpec) const { return d->mRecurrence && d->mRecurrence->recursOn(date, timeSpec); } bool Incidence::recursAt(const KDateTime &qdt) const { return d->mRecurrence && d->mRecurrence->recursAt(qdt); } QList Incidence::startDateTimesForDate(const QDate &date, const KDateTime::Spec &timeSpec) const { KDateTime start = dtStart(); KDateTime end = dateTime(RoleEndRecurrenceBase); QList result; // TODO_Recurrence: Also work if only due date is given... if (!start.isValid() && ! end.isValid()) { return result; } // if the incidence doesn't recur, KDateTime kdate(date, timeSpec); if (!recurs()) { if (!(start > kdate || end < kdate)) { result << start; } return result; } int days = start.daysTo(end); // Account for possible recurrences going over midnight, while the original event doesn't QDate tmpday(date.addDays(-days - 1)); KDateTime tmp; while (tmpday <= date) { if (recurrence()->recursOn(tmpday, timeSpec)) { QList times = recurrence()->recurTimesOn(tmpday, timeSpec); foreach(const QTime &time, times) { tmp = KDateTime(tmpday, time, start.timeSpec()); if (endDateForStart(tmp) >= kdate) { result << tmp; } } } tmpday = tmpday.addDays(1); } return result; } QList Incidence::startDateTimesForDateTime(const KDateTime &datetime) const { KDateTime start = dtStart(); KDateTime end = dateTime(RoleEndRecurrenceBase); QList result; // TODO_Recurrence: Also work if only due date is given... if (!start.isValid() && ! end.isValid()) { return result; } // if the incidence doesn't recur, if (!recurs()) { if (!(start > datetime || end < datetime)) { result << start; } return result; } int days = start.daysTo(end); // Account for possible recurrences going over midnight, while the original event doesn't QDate tmpday(datetime.date().addDays(-days - 1)); KDateTime tmp; while (tmpday <= datetime.date()) { if (recurrence()->recursOn(tmpday, datetime.timeSpec())) { // Get the times during the day (in start date's time zone) when recurrences happen QList times = recurrence()->recurTimesOn(tmpday, start.timeSpec()); foreach(const QTime &time, times) { tmp = KDateTime(tmpday, time, start.timeSpec()); if (!(tmp > datetime || endDateForStart(tmp) < datetime)) { result << tmp; } } } tmpday = tmpday.addDays(1); } return result; } KDateTime Incidence::endDateForStart(const KDateTime &startDt) const { KDateTime start = dtStart(); KDateTime end = dateTime(RoleEndRecurrenceBase); if (!end.isValid()) { return start; } if (!start.isValid()) { return end; } return startDt.addSecs(start.secsTo(end)); } void Incidence::addAttachment(const Attachment::Ptr &attachment) { if (mReadOnly || !attachment) { return; } Q_ASSERT(!d->mAttachments.contains(attachment)); update(); d->mAttachments.append(attachment); setFieldDirty(FieldAttachment); updated(); } void Incidence::deleteAttachment(const Attachment::Ptr &attachment) { int index = d->mAttachments.indexOf(attachment); if (index > -1) { setFieldDirty(FieldAttachment); d->mAttachments.remove(index); } } void Incidence::deleteAttachments(const QString &mime) { Attachment::List result; Attachment::List::Iterator it = d->mAttachments.begin(); while (it != d->mAttachments.end()) { if ((*it)->mimeType() != mime) { result += *it; } ++it; } d->mAttachments = result; setFieldDirty(FieldAttachment); } Attachment::List Incidence::attachments() const { return d->mAttachments; } Attachment::List Incidence::attachments(const QString &mime) const { Attachment::List attachments; foreach(Attachment::Ptr attachment, d->mAttachments) { if (attachment->mimeType() == mime) { attachments.append(attachment); } } return attachments; } void Incidence::clearAttachments() { setFieldDirty(FieldAttachment); d->mAttachments.clear(); } QString Incidence::writeAttachmentToTempFile(const Attachment::Ptr &attachment) const { if (d->mTempFiles.contains(attachment)) { return d->mTempFiles.value(attachment); } KTemporaryFile *file = new KTemporaryFile(); QStringList patterns = KMimeType::mimeType(attachment->mimeType())->patterns(); if (!patterns.empty()) { file->setSuffix(QString(patterns.first()).remove(QLatin1Char('*'))); } file->setAutoRemove(true); file->open(); // read-only not to give the idea that it could be written to file->setPermissions(QFile::ReadUser); file->write(QByteArray::fromBase64(attachment->data())); d->mTempFiles.insert(attachment, file->fileName()); file->close(); return d->mTempFiles.value(attachment); } void Incidence::clearTempFiles() { QHash::const_iterator it = d->mTempFiles.constBegin(); const QHash::const_iterator end = d->mTempFiles.constEnd(); for (; it != end; ++it) { QFile::remove(it.value()); } d->mTempFiles.clear(); } void Incidence::setResources(const QStringList &resources) { if (mReadOnly) { return; } update(); d->mResources = resources; setFieldDirty(FieldResources); updated(); } QStringList Incidence::resources() const { return d->mResources; } void Incidence::setPriority(int priority) { if (mReadOnly) { return; } update(); d->mPriority = priority; setFieldDirty(FieldPriority); updated(); } int Incidence::priority() const { return d->mPriority; } void Incidence::setStatus(Incidence::Status status) { if (mReadOnly || status == StatusX) { return; } update(); d->mStatus = status; d->mStatusString.clear(); setFieldDirty(FieldStatus); updated(); } void Incidence::setCustomStatus(const QString &status) { if (mReadOnly) { return; } update(); d->mStatus = status.isEmpty() ? StatusNone : StatusX; d->mStatusString = status; setFieldDirty(FieldStatus); updated(); } Incidence::Status Incidence::status() const { return d->mStatus; } QString Incidence::customStatus() const { if (d->mStatus == StatusX) { return d->mStatusString; } else { return QString(); } } void Incidence::setSecrecy(Incidence::Secrecy secrecy) { if (mReadOnly) { return; } update(); d->mSecrecy = secrecy; setFieldDirty(FieldSecrecy); updated(); } Incidence::Secrecy Incidence::secrecy() const { return d->mSecrecy; } Alarm::List Incidence::alarms() const { return d->mAlarms; } Alarm::Ptr Incidence::newAlarm() { Alarm::Ptr alarm(new Alarm(this)); d->mAlarms.append(alarm); return alarm; } void Incidence::addAlarm(const Alarm::Ptr &alarm) { update(); d->mAlarms.append(alarm); setFieldDirty(FieldAlarms); updated(); } void Incidence::removeAlarm(const Alarm::Ptr &alarm) { const int index = d->mAlarms.indexOf(alarm); if (index > -1) { update(); d->mAlarms.remove(index); setFieldDirty(FieldAlarms); updated(); } } void Incidence::clearAlarms() { update(); d->mAlarms.clear(); setFieldDirty(FieldAlarms); updated(); } bool Incidence::hasEnabledAlarms() const { foreach(Alarm::Ptr alarm, d->mAlarms) { if (alarm->enabled()) { return true; } } return false; } void Incidence::setLocation(const QString &location, bool isRich) { if (mReadOnly) { return; } - update(); - d->mLocation = location; - d->mLocationIsRich = isRich; - setFieldDirty(FieldLocation); - updated(); + if (d->mLocation != location || d->mLocationIsRich != isRich) { + update(); + d->mLocation = location; + d->mLocationIsRich = isRich; + setFieldDirty(FieldLocation); + updated(); + } } void Incidence::setLocation(const QString &location) { setLocation(location, Qt::mightBeRichText(location)); } QString Incidence::location() const { return d->mLocation; } QString Incidence::richLocation() const { if (locationIsRich()) { return d->mLocation; } else { return Qt::escape(d->mLocation).replace(QLatin1Char('\n'), QLatin1String("
")); } } bool Incidence::locationIsRich() const { return d->mLocationIsRich; } void Incidence::setSchedulingID(const QString &sid, const QString &uid) { if (!uid.isEmpty()) { setUid(uid); } if (sid != d->mSchedulingID) { d->mSchedulingID = sid; setFieldDirty(FieldSchedulingId); } } QString Incidence::schedulingID() const { if (d->mSchedulingID.isNull()) { // Nothing set, so use the normal uid return uid(); } return d->mSchedulingID; } bool Incidence::hasGeo() const { return d->mHasGeo; } void Incidence::setHasGeo(bool hasGeo) { if (mReadOnly) { return; } if (hasGeo == d->mHasGeo) { return; } update(); d->mHasGeo = hasGeo; setFieldDirty(FieldGeoLatitude); setFieldDirty(FieldGeoLongitude); updated(); } float Incidence::geoLatitude() const { return d->mGeoLatitude; } void Incidence::setGeoLatitude(float geolatitude) { if (mReadOnly) { return; } update(); d->mGeoLatitude = geolatitude; setFieldDirty(FieldGeoLatitude); updated(); } float Incidence::geoLongitude() const { return d->mGeoLongitude; } void Incidence::setGeoLongitude(float geolongitude) { if (!mReadOnly) { update(); d->mGeoLongitude = geolongitude; setFieldDirty(FieldGeoLongitude); updated(); } } bool Incidence::hasRecurrenceId() const { return d->mRecurrenceId.isValid(); } KDateTime Incidence::recurrenceId() const { return d->mRecurrenceId; } void Incidence::setThisAndFuture(bool thisAndFuture) { d->mThisAndFuture = thisAndFuture; } bool Incidence::thisAndFuture() const { return d->mThisAndFuture; } void Incidence::setRecurrenceId(const KDateTime &recurrenceId) { if (!mReadOnly) { update(); d->mRecurrenceId = recurrenceId; setFieldDirty(FieldRecurrenceId); updated(); } } /** Observer interface for the recurrence class. If the recurrence is changed, this method will be called for the incidence the recurrence object belongs to. */ void Incidence::recurrenceUpdated(Recurrence *recurrence) { if (recurrence == d->mRecurrence) { update(); setFieldDirty(FieldRecurrence); updated(); } } //@cond PRIVATE #define ALT_DESC_FIELD "X-ALT-DESC" #define ALT_DESC_PARAMETERS QLatin1String("FMTTYPE=text/html") //@endcond bool Incidence::hasAltDescription() const { const QString value = nonKDECustomProperty(ALT_DESC_FIELD); const QString parameter = nonKDECustomPropertyParameters(ALT_DESC_FIELD); return parameter == ALT_DESC_PARAMETERS && !value.isEmpty(); } void Incidence::setAltDescription(const QString &altdescription) { if (altdescription.isEmpty()) { removeNonKDECustomProperty(ALT_DESC_FIELD); } else { setNonKDECustomProperty(ALT_DESC_FIELD, altdescription, ALT_DESC_PARAMETERS); } } QString Incidence::altDescription() const { if (!hasAltDescription()) { return QString(); } else { return nonKDECustomProperty(ALT_DESC_FIELD); } } bool Incidence::supportsGroupwareCommunication() const { return type() == TypeEvent || type() == TypeTodo; } /** static */ QStringList Incidence::mimeTypes() { return QStringList() << QLatin1String("text/calendar") << KCalCore::Event::eventMimeType() << KCalCore::Todo::todoMimeType() << KCalCore::Journal::journalMimeType(); } void Incidence::serialize(QDataStream &out) { out << d->mCreated << d->mRevision << d->mDescription << d->mDescriptionIsRich << d->mSummary << d->mSummaryIsRich << d->mLocation << d->mLocationIsRich << d->mCategories << d->mResources << d->mStatusString << d->mPriority << d->mSchedulingID << d->mGeoLatitude << d->mGeoLongitude << d->mHasGeo << d->mRecurrenceId << d->mThisAndFuture << d->mLocalOnly << d->mStatus << d->mSecrecy << (d->mRecurrence ? true : false) << d->mAttachments.count() << d->mAlarms.count() << d->mRelatedToUid; if (d->mRecurrence) out << d->mRecurrence; foreach(const Attachment::Ptr &attachment, d->mAttachments) { out << attachment; } foreach(const Alarm::Ptr &alarm, d->mAlarms) { out << alarm; } } void Incidence::deserialize(QDataStream &in) { quint32 status, secrecy; bool hasRecurrence; int attachmentCount, alarmCount; QMap relatedToUid; in >> d->mCreated >> d->mRevision >> d->mDescription >> d->mDescriptionIsRich >> d->mSummary >> d->mSummaryIsRich >> d->mLocation >> d->mLocationIsRich >> d->mCategories >> d->mResources >> d->mStatusString >> d->mPriority >> d->mSchedulingID >> d->mGeoLatitude >> d->mGeoLongitude >> d->mHasGeo >> d->mRecurrenceId >> d->mThisAndFuture >> d->mLocalOnly >> status >> secrecy >> hasRecurrence >> attachmentCount >> alarmCount >> relatedToUid; if (hasRecurrence) { d->mRecurrence = new Recurrence(); d->mRecurrence->addObserver(const_cast(this)); in >> d->mRecurrence; } d->mAttachments.clear(); d->mAlarms.clear(); for (int i=0; i> attachment; d->mAttachments.append(attachment); } for (int i=0; i> alarm; d->mAlarms.append(alarm); } d->mStatus = static_cast(status); d->mSecrecy = static_cast(secrecy); d->mRelatedToUid.clear(); foreach(int key, relatedToUid.keys()) { //krazy:exclude=foreach d->mRelatedToUid.insert(static_cast(key), relatedToUid.value(key)); } } diff --git a/kcalcore/incidencebase.cpp b/kcalcore/incidencebase.cpp index 5e9bddf3a..73661ba5c 100644 --- a/kcalcore/incidencebase.cpp +++ b/kcalcore/incidencebase.cpp @@ -1,748 +1,750 @@ /* This file is part of the kcalcore library. Copyright (c) 2001,2004 Cornelius Schumacher Copyright (C) 2003-2004 Reinhold Kainhofer Copyright (c) 2009 Nokia Corporation and/or its subsidiary(-ies). All rights reserved. Contact: Alvaro Manera This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ /** @file This file is part of the API for handling calendar data and defines the IncidenceBase class. @brief An abstract base class that provides a common base for all calendar incidence classes. @author Cornelius Schumacher \ @author Reinhold Kainhofer \ */ #include "incidencebase.h" #include "calformat.h" #include "visitor.h" #include #include #include #include #define KCALCORE_MAGIC_NUMBER 0xCA1C012E #define KCALCORE_SERIALIZATION_VERSION 1 using namespace KCalCore; /** Private class that helps to provide binary compatibility between releases. @internal */ //@cond PRIVATE class KCalCore::IncidenceBase::Private { public: Private() : mOrganizer(0), mUpdateGroupLevel(0), mUpdatedPending(false), mAllDay(true), mHasDuration(false) {} Private(const Private &other) : mUpdateGroupLevel(0), mUpdatedPending(false), mAllDay(true), mHasDuration(false) { init(other); } ~Private() { } void init(const Private &other); KDateTime mLastModified; // incidence last modified date KDateTime mDtStart; // incidence start time Person::Ptr mOrganizer; // incidence person (owner) QString mUid; // incidence unique id Duration mDuration; // incidence duration int mUpdateGroupLevel; // if non-zero, suppresses update() calls bool mUpdatedPending; // true if an update has occurred since startUpdates() bool mAllDay; // true if the incidence is all-day bool mHasDuration; // true if the incidence has a duration Attendee::List mAttendees; // list of incidence attendees QStringList mComments; // list of incidence comments QStringList mContacts; // list of incidence contacts QList mObservers; // list of incidence observers QSet mDirtyFields; // Fields that changed since last time the incidence was created // or since resetDirtyFlags() was called QUrl mUrl; // incidence url property }; void IncidenceBase::Private::init(const Private &other) { mLastModified = other.mLastModified; mDtStart = other.mDtStart; mOrganizer = other.mOrganizer; mUid = other.mUid; mDuration = other.mDuration; mAllDay = other.mAllDay; mHasDuration = other.mHasDuration; mComments = other.mComments; mContacts = other.mContacts; mAttendees.clear(); Attendee::List::ConstIterator it; for (it = other.mAttendees.constBegin(); it != other.mAttendees.constEnd(); ++it) { mAttendees.append(Attendee::Ptr(new Attendee(*(*it)))); } mUrl = other.mUrl; } //@endcond IncidenceBase::IncidenceBase() : d(new KCalCore::IncidenceBase::Private) { mReadOnly = false; setUid(CalFormat::createUniqueId()); } IncidenceBase::IncidenceBase(const IncidenceBase &i) : CustomProperties(i), d(new KCalCore::IncidenceBase::Private(*i.d)) { mReadOnly = i.mReadOnly; } IncidenceBase::~IncidenceBase() { delete d; } IncidenceBase &IncidenceBase::operator=(const IncidenceBase &other) { Q_ASSERT(type() == other.type()); startUpdates(); // assign is virtual, will call the derived class's IncidenceBase &ret = assign(other); endUpdates(); return ret; } IncidenceBase &IncidenceBase::assign(const IncidenceBase &other) { CustomProperties::operator=(other); d->init(*other.d); mReadOnly = other.mReadOnly; d->mDirtyFields.clear(); d->mDirtyFields.insert(FieldUnknown); return *this; } bool IncidenceBase::operator==(const IncidenceBase &i2) const { if (i2.type() != type()) { return false; } else { // equals is virtual, so here we're calling the derived class method return equals(i2); } } bool IncidenceBase::operator!=(const IncidenceBase &i2) const { return !operator==(i2); } bool IncidenceBase::equals(const IncidenceBase &i2) const { if (attendees().count() != i2.attendees().count()) { // kDebug() << "Attendee count is different"; return false; } Attendee::List al1 = attendees(); Attendee::List al2 = i2.attendees(); Attendee::List::ConstIterator a1 = al1.constBegin(); Attendee::List::ConstIterator a2 = al2.constBegin(); //TODO Does the order of attendees in the list really matter? //Please delete this comment if you know it's ok, kthx for (; a1 != al1.constEnd() && a2 != al2.constEnd(); ++a1, ++a2) { if (!(**a1 == **a2)) { // kDebug() << "Attendees are different"; return false; } } if (!CustomProperties::operator==(i2)) { // kDebug() << "Properties are different"; return false; } // Don't compare lastModified, otherwise the operator is not // of much use. We are not comparing for identity, after all. // no need to compare mObserver bool a = ((dtStart() == i2.dtStart()) || (!dtStart().isValid() && !i2.dtStart().isValid())); bool b = *(organizer().data()) == *(i2.organizer().data()); bool c = uid() == i2.uid(); bool d = allDay() == i2.allDay(); bool e = duration() == i2.duration(); bool f = hasDuration() == i2.hasDuration(); bool g = url() == i2.url(); //kDebug() << a << b << c << d << e << f << g; return a && b && c && d && e && f && g; } bool IncidenceBase::accept(Visitor &v, IncidenceBase::Ptr incidence) { Q_UNUSED(v); Q_UNUSED(incidence); return false; } void IncidenceBase::setUid(const QString &uid) { if (d->mUid != uid) { update(); d->mUid = uid; d->mDirtyFields.insert(FieldUid); updated(); } } QString IncidenceBase::uid() const { return d->mUid; } void IncidenceBase::setLastModified(const KDateTime &lm) { // DON'T! updated() because we call this from // Calendar::updateEvent(). d->mDirtyFields.insert(FieldLastModified); // Convert to UTC and remove milliseconds part. KDateTime current = lm.toUtc(); QTime t = current.time(); t.setHMS(t.hour(), t.minute(), t.second(), 0); current.setTime(t); d->mLastModified = current; } KDateTime IncidenceBase::lastModified() const { return d->mLastModified; } void IncidenceBase::setOrganizer(const Person::Ptr &organizer) { if (organizer) { update(); // we don't check for readonly here, because it is // possible that by setting the organizer we are changing // the event's readonly status... d->mOrganizer = organizer; d->mDirtyFields.insert(FieldOrganizer); updated(); } } void IncidenceBase::setOrganizer(const QString &o) { QString mail(o); if (mail.startsWith(QLatin1String("MAILTO:"), Qt::CaseInsensitive)) { mail = mail.remove(0, 7); } // split the string into full name plus email. const Person::Ptr organizer = Person::fromFullName(mail); setOrganizer(organizer); } Person::Ptr IncidenceBase::organizer() const { if (!d->mOrganizer) d->mOrganizer = Person::Ptr(new Person()); // init at first use only to save memory return d->mOrganizer; } void IncidenceBase::setReadOnly(bool readOnly) { mReadOnly = readOnly; } bool IncidenceBase::isReadOnly() const { return mReadOnly; } void IncidenceBase::setDtStart(const KDateTime &dtStart) { // if ( mReadOnly ) return; if (!dtStart.isValid() && type() != IncidenceBase::TypeTodo) { kWarning() << "Invalid dtStart"; } - update(); - d->mDtStart = dtStart; - d->mAllDay = dtStart.isDateOnly(); - d->mDirtyFields.insert(FieldDtStart); - updated(); + if (d->mDtStart != dtStart || d->mAllDay != dtStart.isDateOnly()) { + update(); + d->mDtStart = dtStart; + d->mAllDay = dtStart.isDateOnly(); + d->mDirtyFields.insert(FieldDtStart); + updated(); + } } KDateTime IncidenceBase::dtStart() const { return d->mDtStart; } bool IncidenceBase::allDay() const { return d->mAllDay; } void IncidenceBase::setAllDay(bool f) { if (mReadOnly || f == d->mAllDay) { return; } update(); d->mAllDay = f; if (d->mDtStart.isValid()) { d->mDirtyFields.insert(FieldDtStart); } updated(); } void IncidenceBase::shiftTimes(const KDateTime::Spec &oldSpec, const KDateTime::Spec &newSpec) { update(); d->mDtStart = d->mDtStart.toTimeSpec(oldSpec); d->mDtStart.setTimeSpec(newSpec); d->mDirtyFields.insert(FieldDtStart); d->mDirtyFields.insert(FieldDtEnd); updated(); } void IncidenceBase::addComment(const QString &comment) { d->mComments += comment; } bool IncidenceBase::removeComment(const QString &comment) { bool found = false; QStringList::Iterator i; for (i = d->mComments.begin(); !found && i != d->mComments.end(); ++i) { if ((*i) == comment) { found = true; d->mComments.erase(i); } } if (found) { d->mDirtyFields.insert(FieldComment); } return found; } void IncidenceBase::clearComments() { d->mDirtyFields.insert(FieldComment); d->mComments.clear(); } QStringList IncidenceBase::comments() const { return d->mComments; } void IncidenceBase::addContact(const QString &contact) { if (!contact.isEmpty()) { d->mContacts += contact; d->mDirtyFields.insert(FieldContact); } } bool IncidenceBase::removeContact(const QString &contact) { bool found = false; QStringList::Iterator i; for (i = d->mContacts.begin(); !found && i != d->mContacts.end(); ++i) { if ((*i) == contact) { found = true; d->mContacts.erase(i); } } if (found) { d->mDirtyFields.insert(FieldContact); } return found; } void IncidenceBase::clearContacts() { d->mDirtyFields.insert(FieldContact); d->mContacts.clear(); } QStringList IncidenceBase::contacts() const { return d->mContacts; } void IncidenceBase::addAttendee(const Attendee::Ptr &a, bool doupdate) { if (!a || mReadOnly) { return; } Q_ASSERT(!d->mAttendees.contains(a)); if (doupdate) { update(); } if (a->name().left(7).toUpper() == QLatin1String("MAILTO:")) { a->setName(a->name().remove(0, 7)); } /* If Uid is empty, just use the pointer to Attendee (encoded to * string) as Uid. Only thing that matters is that the Uid is unique * insofar IncidenceBase is concerned, and this does that (albeit * not very nicely). If these are ever saved to disk, should use * (considerably more expensive) CalFormat::createUniqueId(). As Uid * is not part of Attendee in iCal std, it's fairly safe bet that * these will never hit disc though so faster generation speed is * more important than actually being forever unique.*/ if (a->uid().isEmpty()) { a->setUid(QString::number((qlonglong)a.data())); } d->mAttendees.append(a); if (doupdate) { d->mDirtyFields.insert(FieldAttendees); updated(); } } void IncidenceBase::deleteAttendee(const Attendee::Ptr &a, bool doupdate) { if (!a || mReadOnly) { return; } int index = d->mAttendees.indexOf(a); if (index >= 0) { if (doupdate) { update(); } d->mAttendees.remove(index); if (doupdate) { d->mDirtyFields.insert(FieldAttendees); updated(); } } } Attendee::List IncidenceBase::attendees() const { return d->mAttendees; } int IncidenceBase::attendeeCount() const { return d->mAttendees.count(); } void IncidenceBase::clearAttendees() { if (mReadOnly) { return; } d->mDirtyFields.insert(FieldAttendees); d->mAttendees.clear(); } Attendee::Ptr IncidenceBase::attendeeByMail(const QString &email) const { Attendee::List::ConstIterator it; for (it = d->mAttendees.constBegin(); it != d->mAttendees.constEnd(); ++it) { if ((*it)->email() == email) { return *it; } } return Attendee::Ptr(); } Attendee::Ptr IncidenceBase::attendeeByMails(const QStringList &emails, const QString &email) const { QStringList mails = emails; if (!email.isEmpty()) { mails.append(email); } Attendee::List::ConstIterator itA; for (itA = d->mAttendees.constBegin(); itA != d->mAttendees.constEnd(); ++itA) { for (QStringList::const_iterator it = mails.constBegin(); it != mails.constEnd(); ++it) { if ((*itA)->email() == (*it)) { return *itA; } } } return Attendee::Ptr(); } Attendee::Ptr IncidenceBase::attendeeByUid(const QString &uid) const { Attendee::List::ConstIterator it; for (it = d->mAttendees.constBegin(); it != d->mAttendees.constEnd(); ++it) { if ((*it)->uid() == uid) { return *it; } } return Attendee::Ptr(); } void IncidenceBase::setDuration(const Duration &duration) { update(); d->mDuration = duration; setHasDuration(true); d->mDirtyFields.insert(FieldDuration); updated(); } Duration IncidenceBase::duration() const { return d->mDuration; } void IncidenceBase::setHasDuration(bool hasDuration) { d->mHasDuration = hasDuration; } bool IncidenceBase::hasDuration() const { return d->mHasDuration; } void IncidenceBase::setUrl(const QUrl& url) { d->mDirtyFields.insert(FieldUrl); d->mUrl = url; } QUrl IncidenceBase::url() const { return d->mUrl; } void IncidenceBase::registerObserver(IncidenceBase::IncidenceObserver *observer) { if (observer && !d->mObservers.contains(observer)) { d->mObservers.append(observer); } } void IncidenceBase::unRegisterObserver(IncidenceBase::IncidenceObserver *observer) { d->mObservers.removeAll(observer); } void IncidenceBase::update() { if (!d->mUpdateGroupLevel) { d->mUpdatedPending = true; KDateTime rid = recurrenceId(); foreach(IncidenceObserver *o, d->mObservers) { o->incidenceUpdate(uid(), rid); } } } void IncidenceBase::updated() { if (d->mUpdateGroupLevel) { d->mUpdatedPending = true; } else { const KDateTime rid = recurrenceId(); foreach(IncidenceObserver *o, d->mObservers) { o->incidenceUpdated(uid(), rid); } } } void IncidenceBase::startUpdates() { update(); ++d->mUpdateGroupLevel; } void IncidenceBase::endUpdates() { if (d->mUpdateGroupLevel > 0) { if (--d->mUpdateGroupLevel == 0 && d->mUpdatedPending) { d->mUpdatedPending = false; updated(); } } } void IncidenceBase::customPropertyUpdate() { update(); } void IncidenceBase::customPropertyUpdated() { updated(); } KDateTime IncidenceBase::recurrenceId() const { return KDateTime(); } void IncidenceBase::resetDirtyFields() { d->mDirtyFields.clear(); } QSet IncidenceBase::dirtyFields() const { return d->mDirtyFields; } void IncidenceBase::setFieldDirty(IncidenceBase::Field field) { d->mDirtyFields.insert(field); } KUrl IncidenceBase::uri() const { return KUrl(QLatin1String("urn:x-ical:") + uid()); } void IncidenceBase::setDirtyFields(const QSet &dirtyFields) { d->mDirtyFields = dirtyFields; } /** static */ quint32 IncidenceBase::magicSerializationIdentifier() { return KCALCORE_MAGIC_NUMBER; } QDataStream& KCalCore::operator<<(QDataStream &out, const KCalCore::IncidenceBase::Ptr &i) { if (!i) return out; out << static_cast(KCALCORE_MAGIC_NUMBER); // Magic number to identify KCalCore data out << static_cast(KCALCORE_SERIALIZATION_VERSION); out << static_cast(i->type()); out << *(static_cast(i.data())); out << i->d->mLastModified << i->d->mDtStart << i->organizer() << i->d->mUid << i->d->mDuration << i->d->mAllDay << i->d->mHasDuration << i->d->mComments << i->d->mContacts << i->d->mAttendees.count() << i->d->mUrl; foreach(const Attendee::Ptr &attendee, i->d->mAttendees) { out << attendee; } // Serialize the sub-class data. In KDE5 we can add new virtuals. i->virtual_hook(KCalCore::IncidenceBase::SerializerHook, &out); return out; } QDataStream& KCalCore::operator>>(QDataStream &in, const KCalCore::IncidenceBase::Ptr &i) { if (!i) return in; qint32 attendeeCount, type; quint32 magic, version; in >> magic; if (magic != KCALCORE_MAGIC_NUMBER) { kWarning() << "Invalid magic on serialized data"; return in; } in >> version; if (version > KCALCORE_MAGIC_NUMBER) { kWarning() << "Invalid version on serialized data"; return in; } in >> type; in >> *(static_cast(i.data())); in >> i->d->mLastModified >> i->d->mDtStart >> i->d->mOrganizer >> i->d->mUid >> i->d->mDuration >> i->d->mAllDay >> i->d->mHasDuration >> i->d->mComments >> i->d->mContacts >> attendeeCount >> i->d->mUrl; i->d->mAttendees.clear(); for (int it=0; it> attendee; i->d->mAttendees.append(attendee); } // Deserialize the sub-class data. In KDE5 we can add new virtuals. i->virtual_hook(KCalCore::IncidenceBase::DeserializerHook, &in); return in; } IncidenceBase::IncidenceObserver::~IncidenceObserver() { } diff --git a/kcalcore/recurrence.cpp b/kcalcore/recurrence.cpp index a1ad46034..f2f192a58 100644 --- a/kcalcore/recurrence.cpp +++ b/kcalcore/recurrence.cpp @@ -1,1450 +1,1561 @@ /* This file is part of kcalcore library. Copyright (c) 1998 Preston Brown Copyright (c) 2001 Cornelius Schumacher Copyright (c) 2002,2006 David Jarvie Copyright (C) 2005 Reinhold Kainhofer This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "recurrence.h" #include #include #include using namespace KCalCore; //@cond PRIVATE class KCalCore::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; DateTimeList mRDateTimes; DateList mRDates; DateTimeList mExDateTimes; DateList mExDates; KDateTime 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; // the recurrence has no time, just a date bool mRecurReadOnly; }; bool Recurrence::Private::operator==(const Recurrence::Private &p) const { 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 KCalCore::Recurrence::Private()) { } Recurrence::Recurrence(const Recurrence &r) : RecurrenceRule::RuleObserver(), d(new KCalCore::Recurrence::Private(*r.d)) { int i, end; 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); } 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; } Recurrence &Recurrence::operator=(const Recurrence &recurrence) { // check for self assignment if (&recurrence == this) { return *this; } *d = *recurrence.d; return *this; } void Recurrence::addObserver(RecurrenceObserver *observer) { if (!d->mObservers.contains(observer)) { d->mObservers.append(observer); } } void Recurrence::removeObserver(RecurrenceObserver *observer) { if (d->mObservers.contains(observer)) { d->mObservers.removeAll(observer); } } KDateTime 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 0; } 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() ? 0 : 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; } break; } default: return rOther; } return rOther; } bool Recurrence::recursOn(const QDate &qd, const KDateTime::Spec &timeSpec) const { // Don't waste time if date is before the start of the recurrence if (KDateTime(qd, QTime(23, 59, 59), timeSpec) < d->mStartDateTime) { return false; } // First handle dates. Exrules override if (d->mExDates.containsSorted(qd)) { return false; } int i, end; TimeList tms; // 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, timeSpec)) { return false; } } } if (d->mRDates.containsSorted(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].toTimeSpec(timeSpec).date() == qd); } for (i = 0, end = d->mRRules.count(); i < end && !recurs; ++i) { recurs = d->mRRules[i]->recursOn(qd, timeSpec); } // 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].toTimeSpec(timeSpec).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, timeSpec); } } 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, timeSpec)); return !timesForDay.isEmpty(); } } bool Recurrence::recursAt(const KDateTime &dt) const { // Convert to recurrence's time zone for date comparisons, and for more efficient time comparisons KDateTime dtrecur = dt.toTimeSpec(d->mStartDateTime.timeSpec()); // if it's excluded anyway, don't bother to check if it recurs at all. if (d->mExDateTimes.containsSorted(dtrecur) || d->mExDates.containsSorted(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 || d->mRDateTimes.containsSorted(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. */ KDateTime Recurrence::endDateTime() const { DateTimeList dts; dts << startDateTime(); if (!d->mRDates.isEmpty()) { dts << KDateTime(d->mRDates.last(), QTime(0, 0, 0), d->mStartDateTime.timeSpec()); } if (!d->mRDateTimes.isEmpty()) { dts << d->mRDateTimes.last(); } for (int i = 0, end = d->mRRules.count(); i < end; ++i) { KDateTime rl(d->mRRules[i]->endDt()); // if any of the rules is infinite, the whole recurrence is if (!rl.isValid()) { return KDateTime(); } dts << rl; } dts.sortUnique(); return dts.isEmpty() ? KDateTime() : 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 { KDateTime end(endDateTime()); return end.isValid() ? end.date() : QDate(); } void Recurrence::setEndDate(const QDate &date) { KDateTime dt(date, d->mStartDateTime.time(), d->mStartDateTime.timeSpec()); if (allDay()) { dt.setTime(QTime(23, 59, 59)); } setEndDateTime(dt); } void Recurrence::setEndDateTime(const KDateTime &dateTime) { if (d->mRecurReadOnly) { return; } RecurrenceRule *rrule = defaultRRule(true); if (!rrule) { return; } - rrule->setEndDt(dateTime); - updated(); + + // we have duration end and set a invalid date -> we still have duration ending + 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 KDateTime &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(KDateTime(date, QTime(23, 59, 59), d->mStartDateTime.timeSpec())); } void Recurrence::setDuration(int duration) { if (d->mRecurReadOnly) { return; } RecurrenceRule *rrule = defaultRRule(true); if (!rrule) { return; } - rrule->setDuration(duration); - updated(); + + if (duration != rrule->duration()) { + rrule->setDuration(duration); + updated(); + } } void Recurrence::shiftTimes(const KDateTime::Spec &oldSpec, const KDateTime::Spec &newSpec) { if (d->mRecurReadOnly) { return; } d->mStartDateTime = d->mStartDateTime.toTimeSpec(oldSpec); d->mStartDateTime.setTimeSpec(newSpec); int i, end; for (i = 0, end = d->mRDateTimes.count(); i < end; ++i) { d->mRDateTimes[i] = d->mRDateTimes[i].toTimeSpec(oldSpec); d->mRDateTimes[i].setTimeSpec(newSpec); } for (i = 0, end = d->mExDateTimes.count(); i < end; ++i) { d->mExDateTimes[i] = d->mExDateTimes[i].toTimeSpec(oldSpec); d->mExDateTimes[i].setTimeSpec(newSpec); } for (i = 0, end = d->mRRules.count(); i < end; ++i) { d->mRRules[i]->shiftTimes(oldSpec, newSpec); } for (i = 0, end = d->mExRules.count(); i < end; ++i) { d->mExRules[i]->shiftTimes(oldSpec, newSpec); } } 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 KDateTime &start) { if (d->mRecurReadOnly) { return; } d->mStartDateTime = start; setAllDay(start.isDateOnly()); // 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) { 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 0; } + if ( defaultRRuleConst() && defaultRRuleConst()->recurrenceType() == type && frequency() == freq) { + rrule->setDuration(-1); + return 0; + } + qDeleteAll(d->mRRules); d->mRRules.clear(); updated(); RecurrenceRule *rrule = defaultRRule(true); if (!rrule) { return 0; } 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); - rrule->setByDays(positions); - updated(); + 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); - rrule->setByMonthDays(monthDays); - updated(); + setMonthlyDate(monthDays); + } +} + +void Recurrence::setMonthlyDate(const QList< int > &monthlyDays) +{ + if (d->mRecurReadOnly) { + return; + } + + RecurrenceRule *rrule = defaultRRule(true); + if (!rrule) { + return; + } + + SortableList mD(monthlyDays); + SortableList rbD(rrule->byMonthDays()); + + mD.sortUnique(); + rbD.sortUnique(); + + 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; + } + + SortableList d(days); + SortableList bYD(rrule->byYearDays()); + + d.sortUnique(); + bYD.sortUnique(); + + 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(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; + } + + SortableList m(months); + SortableList bM(rrule->byMonths()); + + m.sortUnique(); + bM.sortUnique(); + + if (m != bM) { rrule->setByMonths(months); updated(); } } TimeList Recurrence::recurTimesOn(const QDate &date, const KDateTime::Spec &timeSpec) const { // kDebug() << "recurTimesOn(" << date << ")"; int i, end; TimeList times; // The whole day is excepted if (d->mExDates.containsSorted(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, timeSpec)) { return times; } } } KDateTime dt = startDateTime().toTimeSpec(timeSpec); if (dt.date() == date) { times << dt.time(); } bool foundDate = false; for (i = 0, end = d->mRDateTimes.count(); i < end; ++i) { dt = d->mRDateTimes[i].toTimeSpec(timeSpec); 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, timeSpec); } times.sortUnique(); foundDate = false; TimeList extimes; for (i = 0, end = d->mExDateTimes.count(); i < end; ++i) { dt = d->mExDateTimes[i].toTimeSpec(timeSpec); 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, timeSpec); } } extimes.sortUnique(); int st = 0; for (i = 0, end = extimes.count(); i < end; ++i) { int j = times.removeSorted(extimes[i], st); if (j >= 0) { st = j; } } return times; } DateTimeList Recurrence::timesInInterval(const KDateTime &start, const KDateTime &end) const { int i, count; DateTimeList 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 KDateTime 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; } times.sortUnique(); // 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; } } DateTimeList extimes; for (i = 0, count = d->mExRules.count(); i < count; ++i) { extimes += d->mExRules[i]->timesInInterval(start, end); } 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; } } return times; } KDateTime Recurrence::getNextDateTime(const KDateTime &preDateTime) const { KDateTime 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 KDateTime // 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 = KDateTime 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 DateTimeList dates; if (nextDT < startDateTime()) { dates << startDateTime(); } int end; // Assume that the rdatetime list is sorted int i = d->mRDateTimes.findGT(nextDT); if (i >= 0) { dates << d->mRDateTimes[i]; } KDateTime kdt(startDateTime()); for (i = 0, end = d->mRDates.count(); i < end; ++i) { kdt.setDate(d->mRDates[i]); if (kdt > nextDT) { dates << kdt; break; } } // Add the next occurrences from all RRULEs. for (i = 0, end = d->mRRules.count(); i < end; ++i) { KDateTime dt = d->mRRules[i]->getNextDate(nextDT); if (dt.isValid()) { dates << dt; } } // Take the first of these (all others can't be used later on) dates.sortUnique(); if (dates.isEmpty()) { return KDateTime(); } nextDT = dates.first(); // Check if that date/time is excluded explicitly or by an exrule: if (!d->mExDates.containsSorted(nextDT.date()) && !d->mExDateTimes.containsSorted(nextDT)) { bool allowed = true; for (i = 0, end = d->mExRules.count(); i < end; ++i) { allowed = allowed && !(d->mExRules[i]->recursAt(nextDT)); } if (allowed) { return nextDT; } } } // Couldn't find a valid occurrences in 1000 loops, something is wrong! return KDateTime(); } KDateTime Recurrence::getPreviousDateTime(const KDateTime &afterDateTime) const { KDateTime 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 KDateTime 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 = KDateTime 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 DateTimeList dates; if (prevDT > startDateTime()) { dates << startDateTime(); } int i = d->mRDateTimes.findLT(prevDT); if (i >= 0) { dates << d->mRDateTimes[i]; } KDateTime kdt(startDateTime()); for (i = d->mRDates.count(); --i >= 0;) { kdt.setDate(d->mRDates[i]); if (kdt < prevDT) { dates << kdt; break; } } // Add the previous occurrences from all RRULEs. int end; for (i = 0, end = d->mRRules.count(); i < end; ++i) { KDateTime dt = d->mRRules[i]->getPreviousDate(prevDT); if (dt.isValid()) { dates << dt; } } // Take the last of these (all others can't be used later on) dates.sortUnique(); if (dates.isEmpty()) { return KDateTime(); } prevDT = dates.last(); // Check if that date/time is excluded explicitly or by an exrule: if (!d->mExDates.containsSorted(prevDT.date()) && !d->mExDateTimes.containsSorted(prevDT)) { bool allowed = true; for (i = 0, end = d->mExRules.count(); i < end; ++i) { allowed = allowed && !(d->mExRules[i]->recursAt(prevDT)); } if (allowed) { return prevDT; } } } // Couldn't find a valid occurrences in 1000 loops, something is wrong! return KDateTime(); } /***************************** 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(); } DateTimeList Recurrence::rDateTimes() const { return d->mRDateTimes; } void Recurrence::setRDateTimes(const DateTimeList &rdates) { if (d->mRecurReadOnly) { return; } d->mRDateTimes = rdates; d->mRDateTimes.sortUnique(); updated(); } void Recurrence::addRDateTime(const KDateTime &rdate) { if (d->mRecurReadOnly) { return; } d->mRDateTimes.insertSorted(rdate); updated(); } DateList Recurrence::rDates() const { return d->mRDates; } void Recurrence::setRDates(const DateList &rdates) { if (d->mRecurReadOnly) { return; } d->mRDates = rdates; d->mRDates.sortUnique(); updated(); } void Recurrence::addRDate(const QDate &rdate) { if (d->mRecurReadOnly) { return; } d->mRDates.insertSorted(rdate); updated(); } DateTimeList Recurrence::exDateTimes() const { return d->mExDateTimes; } void Recurrence::setExDateTimes(const DateTimeList &exdates) { if (d->mRecurReadOnly) { return; } d->mExDateTimes = exdates; d->mExDateTimes.sortUnique(); } void Recurrence::addExDateTime(const KDateTime &exdate) { if (d->mRecurReadOnly) { return; } d->mExDateTimes.insertSorted(exdate); updated(); } DateList Recurrence::exDates() const { return d->mExDates; } void Recurrence::setExDates(const DateList &exdates) { if (d->mRecurReadOnly) { return; } - d->mExDates = exdates; - d->mExDates.sortUnique(); - updated(); + DateList l = exdates; + l.sortUnique(); + + if (d->mExDates != l) { + d->mExDates = l; + updated(); + } } void Recurrence::addExDate(const QDate &exdate) { if (d->mRecurReadOnly) { return; } d->mExDates.insertSorted(exdate); updated(); } void Recurrence::recurrenceChanged(RecurrenceRule *) { updated(); } // %%%%%%%%%%%%%%%%%% end:Recurrencerule %%%%%%%%%%%%%%%%%% void Recurrence::dump() const { kDebug(); int i; int count = d->mRRules.count(); kDebug() << " -)" << count << "RRULEs:"; for (i = 0; i < count; ++i) { kDebug() << " -) RecurrenceRule: "; d->mRRules[i]->dump(); } count = d->mExRules.count(); kDebug() << " -)" << count << "EXRULEs:"; for (i = 0; i < count; ++i) { kDebug() << " -) ExceptionRule :"; d->mExRules[i]->dump(); } count = d->mRDates.count(); kDebug() << endl << " -)" << count << "Recurrence Dates:"; for (i = 0; i < count; ++i) { kDebug() << " " << d->mRDates[i]; } count = d->mRDateTimes.count(); kDebug() << endl << " -)" << count << "Recurrence Date/Times:"; for (i = 0; i < count; ++i) { kDebug() << " " << d->mRDateTimes[i].dateTime(); } count = d->mExDates.count(); kDebug() << endl << " -)" << count << "Exceptions Dates:"; for (i = 0; i < count; ++i) { kDebug() << " " << d->mExDates[i]; } count = d->mExDateTimes.count(); kDebug() << endl << " -)" << count << "Exception Date/Times:"; for (i = 0; i < count; ++i) { kDebug() << " " << d->mExDateTimes[i].dateTime(); } } Recurrence::RecurrenceObserver::~RecurrenceObserver() { } KCALCORE_EXPORT QDataStream& KCalCore::operator<<(QDataStream &out, KCalCore::Recurrence *r) { if (!r) return out; out << r->d->mRDateTimes << r->d->mExDateTimes << r->d->mRDates << r->d->mStartDateTime << r->d->mCachedType << r->d->mAllDay << r->d->mRecurReadOnly << r->d->mExDates << r->d->mExRules.count() << r->d->mRRules.count(); foreach(RecurrenceRule *rule, r->d->mExRules) { out << rule; } foreach(RecurrenceRule *rule, r->d->mRRules) { out << rule; } return out; } KCALCORE_EXPORT QDataStream& KCalCore::operator>>(QDataStream &in, KCalCore::Recurrence *r) { if (!r) return in; int rruleCount, exruleCount; in >> r->d->mRDateTimes >> r->d->mExDateTimes >> r->d->mRDates >> r->d->mStartDateTime >> 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; iaddObserver(r); in >> rule; r->d->mExRules.append(rule); } for (int i=0; iaddObserver(r); in >> rule; r->d->mRRules.append(rule); } return in; } diff --git a/kcalcore/recurrence.h b/kcalcore/recurrence.h index acf5ebcce..473865cb6 100644 --- a/kcalcore/recurrence.h +++ b/kcalcore/recurrence.h @@ -1,663 +1,675 @@ /* This file is part of the kcalcore library. Copyright (c) 1998 Preston Brown Copyright (c) 2001,2003 Cornelius Schumacher Copyright (c) 2002,2006 David Jarvie Copyright (C) 2005 Reinhold Kainhofer This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KCALCORE_RECURRENCE_H #define KCALCORE_RECURRENCE_H #include "kcalcore_export.h" #include "recurrencerule.h" class QBitArray; namespace KCalCore { class RecurrenceRule; /** This class represents a recurrence rule for a calendar incidence. It manages all recurrence rules, recurrence date/times, exception rules and exception date times that can appear inside calendar items. Each recurrence rule and exception rule is represented as an object of type RecurrenceRule. For the simple case where at most one recurrence rule is present, this class provides shortcut methods to set the type: setMinutely() setHourly() setDaily() setWeekly() setMonthly() setYearly() to set/get general information about the recurrence: setEndDate() setEndDateTime() duration() durationTo() setDuration() frequency() setFrequency() and to set/get specific information about the recurrence within the interval: days() monthDays() monthPositions() yearDays() yearDates() yearMonths() yearPositions() addMonthlyPos() addMonthlyDate() addYearlyDay() addYearlyDate() addYearlyPos() addYearlyMonth() These are all available so that you don't have to work on the RecurrenceRule objects themselves. In other words, in that simple situation the interface stays almost the same compared to the old Recurrence class, which allowed only one recurrence rule. As soon as your recurrence consists of multiple recurrence rules or exception rules, you cannot use the methods mentioned above any more (since each rule will have a different type and different settings). If you still call any of them, the set*ly methods will remove all rules and add one rule with the specified type. The add* and the other set* methods will change only the first recurrence rule, but leave the others untouched. */ class KCALCORE_EXPORT Recurrence : public RecurrenceRule::RuleObserver { public: class RecurrenceObserver { public: virtual ~RecurrenceObserver(); /** This method will be called on each change of the recurrence object */ virtual void recurrenceUpdated(Recurrence *r) = 0; }; /** enumeration for describing how an event recurs, if at all. */ enum { rNone = 0, rMinutely = 0x001, rHourly = 0x0002, rDaily = 0x0003, rWeekly = 0x0004, rMonthlyPos = 0x0005, rMonthlyDay = 0x0006, rYearlyMonth = 0x0007, rYearlyDay = 0x0008, rYearlyPos = 0x0009, rOther = 0x000A, rMax=0x00FF }; /** Constructs an empty recurrence. */ Recurrence(); /** Copy constructor. @param r instance to copy from */ Recurrence(const Recurrence &r); /** Destructor. */ ~Recurrence(); /** Comparison operator for equality. @param r instance to compare with @return true if recurrences are the same, false otherwise */ bool operator==(const Recurrence &r) const; /** Comparison operator for inequality. @param r instance to compare with @return true if recurrences are the different, false if the same */ bool operator!=(const Recurrence &r) const { return !operator==(r); } /** Assignment operator. @param r the recurrence which will be assigned to this. */ Recurrence &operator=(const Recurrence &r); /** Return the start date/time of the recurrence (Time for all-day recurrences will be 0:00). @return the current start/time of the recurrence. */ KDateTime startDateTime() const; /** Return the start date/time of the recurrence */ QDate startDate() const; /** Set start of recurrence. If @p start is date-only, the recurrence is set to all-day. Otherwise, the start is set to a date and time, and the recurrence is set to non-all-day. @param start the new start date or date/time of the recurrence. */ void setStartDateTime(const KDateTime &start); /** Set whether the recurrence has no time, just a date. * All-day means -- according to rfc2445 -- that the event has no time * associated. * N.B. This property is derived by default from whether setStartDateTime() is * called with a date-only or date/time parameter. * @return whether the recurrence has a time (false) or it is just a date (true). */ bool allDay() const; /** Sets whether the dtstart is a all-day (i.e. has no time attached) @param allDay If the recurrence is for all-day item (true) or has a time associated (false). */ void setAllDay(bool allDay); /** Set if recurrence is read-only or can be changed. */ void setRecurReadOnly(bool readOnly); /** Returns true if the recurrence is read-only, or false if it can be changed. */ bool recurReadOnly() const; /** Returns whether the event recurs at all. */ bool recurs() const; /** Returns the event's recurrence status. See the enumeration at the top * of this file for possible values. */ ushort recurrenceType() const; /** Returns the recurrence status for a recurrence rule. * See the enumeration at the top of this file for possible values. * * @param rrule the recurrence rule to get the type for */ static ushort recurrenceType(const RecurrenceRule *rrule); /** Returns true if the date specified is one on which the event will recur. @param date date to check. @param timeSpec time specification for @p date. */ bool recursOn(const QDate &date, const KDateTime::Spec &timeSpec) const; /** Returns true if the date/time specified is one at which the event will recur. Times are rounded down to the nearest minute to determine the result. @param dt is the date/time to check. */ bool recursAt(const KDateTime &dt) const; /** Removes all recurrence rules. Recurrence dates and exceptions are not removed. */ void unsetRecurs(); /** Removes all recurrence and exception rules and dates. */ void clear(); /** Returns a list of the times on the specified date at which the * recurrence will occur. The returned times should be interpreted in the * context of @p timeSpec. * @param date the date for which to find the recurrence times * @param timeSpec time specification for @p date */ TimeList recurTimesOn(const QDate &date, const KDateTime::Spec &timeSpec) const; /** Returns a list of all the times at which the recurrence will occur * between two specified times. * * There is a (large) maximum limit to the number of times returned. If due to * this limit the list is incomplete, this is indicated by the last entry being * set to an invalid KDateTime value. If you need further values, call the * method again with a start time set to just after the last valid time returned. * * @param start inclusive start of interval * @param end inclusive end of interval * @return list of date/time values */ DateTimeList timesInInterval(const KDateTime &start, const KDateTime &end) const; /** Returns the date and time of the next recurrence, after the specified date/time. * If the recurrence has no time, the next date after the specified date is returned. * @param preDateTime the date/time after which to find the recurrence. * @return date/time of next recurrence (strictly later than the given * KDateTime), or invalid date if none. */ KDateTime getNextDateTime(const KDateTime &preDateTime) const; /** Returns the date and time of the last previous recurrence, before the specified date/time. * If a time later than 00:00:00 is specified and the recurrence has no time, 00:00:00 on * the specified date is returned if that date recurs. * * @param afterDateTime the date/time before which to find the recurrence. * @return date/time of previous recurrence (strictly earlier than the given * KDateTime), or invalid date if none. */ KDateTime getPreviousDateTime(const KDateTime &afterDateTime) const; /** Returns frequency of recurrence, in terms of the recurrence time period type. */ int frequency() const; /** Sets the frequency of recurrence, in terms of the recurrence time period type. */ void setFrequency(int freq); /** * Returns -1 if the event recurs infinitely, 0 if the end date is set, * otherwise the total number of recurrences, including the initial occurrence. */ int duration() const; /** Sets the total number of times the event is to occur, including both the * first and last. */ void setDuration(int duration); /** Returns the number of recurrences up to and including the date/time specified. * @warning This function can be very time consuming - use it sparingly! */ int durationTo(const KDateTime &dt) const; /** Returns the number of recurrences up to and including the date specified. * @warning This function can be very time consuming - use it sparingly! */ int durationTo(const QDate &date) const; /** Returns the date/time of the last recurrence. * An invalid date is returned if the recurrence has no end. */ KDateTime endDateTime() const; /** Returns the date of the last recurrence. * An invalid date is returned if the recurrence has no end. */ QDate endDate() const; /** Sets the date of the last recurrence. The end time is set to the recurrence start time. * @param endDate the ending date after which to stop recurring. If the * recurrence is not all-day, the end time will be 23:59.*/ void setEndDate(const QDate &endDate); /** Sets the date and time of the last recurrence. * @param endDateTime the ending date/time after which to stop recurring. */ void setEndDateTime(const KDateTime &endDateTime); /** Shift the times of the recurrence so that they appear at the same clock time as before but in a new time zone. The shift is done from a viewing time zone rather than from the actual recurrence time zone. For example, shifting a recurrence whose start time is 09:00 America/New York, using an old viewing time zone (@p oldSpec) of Europe/London, to a new time zone (@p newSpec) of Europe/Paris, will result in the time being shifted from 14:00 (which is the London time of the recurrence start) to 14:00 Paris time. @param oldSpec the time specification which provides the clock times @param newSpec the new time specification */ void shiftTimes(const KDateTime::Spec &oldSpec, const KDateTime::Spec &newSpec); /** Sets an event to recur minutely. By default infinite recurrence is used. To set an end date use the method setEndDate and to set the number of occurrences use setDuration. This method clears all recurrence rules and adds one rule with a minutely recurrence. All other recurrence components (recurrence date/times, exception date/times and exception rules) are not modified. * @param freq the frequency to recur, e.g. 2 is every other minute */ void setMinutely(int freq); /** Sets an event to recur hourly. By default infinite recurrence is used. The minute of the recurrence is taken from the start date (if you need to change it, you will have to modify the defaultRRule's byMinute list manually. To set an end date use the method setEndDate and to set the number of occurrences use setDuration. This method clears all recurrence rules and adds one rule with a hourly recurrence. All other recurrence components (recurrence date/times, exception date/times and exception rules) are not modified. * @param freq the frequency to recur, e.g. 2 is every other hour */ void setHourly(int freq); /** Sets an event to recur daily. By default infinite recurrence is used. The minute and second of the recurrence is taken from the start date (if you need to change them, you will have to modify the defaultRRule's byMinute list manually. To set an end date use the method setEndDate and to set the number of occurrences use setDuration. This method clears all recurrence rules and adds one rule with a daily recurrence. All other recurrence components (recurrence date/times, exception date/times and exception rules) are not modified. * @param freq the frequency to recur, e.g. 2 is every other day */ void setDaily(int freq); /** Sets an event to recur weekly. By default infinite recurrence is used. To set an end date use the method setEndDate and to set the number of occurrences use setDuration. This method clears all recurrence rules and adds one rule with a weekly recurrence. All other recurrence components (recurrence date/times, exception date/times and exception rules) are not modified. * @param freq the frequency to recur, e.g. every other week etc. * @param weekStart the first day of the week (Monday=1 .. Sunday=7, default is Monday). */ void setWeekly(int freq, int weekStart = 1); /** Sets an event to recur weekly. By default infinite recurrence is used. To set an end date use the method setEndDate and to set the number of occurrences use setDuration. This method clears all recurrence rules and adds one rule with a weekly recurrence. All other recurrence components (recurrence date/times, exception date/times and exception rules) are not modified. * @param freq the frequency to recur, e.g. every other week etc. * @param days a 7 bit array indicating which days on which to recur (bit 0 = Monday). * @param weekStart the first day of the week (Monday=1 .. Sunday=7, default is Monday). */ void setWeekly(int freq, const QBitArray &days, int weekStart = 1); /** Adds days to the weekly day recurrence list. * @param days a 7 bit array indicating which days on which to recur (bit 0 = Monday). */ void addWeeklyDays(const QBitArray &days); /** Returns the first day of the week. Uses only the * first RRULE if present (i.e. a second RRULE as well as all EXRULES are * ignored! * @return Weekday of the first day of the week (Monday=1 .. Sunday=7) */ int weekStart() const; /** Returns week day mask (bit 0 = Monday). */ QBitArray days() const; // Emulate the old behavior /** Sets an event to recur monthly. By default infinite recurrence is used. The date of the monthly recurrence will be taken from the start date unless you explicitly add one or more recurrence dates with addMonthlyDate or a recurrence position in the month (e.g. first monday) using addMonthlyPos. To set an end date use the method setEndDate and to set the number of occurrences use setDuration. This method clears all recurrence rules and adds one rule with a monthly recurrence. All other recurrence components (recurrence date/times, exception date/times and exception rules) are not modified. * @param freq the frequency to recur, e.g. 3 for every third month. */ void setMonthly(int freq); /** Adds a position (e.g. first monday) to the monthly recurrence rule. * @param pos the position in the month for the recurrence, with valid * values being 1-5 (5 weeks max in a month). * @param days the days for the position to recur on (bit 0 = Monday). * Example: pos = 2, and bits 0 and 2 are set in days: * the rule is to repeat every 2nd Monday and Wednesday in the month. */ void addMonthlyPos(short pos, const QBitArray &days); void addMonthlyPos(short pos, ushort day); + void setMonthlyPos(const QList &monthlyDays); + /** Adds a date (e.g. the 15th of each month) to the monthly day * recurrence list. * @param day the date in the month to recur. */ void addMonthlyDate(short day); + void setMonthlyDate(const QList &monthlyDays); + /** Returns list of day positions in months. */ QList monthPositions() const; /** Returns list of day numbers of a month. */ // Emulate old behavior QList monthDays() const; /** Sets an event to recur yearly. By default, this will recur every year * on the same date (e.g. every year on April 15 if the start date was * April 15). * The day of the year can be specified with addYearlyDay(). * The day of the month can be specified with addYearlyByDate * If both a month and a day ar specified with addYearlyMonth and * addYearlyDay, the day is understood as day number within the month. * * A position (e.g. 3rd Sunday of year/month, or last Friday of year/month) * can be specified with addYearlyPos. Again, if a month is specified, * this position is understood as within that month, otherwise within * the year. * * By default infinite recurrence is used. To set an end date use the * method setEndDate and to set the number of occurrences use setDuration. This method clears all recurrence rules and adds one rule with a yearly recurrence. All other recurrence components (recurrence date/times, exception date/times and exception rules) are not modified. * @param freq the frequency to recur, e.g. 3 for every third year. */ void setYearly(int freq); /** Adds day number of year within a yearly recurrence. * By default infinite recurrence is used. To set an end date use the * method setEndDate and to set the number of occurrences use setDuration. * @param day the day of the year for the event. E.g. if day is 60, this * means Feb 29 in leap years and March 1 in non-leap years. */ void addYearlyDay(int day); + void setYearlyDay(const QList &days); + /** Adds date within a yearly recurrence. The month(s) for the recurrence * can be specified with addYearlyMonth(), otherwise the month of the * start date is used. * * By default infinite recurrence is used. To set an end date use the * method setEndDate and to set the number of occurrences use setDuration. * @param date the day of the month for the event */ void addYearlyDate(int date); + void setYearlyDate(const QList &dates); + /** Adds month in yearly recurrence. You can specify specific day numbers * within the months (by calling addYearlyDate()) or specific day positions * within the month (by calling addYearlyPos). * @param _rNum the month in which the event shall recur. */ void addYearlyMonth(short _rNum); + void setYearlyMonth(const QList< int > &months); + /** Adds position within month/year within a yearly recurrence. If months * are specified (via addYearlyMonth()), the parameters are understood as * position within these months, otherwise within the year. * * By default infinite recurrence is used. * To set an end date use the method setEndDate and to set the number * of occurrences use setDuration. * @param pos the position in the month/year for the recurrence, with valid * values being 1 to 53 and -1 to -53 (53 weeks max in a year). * @param days the days for the position to recur on (bit 0 = Monday). * Example: pos = 2, and bits 0 and 2 are set in days * If months are specified (via addYearlyMonth), e.g. March, the rule is * to repeat every year on the 2nd Monday and Wednesday of March. * If no months are specified, the fule is to repeat every year on the * 2nd Monday and Wednesday of the year. */ void addYearlyPos(short pos, const QBitArray &days); + void setYearlyPos(QList & days); + /** Returns the day numbers within a yearly recurrence. * @return the days of the year for the event. E.g. if the list contains * 60, this means the recurrence happens on day 60 of the year, i.e. * on Feb 29 in leap years and March 1 in non-leap years. */ QList yearDays() const; /** Returns the dates within a yearly recurrence. * @return the days of the month for the event. E.g. if the list contains * 13, this means the recurrence happens on the 13th of the month. * The months for the recurrence can be obtained through * yearlyMonths(). If this list is empty, the month of the start * date is used. */ QList yearDates() const; /** Returns the months within a yearly recurrence. * @return the months for the event. E.g. if the list contains * 11, this means the recurrence happens in November. * The days for the recurrence can be obtained either through * yearDates() if they are given as dates within the month or * through yearlyPositions() if they are given as positions within the * month. If none is specified, the date of the start date is used. */ QList yearMonths() const; /** Returns the positions within a yearly recurrence. * @return the positions for the event, either within a month (if months * are set through addYearlyMonth()) or within the year. * E.g. if the list contains {Pos=3, Day=5}, this means the third * friday. If a month is set this position is understoodas third * Friday in the given months, otherwise as third Friday of the * year. */ /** Returns list of day positions in months, for a recursYearlyPos recurrence rule. */ QList yearPositions() const; /** Upper date limit for recurrences */ static const QDate MAX_DATE; /** Debug output. */ void dump() const; // RRULE RecurrenceRule::List rRules() const; /** Add a recurrence rule to the recurrence. @param rrule the recurrence rule to add */ void addRRule(RecurrenceRule *rrule); /** Remove a recurrence rule from the recurrence. @p rrule is not deleted; it is the responsibility of the caller to ensure that it is deleted. @param rrule the recurrence rule to remove */ void removeRRule(RecurrenceRule *rrule); /** Remove a recurrence rule from the recurrence and delete it. @param rrule the recurrence rule to remove */ void deleteRRule(RecurrenceRule *rrule); // EXRULE RecurrenceRule::List exRules() const; /** Add an exception rule to the recurrence. @param exrule the exception rule to add */ void addExRule(RecurrenceRule *exrule); /** Remove an exception rule from the recurrence. @p exrule is not deleted; it is the responsibility of the caller to ensure that it is deleted. @param exrule the exception rule to remove */ void removeExRule(RecurrenceRule *exrule); /** Remove an exception rule from the recurrence and delete it. @param exrule the exception rule to remove */ void deleteExRule(RecurrenceRule *exrule); // RDATE DateTimeList rDateTimes() const; DateList rDates() const; void setRDateTimes(const DateTimeList &rdates); void setRDates(const DateList &rdates); void addRDateTime(const KDateTime &rdate); void addRDate(const QDate &rdate); // ExDATE DateTimeList exDateTimes() const; DateList exDates() const; void setExDateTimes(const DateTimeList &exdates); void setExDates(const DateList &exdates); void addExDateTime(const KDateTime &exdate); void addExDate(const QDate &exdate); RecurrenceRule *defaultRRule(bool create = false) const; RecurrenceRule *defaultRRuleConst() const; void updated(); /** Installs an observer. Whenever some setting of this recurrence object is changed, the recurrenceUpdated( Recurrence* ) method of each observer will be called to inform it of changes. @param observer the Recurrence::Observer-derived object, which will be installed as an observer of this object. */ void addObserver(RecurrenceObserver *observer); /** Removes an observer that was added with addObserver. If the given object was not an observer, it does nothing. @param observer the Recurrence::Observer-derived object to be removed from the list of observers of this object. */ void removeObserver(RecurrenceObserver *observer); void recurrenceChanged(RecurrenceRule *); protected: RecurrenceRule *setNewRecurrenceType(RecurrenceRule::PeriodType type, int freq); private: //@cond PRIVATE class Private; Private *const d; //@endcond friend KCALCORE_EXPORT QDataStream& operator<<(QDataStream &out, KCalCore::Recurrence *); friend KCALCORE_EXPORT QDataStream& operator>>(QDataStream &in, KCalCore::Recurrence *); }; /** * Recurrence serializer and deserializer. * @since 4.12 */ KCALCORE_EXPORT QDataStream& operator<<(QDataStream &out, KCalCore::Recurrence *); KCALCORE_EXPORT QDataStream& operator>>(QDataStream &in, KCalCore::Recurrence *); } #endif diff --git a/kcalcore/recurrencerule.cpp b/kcalcore/recurrencerule.cpp index 40f63c1fe..553799ccf 100644 --- a/kcalcore/recurrencerule.cpp +++ b/kcalcore/recurrencerule.cpp @@ -1,2333 +1,2335 @@ /* This file is part of the kcalcore library. Copyright (c) 2005 Reinhold Kainhofer Copyright (c) 2006-2008 David Jarvie This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "recurrencerule.h" #include #include #include using namespace KCalCore; // Maximum number of intervals to process const int LOOP_LIMIT = 10000; static QString dumpTime(const KDateTime &dt); // for debugging /*========================================================================= = = = IMPORTANT CODING NOTE: = = = = Recurrence handling code is time critical, especially for sub-daily = = recurrences. For example, if getNextDate() is called repeatedly to = = check all consecutive occurrences over a few years, on a slow machine = = this could take many seconds to complete in the worst case. Simple = = sub-daily recurrences are optimised by use of mTimedRepetition. = = = ==========================================================================*/ /************************************************************************** * DateHelper * **************************************************************************/ //@cond PRIVATE class DateHelper { public: #ifndef NDEBUG static QString dayName(short day); #endif static QDate getNthWeek(int year, int weeknumber, short weekstart = 1); static int weekNumbersInYear(int year, short weekstart = 1); static int getWeekNumber(const QDate &date, short weekstart, int *year = 0); static int getWeekNumberNeg(const QDate &date, short weekstart, int *year = 0); // Convert to QDate, allowing for day < 0. // month and day must be non-zero. static QDate getDate(int year, int month, int day) { if (day >= 0) { return QDate(year, month, day); } else { if (++month > 12) { month = 1; ++year; } return QDate(year, month, 1).addDays(day); } } }; #ifndef NDEBUG // TODO: Move to a general library / class, as we need the same in the iCal // generator and in the xcal format QString DateHelper::dayName(short day) { switch (day) { case 1: return QLatin1String("MO"); case 2: return QLatin1String("TU"); case 3: return QLatin1String("WE"); case 4: return QLatin1String("TH"); case 5: return QLatin1String("FR"); case 6: return QLatin1String("SA"); case 7: return QLatin1String("SU"); default: return QLatin1String("??"); } } #endif QDate DateHelper::getNthWeek(int year, int weeknumber, short weekstart) { if (weeknumber == 0) { return QDate(); } // Adjust this to the first day of week #1 of the year and add 7*weekno days. QDate dt(year, 1, 4); // Week #1 is the week that contains Jan 4 int adjust = -(7 + dt.dayOfWeek() - weekstart) % 7; if (weeknumber > 0) { dt = dt.addDays(7 * (weeknumber-1) + adjust); } else if (weeknumber < 0) { dt = dt.addYears(1); dt = dt.addDays(7 * weeknumber + adjust); } return dt; } int DateHelper::getWeekNumber(const QDate &date, short weekstart, int *year) { int y = date.year(); QDate dt(y, 1, 4); // <= definitely in week #1 dt = dt.addDays(-(7 + dt.dayOfWeek() - weekstart) % 7); // begin of week #1 int daysto = dt.daysTo(date); if (daysto < 0) { // in first week of year --y; dt = QDate(y, 1, 4); dt = dt.addDays(-(7 + dt.dayOfWeek() - weekstart) % 7); // begin of week #1 daysto = dt.daysTo(date); } else if (daysto > 355) { // near the end of the year - check if it's next year QDate dtn(y+1, 1, 4); // <= definitely first week of next year dtn = dtn.addDays(-(7 + dtn.dayOfWeek() - weekstart) % 7); int dayston = dtn.daysTo(date); if (dayston >= 0) { // in first week of next year; ++y; daysto = dayston; } } if (year) { *year = y; } return daysto / 7 + 1; } int DateHelper::weekNumbersInYear(int year, short weekstart) { QDate dt(year, 1, weekstart); QDate dt1(year + 1, 1, weekstart); return dt.daysTo(dt1) / 7; } // Week number from the end of the year int DateHelper::getWeekNumberNeg(const QDate &date, short weekstart, int *year) { int weekpos = getWeekNumber(date, weekstart, year); return weekNumbersInYear(*year, weekstart) - weekpos - 1; } //@endcond /************************************************************************** * WDayPos * **************************************************************************/ bool RecurrenceRule::WDayPos::operator==(const RecurrenceRule::WDayPos &pos2) const { return mDay == pos2.mDay && mPos == pos2.mPos; } bool RecurrenceRule::WDayPos::operator!=(const RecurrenceRule::WDayPos &pos2) const { return !operator==(pos2); } /************************************************************************** * Constraint * **************************************************************************/ //@cond PRIVATE class Constraint { public: typedef QList List; Constraint() {} explicit Constraint(KDateTime::Spec, int wkst = 1); Constraint(const KDateTime &dt, RecurrenceRule::PeriodType type, int wkst); void clear(); void setYear(int n) { year = n; useCachedDt = false; } void setMonth(int n) { month = n; useCachedDt = false; } void setDay(int n) { day = n; useCachedDt = false; } void setHour(int n) { hour = n; useCachedDt = false; } void setMinute(int n) { minute = n; useCachedDt = false; } void setSecond(int n) { second = n; useCachedDt = false; } void setWeekday(int n) { weekday = n; useCachedDt = false; } void setWeekdaynr(int n) { weekdaynr = n; useCachedDt = false; } void setWeeknumber(int n) { weeknumber = n; useCachedDt = false; } void setYearday(int n) { yearday = n; useCachedDt = false; } void setWeekstart(int n) { weekstart = n; useCachedDt = false; } void setSecondOccurrence(int n) { secondOccurrence = n; useCachedDt = false; } int year; // 0 means unspecified int month; // 0 means unspecified int day; // 0 means unspecified int hour; // -1 means unspecified int minute; // -1 means unspecified int second; // -1 means unspecified int weekday; // 0 means unspecified int weekdaynr; // index of weekday in month/year (0=unspecified) int weeknumber; // 0 means unspecified int yearday; // 0 means unspecified int weekstart; // first day of week (1=monday, 7=sunday, 0=unspec.) KDateTime::Spec timespec; // time zone etc. to use bool secondOccurrence; // the time is the second occurrence during daylight savings shift bool readDateTime(const KDateTime &dt, RecurrenceRule::PeriodType type); bool matches(const QDate &dt, RecurrenceRule::PeriodType type) const; bool matches(const KDateTime &dt, RecurrenceRule::PeriodType type) const; bool merge(const Constraint &interval); bool isConsistent() const; bool isConsistent(RecurrenceRule::PeriodType period) const; bool increase(RecurrenceRule::PeriodType type, int freq); KDateTime intervalDateTime(RecurrenceRule::PeriodType type) const; QList dateTimes(RecurrenceRule::PeriodType type) const; void appendDateTime(const QDate &date, const QTime &time, QList &list) const; void dump() const; private: mutable bool useCachedDt; mutable KDateTime cachedDt; }; Constraint::Constraint(KDateTime::Spec spec, int wkst) : weekstart(wkst), timespec(spec) { clear(); } Constraint::Constraint(const KDateTime &dt, RecurrenceRule::PeriodType type, int wkst) : weekstart(wkst), timespec(dt.timeSpec()) { clear(); readDateTime(dt, type); } void Constraint::clear() { year = 0; month = 0; day = 0; hour = -1; minute = -1; second = -1; weekday = 0; weekdaynr = 0; weeknumber = 0; yearday = 0; secondOccurrence = false; useCachedDt = false; } bool Constraint::matches(const QDate &dt, RecurrenceRule::PeriodType type) const { // If the event recurs in week 53 or 1, the day might not belong to the same // year as the week it is in. E.g. Jan 1, 2005 is in week 53 of year 2004. // So we can't simply check the year in that case! if (weeknumber == 0) { if (year > 0 && year != dt.year()) { return false; } } else { int y; if (weeknumber > 0 && weeknumber != DateHelper::getWeekNumber(dt, weekstart, &y)) { return false; } if (weeknumber < 0 && weeknumber != DateHelper::getWeekNumberNeg(dt, weekstart, &y)) { return false; } if (year > 0 && year != y) { return false; } } if (month > 0 && month != dt.month()) { return false; } if (day > 0 && day != dt.day()) { return false; } if (day < 0 && dt.day() != (dt.daysInMonth() + day + 1)) { return false; } if (weekday > 0) { if (weekday != dt.dayOfWeek()) { return false; } if (weekdaynr != 0) { // If it's a yearly recurrence and a month is given, the position is // still in the month, not in the year. if ((type == RecurrenceRule::rMonthly) || (type == RecurrenceRule::rYearly && month > 0)) { // Monthly if (weekdaynr > 0 && weekdaynr != (dt.day() - 1) / 7 + 1) { return false; } if (weekdaynr < 0 && weekdaynr != -((dt.daysInMonth() - dt.day()) / 7 + 1)) { return false; } } else { // Yearly if (weekdaynr > 0 && weekdaynr != (dt.dayOfYear() - 1) / 7 + 1) { return false; } if (weekdaynr < 0 && weekdaynr != -((dt.daysInYear() - dt.dayOfYear()) / 7 + 1)) { return false; } } } } if (yearday > 0 && yearday != dt.dayOfYear()) { return false; } if (yearday < 0 && yearday != dt.daysInYear() - dt.dayOfYear() + 1) { return false; } return true; } /* Check for a match with the specified date/time. * The date/time's time specification must correspond with that of the start date/time. */ bool Constraint::matches(const KDateTime &dt, RecurrenceRule::PeriodType type) const { if ((hour >= 0 && (hour != dt.time().hour() || secondOccurrence != dt.isSecondOccurrence())) || (minute >= 0 && minute != dt.time().minute()) || (second >= 0 && second != dt.time().second()) || !matches(dt.date(), type)) { return false; } return true; } bool Constraint::isConsistent(RecurrenceRule::PeriodType /*period*/) const { // TODO: Check for consistency, e.g. byyearday=3 and bymonth=10 return true; } // Return a date/time set to the constraint values, but with those parts less // significant than the given period type set to 1 (for dates) or 0 (for times). KDateTime Constraint::intervalDateTime(RecurrenceRule::PeriodType type) const { if (useCachedDt) { return cachedDt; } QDate d; QTime t(0, 0, 0); bool subdaily = true; switch (type) { case RecurrenceRule::rSecondly: t.setHMS(hour, minute, second); break; case RecurrenceRule::rMinutely: t.setHMS(hour, minute, 0); break; case RecurrenceRule::rHourly: t.setHMS(hour, 0, 0); break; case RecurrenceRule::rDaily: break; case RecurrenceRule::rWeekly: d = DateHelper::getNthWeek(year, weeknumber, weekstart); subdaily = false; break; case RecurrenceRule::rMonthly: d.setYMD(year, month, 1); subdaily = false; break; case RecurrenceRule::rYearly: d.setYMD(year, 1, 1); subdaily = false; break; default: break; } if (subdaily) { d = DateHelper::getDate(year, (month>0)?month:1, day?day:1); } cachedDt = KDateTime(d, t, timespec); if (secondOccurrence) { cachedDt.setSecondOccurrence(true); } useCachedDt = true; return cachedDt; } bool Constraint::merge(const Constraint &interval) { #define mergeConstraint( name, cmparison ) \ if ( interval.name cmparison ) { \ if ( !( name cmparison ) ) { \ name = interval.name; \ } else if ( name != interval.name ) { \ return false;\ } \ } useCachedDt = false; mergeConstraint(year, > 0); mergeConstraint(month, > 0); mergeConstraint(day, != 0); mergeConstraint(hour, >= 0); mergeConstraint(minute, >= 0); mergeConstraint(second, >= 0); mergeConstraint(weekday, != 0); mergeConstraint(weekdaynr, != 0); mergeConstraint(weeknumber, != 0); mergeConstraint(yearday, != 0); #undef mergeConstraint return true; } // Y M D | H Mn S | WD #WD | WN | YD // required: // x | x x x | | | // 0) Trivial: Exact date given, maybe other restrictions // x x x | x x x | | | // 1) Easy case: no weekly restrictions -> at most a loop through possible dates // x + + | x x x | - - | - | - // 2) Year day is given -> date known // x | x x x | | | + // 3) week number is given -> loop through all days of that week. Further // restrictions will be applied in the end, when we check all dates for // consistency with the constraints // x | x x x | | + | (-) // 4) week day is specified -> // x | x x x | x ? | (-)| (-) // 5) All possiblecases have already been treated, so this must be an error! QList Constraint::dateTimes(RecurrenceRule::PeriodType type) const { QList result; bool done = false; if (!isConsistent(type)) { return result; } // TODO_Recurrence: Handle all-day QTime tm(hour, minute, second); if (!done && day && month > 0) { appendDateTime(DateHelper::getDate(year, month, day), tm, result); done = true; } if (!done && weekday == 0 && weeknumber == 0 && yearday == 0) { // Easy case: date is given, not restrictions by week or yearday uint mstart = (month > 0) ? month : 1; uint mend = (month <= 0) ? 12 : month; for (uint m = mstart; m <= mend; ++m) { uint dstart, dend; if (day > 0) { dstart = dend = day; } else if (day < 0) { QDate date(year, month, 1); dstart = dend = date.daysInMonth() + day + 1; } else { QDate date(year, month, 1); dstart = 1; dend = date.daysInMonth(); } uint d = dstart; for (QDate dt(year, m, dstart); ; dt = dt.addDays(1)) { appendDateTime(dt, tm, result); if (++d > dend) { break; } } } done = true; } // Else: At least one of the week / yearday restrictions was given... // If we have a yearday (and of course a year), we know the exact date if (!done && yearday != 0) { // yearday < 0 means from end of year, so we'll need Jan 1 of the next year QDate d(year + ((yearday > 0) ? 0 : 1), 1, 1); d = d.addDays(yearday - ((yearday > 0) ? 1 : 0)); appendDateTime(d, tm, result); done = true; } // Else: If we have a weeknumber, we have at most 7 possible dates, loop through them if (!done && weeknumber != 0) { QDate wst(DateHelper::getNthWeek(year, weeknumber, weekstart)); if (weekday != 0) { wst = wst.addDays((7 + weekday - weekstart) % 7); appendDateTime(wst, tm, result); } else { for (int i = 0; i < 7; ++i) { appendDateTime(wst, tm, result); wst = wst.addDays(1); } } done = true; } // weekday is given if (!done && weekday != 0) { QDate dt(year, 1, 1); // If type == yearly and month is given, pos is still in month not year! // TODO_Recurrence: Correct handling of n-th BYDAY... int maxloop = 53; bool inMonth = (type == RecurrenceRule::rMonthly) || (type == RecurrenceRule::rYearly && month > 0); if (inMonth && month > 0) { dt = QDate(year, month, 1); maxloop = 5; } if (weekdaynr < 0) { // From end of period (month, year) => relative to begin of next period if (inMonth) { dt = dt.addMonths(1); } else { dt = dt.addYears(1); } } int adj = (7 + weekday - dt.dayOfWeek()) % 7; dt = dt.addDays(adj); // correct first weekday of the period if (weekdaynr > 0) { dt = dt.addDays((weekdaynr - 1) * 7); appendDateTime(dt, tm, result); } else if (weekdaynr < 0) { dt = dt.addDays(weekdaynr * 7); appendDateTime(dt, tm, result); } else { // loop through all possible weeks, non-matching will be filtered later for (int i = 0; i < maxloop; ++i) { appendDateTime(dt, tm, result); dt = dt.addDays(7); } } } // weekday != 0 // Only use those times that really match all other constraints, too QList valid; for (int i = 0, iend = result.count(); i < iend; ++i) { if (matches(result[i], type)) { valid.append(result[i]); } } // Don't sort it here, would be unnecessary work. The results from all // constraints will be merged to one big list of the interval. Sort that one! return valid; } void Constraint::appendDateTime(const QDate &date, const QTime &time, QList &list) const { KDateTime dt(date, time, timespec); if (dt.isValid()) { if (secondOccurrence) { dt.setSecondOccurrence(true); } list.append(dt); } } bool Constraint::increase(RecurrenceRule::PeriodType type, int freq) { // convert the first day of the interval to KDateTime intervalDateTime(type); // Now add the intervals switch (type) { case RecurrenceRule::rSecondly: cachedDt = cachedDt.addSecs(freq); break; case RecurrenceRule::rMinutely: cachedDt = cachedDt.addSecs(60 * freq); break; case RecurrenceRule::rHourly: cachedDt = cachedDt.addSecs(3600 * freq); break; case RecurrenceRule::rDaily: cachedDt = cachedDt.addDays(freq); break; case RecurrenceRule::rWeekly: cachedDt = cachedDt.addDays(7 * freq); break; case RecurrenceRule::rMonthly: cachedDt = cachedDt.addMonths(freq); break; case RecurrenceRule::rYearly: cachedDt = cachedDt.addYears(freq); break; default: break; } // Convert back from KDateTime to the Constraint class readDateTime(cachedDt, type); useCachedDt = true; // readDateTime() resets this return true; } // Set the constraint's value appropriate to 'type', to the value contained in a date/time. bool Constraint::readDateTime(const KDateTime &dt, RecurrenceRule::PeriodType type) { switch (type) { // Really fall through! Only weekly needs to be treated differently! case RecurrenceRule::rSecondly: second = dt.time().second(); case RecurrenceRule::rMinutely: minute = dt.time().minute(); case RecurrenceRule::rHourly: hour = dt.time().hour(); secondOccurrence = dt.isSecondOccurrence(); case RecurrenceRule::rDaily: day = dt.date().day(); case RecurrenceRule::rMonthly: month = dt.date().month(); case RecurrenceRule::rYearly: year = dt.date().year(); break; case RecurrenceRule::rWeekly: // Determine start day of the current week, calculate the week number from that weeknumber = DateHelper::getWeekNumber(dt.date(), weekstart, &year); break; default: break; } useCachedDt = false; return true; } //@endcond /************************************************************************** * RecurrenceRule::Private * **************************************************************************/ //@cond PRIVATE class KCalCore::RecurrenceRule::Private { public: Private(RecurrenceRule *parent) : mParent(parent), mPeriod(rNone), mFrequency(0), mDuration(-1), mWeekStart(1), mIsReadOnly(false), mAllDay(false) { setDirty(); } Private(RecurrenceRule *parent, const Private &p); Private &operator=(const Private &other); bool operator==(const Private &other) const; void clear(); void setDirty(); void buildConstraints(); bool buildCache() const; Constraint getNextValidDateInterval(const KDateTime &preDate, PeriodType type) const; Constraint getPreviousValidDateInterval(const KDateTime &afterDate, PeriodType type) const; DateTimeList datesForInterval(const Constraint &interval, PeriodType type) const; RecurrenceRule *mParent; QString mRRule; // RRULE string PeriodType mPeriod; KDateTime mDateStart; // start of recurrence (but mDateStart is not an occurrence // unless it matches the rule) uint mFrequency; /** how often it recurs: < 0 means no end date, 0 means an explicit end date, positive values give the number of occurrences */ int mDuration; KDateTime mDateEnd; QList mBySeconds; // values: second 0-59 QList mByMinutes; // values: minute 0-59 QList mByHours; // values: hour 0-23 QList mByDays; // n-th weekday of the month or year QList mByMonthDays; // values: day -31 to -1 and 1-31 QList mByYearDays; // values: day -366 to -1 and 1-366 QList mByWeekNumbers; // values: week -53 to -1 and 1-53 QList mByMonths; // values: month 1-12 QList mBySetPos; // values: position -366 to -1 and 1-366 short mWeekStart; // first day of the week (1=Monday, 7=Sunday) Constraint::List mConstraints; QList mObservers; // Cache for duration mutable DateTimeList mCachedDates; mutable KDateTime mCachedDateEnd; mutable KDateTime mCachedLastDate; // when mCachedDateEnd invalid, last date checked mutable bool mCached; bool mIsReadOnly; bool mAllDay; bool mNoByRules; // no BySeconds, ByMinutes, ... rules exist uint mTimedRepetition; // repeats at a regular number of seconds interval, or 0 }; RecurrenceRule::Private::Private(RecurrenceRule *parent, const Private &p) : mParent(parent), mRRule(p.mRRule), mPeriod(p.mPeriod), mDateStart(p.mDateStart), mFrequency(p.mFrequency), mDuration(p.mDuration), mDateEnd(p.mDateEnd), mBySeconds(p.mBySeconds), mByMinutes(p.mByMinutes), mByHours(p.mByHours), mByDays(p.mByDays), mByMonthDays(p.mByMonthDays), mByYearDays(p.mByYearDays), mByWeekNumbers(p.mByWeekNumbers), mByMonths(p.mByMonths), mBySetPos(p.mBySetPos), mWeekStart(p.mWeekStart), mIsReadOnly(p.mIsReadOnly), mAllDay(p.mAllDay), mNoByRules(p.mNoByRules) { setDirty(); } RecurrenceRule::Private &RecurrenceRule::Private::operator=(const Private &p) { // check for self assignment if (&p == this) { return *this; } mRRule = p.mRRule; mPeriod = p.mPeriod; mDateStart = p.mDateStart; mFrequency = p.mFrequency; mDuration = p.mDuration; mDateEnd = p.mDateEnd; mBySeconds = p.mBySeconds; mByMinutes = p.mByMinutes; mByHours = p.mByHours; mByDays = p.mByDays; mByMonthDays = p.mByMonthDays; mByYearDays = p.mByYearDays; mByWeekNumbers = p.mByWeekNumbers; mByMonths = p.mByMonths; mBySetPos = p.mBySetPos; mWeekStart = p.mWeekStart; mIsReadOnly = p.mIsReadOnly; mAllDay = p.mAllDay; mNoByRules = p.mNoByRules; setDirty(); return *this; } bool RecurrenceRule::Private::operator==(const Private &r) const { return mPeriod == r.mPeriod && ((mDateStart == r.mDateStart) || (!mDateStart.isValid() && !r.mDateStart.isValid())) && mDuration == r.mDuration && ((mDateEnd == r.mDateEnd) || (!mDateEnd.isValid() && !r.mDateEnd.isValid())) && mFrequency == r.mFrequency && mIsReadOnly == r.mIsReadOnly && mAllDay == r.mAllDay && mBySeconds == r.mBySeconds && mByMinutes == r.mByMinutes && mByHours == r.mByHours && mByDays == r.mByDays && mByMonthDays == r.mByMonthDays && mByYearDays == r.mByYearDays && mByWeekNumbers == r.mByWeekNumbers && mByMonths == r.mByMonths && mBySetPos == r.mBySetPos && mWeekStart == r.mWeekStart && mNoByRules == r.mNoByRules; } void RecurrenceRule::Private::clear() { if (mIsReadOnly) { return; } mPeriod = rNone; mBySeconds.clear(); mByMinutes.clear(); mByHours.clear(); mByDays.clear(); mByMonthDays.clear(); mByYearDays.clear(); mByWeekNumbers.clear(); mByMonths.clear(); mBySetPos.clear(); mWeekStart = 1; mNoByRules = false; setDirty(); } void RecurrenceRule::Private::setDirty() { buildConstraints(); mCached = false; mCachedDates.clear(); for (int i = 0, iend = mObservers.count(); i < iend; ++i) { if (mObservers[i]) { mObservers[i]->recurrenceChanged(mParent); } } } //@endcond /************************************************************************** * RecurrenceRule * **************************************************************************/ RecurrenceRule::RecurrenceRule() : d(new Private(this)) { } RecurrenceRule::RecurrenceRule(const RecurrenceRule &r) : d(new Private(this, *r.d)) { } RecurrenceRule::~RecurrenceRule() { delete d; } bool RecurrenceRule::operator==(const RecurrenceRule &r) const { return *d == *r.d; } RecurrenceRule &RecurrenceRule::operator=(const RecurrenceRule &r) { // check for self assignment if (&r == this) { return *this; } *d = *r.d; return *this; } void RecurrenceRule::addObserver(RuleObserver *observer) { if (!d->mObservers.contains(observer)) { d->mObservers.append(observer); } } void RecurrenceRule::removeObserver(RuleObserver *observer) { if (d->mObservers.contains(observer)) { d->mObservers.removeAll(observer); } } void RecurrenceRule::setRecurrenceType(PeriodType period) { if (isReadOnly()) { return; } d->mPeriod = period; d->setDirty(); } KDateTime RecurrenceRule::endDt(bool *result) const { if (result) { *result = false; } if (d->mPeriod == rNone) { return KDateTime(); } if (d->mDuration < 0) { return KDateTime(); } if (d->mDuration == 0) { if (result) { *result = true; } return d->mDateEnd; } // N occurrences. Check if we have a full cache. If so, return the cached end date. if (!d->mCached) { // If not enough occurrences can be found (i.e. inconsistent constraints) if (!d->buildCache()) { return KDateTime(); } } if (result) { *result = true; } return d->mCachedDateEnd; } void RecurrenceRule::setEndDt(const KDateTime &dateTime) { if (isReadOnly()) { return; } d->mDateEnd = dateTime; - d->mDuration = 0; // set to 0 because there is an end date/time + if (d->mDateEnd.isValid()) { + d->mDuration = 0; // set to 0 because there is an end date/time + } d->setDirty(); } void RecurrenceRule::setDuration(int duration) { if (isReadOnly()) { return; } d->mDuration = duration; d->setDirty(); } void RecurrenceRule::setAllDay(bool allDay) { if (isReadOnly()) { return; } d->mAllDay = allDay; d->setDirty(); } void RecurrenceRule::clear() { d->clear(); } void RecurrenceRule::setDirty() { d->setDirty(); } void RecurrenceRule::setStartDt(const KDateTime &start) { if (isReadOnly()) { return; } d->mDateStart = start; d->setDirty(); } void RecurrenceRule::setFrequency(int freq) { if (isReadOnly() || freq <= 0) { return; } d->mFrequency = freq; d->setDirty(); } void RecurrenceRule::setBySeconds(const QList &bySeconds) { if (isReadOnly()) { return; } d->mBySeconds = bySeconds; d->setDirty(); } void RecurrenceRule::setByMinutes(const QList &byMinutes) { if (isReadOnly()) { return; } d->mByMinutes = byMinutes; d->setDirty(); } void RecurrenceRule::setByHours(const QList &byHours) { if (isReadOnly()) { return; } d->mByHours = byHours; d->setDirty(); } void RecurrenceRule::setByDays(const QList &byDays) { if (isReadOnly()) { return; } d->mByDays = byDays; d->setDirty(); } void RecurrenceRule::setByMonthDays(const QList &byMonthDays) { if (isReadOnly()) { return; } d->mByMonthDays = byMonthDays; d->setDirty(); } void RecurrenceRule::setByYearDays(const QList &byYearDays) { if (isReadOnly()) { return; } d->mByYearDays = byYearDays; d->setDirty(); } void RecurrenceRule::setByWeekNumbers(const QList &byWeekNumbers) { if (isReadOnly()) { return; } d->mByWeekNumbers = byWeekNumbers; d->setDirty(); } void RecurrenceRule::setByMonths(const QList &byMonths) { if (isReadOnly()) { return; } d->mByMonths = byMonths; d->setDirty(); } void RecurrenceRule::setBySetPos(const QList &bySetPos) { if (isReadOnly()) { return; } d->mBySetPos = bySetPos; d->setDirty(); } void RecurrenceRule::setWeekStart(short weekStart) { if (isReadOnly()) { return; } d->mWeekStart = weekStart; d->setDirty(); } void RecurrenceRule::shiftTimes(const KDateTime::Spec &oldSpec, const KDateTime::Spec &newSpec) { d->mDateStart = d->mDateStart.toTimeSpec(oldSpec); d->mDateStart.setTimeSpec(newSpec); if (d->mDuration == 0) { d->mDateEnd = d->mDateEnd.toTimeSpec(oldSpec); d->mDateEnd.setTimeSpec(newSpec); } d->setDirty(); } // Taken from recurrence.cpp // int RecurrenceRule::maxIterations() const // { // /* Find the maximum number of iterations which may be needed to reach the // * next actual occurrence of a monthly or yearly recurrence. // * More than one iteration may be needed if, for example, it's the 29th February, // * the 31st day of the month or the 5th Monday, and the month being checked is // * February or a 30-day month. // * The following recurrences may never occur: // * - For rMonthlyDay: if the frequency is a whole number of years. // * - For rMonthlyPos: if the frequency is an even whole number of years. // * - For rYearlyDay, rYearlyMonth: if the frequeny is a multiple of 4 years. // * - For rYearlyPos: if the frequency is an even number of years. // * The maximum number of iterations needed, assuming that it does actually occur, // * was found empirically. // */ // switch (recurs) { // case rMonthlyDay: // return (rFreq % 12) ? 6 : 8; // // case rMonthlyPos: // if (rFreq % 12 == 0) { // // Some of these frequencies may never occur // return (rFreq % 84 == 0) ? 364 // frequency = multiple of 7 years // : (rFreq % 48 == 0) ? 7 // frequency = multiple of 4 years // : (rFreq % 24 == 0) ? 14 : 28; // frequency = multiple of 2 or 1 year // } // // All other frequencies will occur sometime // if (rFreq > 120) // return 364; // frequencies of > 10 years will hit the date limit first // switch (rFreq) { // case 23: return 50; // case 46: return 38; // case 56: return 138; // case 66: return 36; // case 89: return 54; // case 112: return 253; // default: return 25; // most frequencies will need < 25 iterations // } // // case rYearlyMonth: // case rYearlyDay: // return 8; // only 29th Feb or day 366 will need more than one iteration // // case rYearlyPos: // if (rFreq % 7 == 0) // return 364; // frequencies of a multiple of 7 years will hit the date limit first // if (rFreq % 2 == 0) { // // Some of these frequencies may never occur // return (rFreq % 4 == 0) ? 7 : 14; // frequency = even number of years // } // return 28; // } // return 1; // } //@cond PRIVATE void RecurrenceRule::Private::buildConstraints() { mTimedRepetition = 0; mNoByRules = mBySetPos.isEmpty(); mConstraints.clear(); Constraint con(mDateStart.timeSpec()); if (mWeekStart > 0) { con.setWeekstart(mWeekStart); } mConstraints.append(con); int c, cend; int i, iend; Constraint::List tmp; #define intConstraint( list, setElement ) \ if ( !list.isEmpty() ) { \ mNoByRules = false; \ iend = list.count(); \ if ( iend == 1 ) { \ for ( c = 0, cend = mConstraints.count(); c < cend; ++c ) { \ mConstraints[c].setElement( list[0] ); \ } \ } else { \ for ( c = 0, cend = mConstraints.count(); c < cend; ++c ) { \ for ( i = 0; i < iend; ++i ) { \ con = mConstraints[c]; \ con.setElement( list[i] ); \ tmp.append( con ); \ } \ } \ mConstraints = tmp; \ tmp.clear(); \ } \ } intConstraint(mBySeconds, setSecond); intConstraint(mByMinutes, setMinute); intConstraint(mByHours, setHour); intConstraint(mByMonthDays, setDay); intConstraint(mByMonths, setMonth); intConstraint(mByYearDays, setYearday); intConstraint(mByWeekNumbers, setWeeknumber); #undef intConstraint if (!mByDays.isEmpty()) { mNoByRules = false; for (c = 0, cend = mConstraints.count(); c < cend; ++c) { for (i = 0, iend = mByDays.count(); i < iend; ++i) { con = mConstraints[c]; con.setWeekday(mByDays[i].day()); con.setWeekdaynr(mByDays[i].pos()); tmp.append(con); } } mConstraints = tmp; tmp.clear(); } #define fixConstraint( setElement, value ) \ { \ for ( c = 0, cend = mConstraints.count(); c < cend; ++c ) { \ mConstraints[c].setElement( value ); \ } \ } // Now determine missing values from DTSTART. This can speed up things, // because we have more restrictions and save some loops. // TODO: Does RFC 2445 intend to restrict the weekday in all cases of weekly? if (mPeriod == rWeekly && mByDays.isEmpty()) { fixConstraint(setWeekday, mDateStart.date().dayOfWeek()); } // Really fall through in the cases, because all smaller time intervals are // constrained from dtstart switch (mPeriod) { case rYearly: if (mByDays.isEmpty() && mByWeekNumbers.isEmpty() && mByYearDays.isEmpty() && mByMonths.isEmpty()) { fixConstraint(setMonth, mDateStart.date().month()); } case rMonthly: if (mByDays.isEmpty() && mByWeekNumbers.isEmpty() && mByYearDays.isEmpty() && mByMonthDays.isEmpty()) { fixConstraint(setDay, mDateStart.date().day()); } case rWeekly: case rDaily: if (mByHours.isEmpty()) { fixConstraint(setHour, mDateStart.time().hour()); } case rHourly: if (mByMinutes.isEmpty()) { fixConstraint(setMinute, mDateStart.time().minute()); } case rMinutely: if (mBySeconds.isEmpty()) { fixConstraint(setSecond, mDateStart.time().second()); } case rSecondly: default: break; } #undef fixConstraint if (mNoByRules) { switch (mPeriod) { case rHourly: mTimedRepetition = mFrequency * 3600; break; case rMinutely: mTimedRepetition = mFrequency * 60; break; case rSecondly: mTimedRepetition = mFrequency; break; default: break; } } else { for (c = 0, cend = mConstraints.count(); c < cend;) { if (mConstraints[c].isConsistent(mPeriod)) { ++c; } else { mConstraints.removeAt(c); --cend; } } } } // Build and cache a list of all occurrences. // Only call buildCache() if mDuration > 0. bool RecurrenceRule::Private::buildCache() const { Q_ASSERT(mDuration > 0); // Build the list of all occurrences of this event (we need that to determine // the end date!) Constraint interval(getNextValidDateInterval(mDateStart, mPeriod)); QDateTime next; DateTimeList dts = datesForInterval(interval, mPeriod); // Only use dates after the event has started (start date is only included // if it matches) int i = dts.findLT(mDateStart); if (i >= 0) { dts.erase(dts.begin(), dts.begin() + i + 1); } // some validity checks to avoid infinite loops (i.e. if we have // done this loop already 10000 times, bail out ) for (int loopnr = 0; loopnr < LOOP_LIMIT && dts.count() < mDuration; ++loopnr) { interval.increase(mPeriod, mFrequency); // The returned date list is already sorted! dts += datesForInterval(interval, mPeriod); } if (dts.count() > mDuration) { // we have picked up more occurrences than necessary, remove them dts.erase(dts.begin() + mDuration, dts.end()); } mCached = true; mCachedDates = dts; // it = dts.begin(); // while ( it != dts.end() ) { // kDebug() << " -=>" << dumpTime(*it); // ++it; // } if (int(dts.count()) == mDuration) { mCachedDateEnd = dts.last(); return true; } else { // The cached date list is incomplete mCachedDateEnd = KDateTime(); mCachedLastDate = interval.intervalDateTime(mPeriod); return false; } } //@endcond bool RecurrenceRule::dateMatchesRules(const KDateTime &kdt) const { KDateTime dt = kdt.toTimeSpec(d->mDateStart.timeSpec()); for (int i = 0, iend = d->mConstraints.count(); i < iend; ++i) { if (d->mConstraints[i].matches(dt, recurrenceType())) { return true; } } return false; } bool RecurrenceRule::recursOn(const QDate &qd, const KDateTime::Spec &timeSpec) const { int i, iend; if (!qd.isValid() || !d->mDateStart.isValid()) { // There can't be recurrences on invalid dates return false; } if (allDay()) { // It's a date-only rule, so it has no time specification. // Therefore ignore 'timeSpec'. if (qd < d->mDateStart.date()) { return false; } // Start date is only included if it really matches QDate endDate; if (d->mDuration >= 0) { endDate = endDt().date(); if (qd > endDate) { return false; } } // The date must be in an appropriate interval (getNextValidDateInterval), // Plus it must match at least one of the constraints bool match = false; for (i = 0, iend = d->mConstraints.count(); i < iend && !match; ++i) { match = d->mConstraints[i].matches(qd, recurrenceType()); } if (!match) { return false; } KDateTime start(qd, QTime(0, 0, 0), d->mDateStart.timeSpec()); Constraint interval(d->getNextValidDateInterval(start, recurrenceType())); // Constraint::matches is quite efficient, so first check if it can occur at // all before we calculate all actual dates. if (!interval.matches(qd, recurrenceType())) { return false; } // We really need to obtain the list of dates in this interval, since // otherwise BYSETPOS will not work (i.e. the date will match the interval, // but BYSETPOS selects only one of these matching dates! KDateTime end = start.addDays(1); do { DateTimeList dts = d->datesForInterval(interval, recurrenceType()); for (i = 0, iend = dts.count(); i < iend; ++i) { if (dts[i].date() >= qd) { return dts[i].date() == qd; } } interval.increase(recurrenceType(), frequency()); } while (interval.intervalDateTime(recurrenceType()) < end); return false; } // It's a date-time rule, so we need to take the time specification into account. KDateTime start(qd, QTime(0, 0, 0), timeSpec); KDateTime end = start.addDays(1).toTimeSpec(d->mDateStart.timeSpec()); start = start.toTimeSpec(d->mDateStart.timeSpec()); if (end < d->mDateStart) { return false; } if (start < d->mDateStart) { start = d->mDateStart; } // Start date is only included if it really matches if (d->mDuration >= 0) { KDateTime endRecur = endDt(); if (endRecur.isValid()) { if (start > endRecur) { return false; } if (end > endRecur) { end = endRecur; // limit end-of-day time to end of recurrence rule } } } if (d->mTimedRepetition) { // It's a simple sub-daily recurrence with no constraints int n = static_cast((d->mDateStart.secsTo_long(start) - 1) % d->mTimedRepetition); return start.addSecs(d->mTimedRepetition - n) < end; } // Find the start and end dates in the time spec for the rule QDate startDay = start.date(); QDate endDay = end.addSecs(-1).date(); int dayCount = startDay.daysTo(endDay) + 1; // The date must be in an appropriate interval (getNextValidDateInterval), // Plus it must match at least one of the constraints bool match = false; for (i = 0, iend = d->mConstraints.count(); i < iend && !match; ++i) { match = d->mConstraints[i].matches(startDay, recurrenceType()); for (int day = 1; day < dayCount && !match; ++day) { match = d->mConstraints[i].matches(startDay.addDays(day), recurrenceType()); } } if (!match) { return false; } Constraint interval(d->getNextValidDateInterval(start, recurrenceType())); // Constraint::matches is quite efficient, so first check if it can occur at // all before we calculate all actual dates. match = false; Constraint intervalm = interval; do { match = intervalm.matches(startDay, recurrenceType()); for (int day = 1; day < dayCount && !match; ++day) { match = intervalm.matches(startDay.addDays(day), recurrenceType()); } if (match) { break; } intervalm.increase(recurrenceType(), frequency()); } while (intervalm.intervalDateTime(recurrenceType()) < end); if (!match) { return false; } // We really need to obtain the list of dates in this interval, since // otherwise BYSETPOS will not work (i.e. the date will match the interval, // but BYSETPOS selects only one of these matching dates! do { DateTimeList dts = d->datesForInterval(interval, recurrenceType()); int i = dts.findGE(start); if (i >= 0) { return dts[i] <= end; } interval.increase(recurrenceType(), frequency()); } while (interval.intervalDateTime(recurrenceType()) < end); return false; } bool RecurrenceRule::recursAt(const KDateTime &kdt) const { // Convert to the time spec used by this recurrence rule KDateTime dt(kdt.toTimeSpec(d->mDateStart.timeSpec())); if (allDay()) { return recursOn(dt.date(), dt.timeSpec()); } if (dt < d->mDateStart) { return false; } // Start date is only included if it really matches if (d->mDuration >= 0 && dt > endDt()) { return false; } if (d->mTimedRepetition) { // It's a simple sub-daily recurrence with no constraints return !(d->mDateStart.secsTo_long(dt) % d->mTimedRepetition); } // The date must be in an appropriate interval (getNextValidDateInterval), // Plus it must match at least one of the constraints if (!dateMatchesRules(dt)) { return false; } // if it recurs every interval, speed things up... // if ( d->mFrequency == 1 && d->mBySetPos.isEmpty() && d->mByDays.isEmpty() ) return true; Constraint interval(d->getNextValidDateInterval(dt, recurrenceType())); // TODO_Recurrence: Does this work with BySetPos??? if (interval.matches(dt, recurrenceType())) { return true; } return false; } TimeList RecurrenceRule::recurTimesOn(const QDate &date, const KDateTime::Spec &timeSpec) const { TimeList lst; if (allDay()) { return lst; } KDateTime start(date, QTime(0, 0, 0), timeSpec); KDateTime end = start.addDays(1).addSecs(-1); DateTimeList dts = timesInInterval(start, end); // returns between start and end inclusive for (int i = 0, iend = dts.count(); i < iend; ++i) { lst += dts[i].toTimeSpec(timeSpec).time(); } return lst; } /** Returns the number of recurrences up to and including the date/time specified. */ int RecurrenceRule::durationTo(const KDateTime &dt) const { // Convert to the time spec used by this recurrence rule KDateTime toDate(dt.toTimeSpec(d->mDateStart.timeSpec())); // Easy cases: // either before start, or after all recurrences and we know their number if (toDate < d->mDateStart) { return 0; } // Start date is only included if it really matches if (d->mDuration > 0 && toDate >= endDt()) { return d->mDuration; } if (d->mTimedRepetition) { // It's a simple sub-daily recurrence with no constraints return static_cast(d->mDateStart.secsTo_long(toDate) / d->mTimedRepetition); } return timesInInterval(d->mDateStart, toDate).count(); } int RecurrenceRule::durationTo(const QDate &date) const { return durationTo(KDateTime(date, QTime(23, 59, 59), d->mDateStart.timeSpec())); } KDateTime RecurrenceRule::getPreviousDate(const KDateTime &afterDate) const { // Convert to the time spec used by this recurrence rule KDateTime toDate(afterDate.toTimeSpec(d->mDateStart.timeSpec())); // Invalid starting point, or beyond end of recurrence if (!toDate.isValid() || toDate < d->mDateStart) { return KDateTime(); } if (d->mTimedRepetition) { // It's a simple sub-daily recurrence with no constraints KDateTime prev = toDate; if (d->mDuration >= 0 && endDt().isValid() && toDate > endDt()) { prev = endDt().addSecs(1).toTimeSpec(d->mDateStart.timeSpec()); } int n = static_cast((d->mDateStart.secsTo_long(prev) - 1) % d->mTimedRepetition); if (n < 0) { return KDateTime(); // before recurrence start } prev = prev.addSecs(-n - 1); return prev >= d->mDateStart ? prev : KDateTime(); } // If we have a cache (duration given), use that if (d->mDuration > 0) { if (!d->mCached) { d->buildCache(); } int i = d->mCachedDates.findLT(toDate); if (i >= 0) { return d->mCachedDates[i]; } return KDateTime(); } KDateTime prev = toDate; if (d->mDuration >= 0 && endDt().isValid() && toDate > endDt()) { prev = endDt().addSecs(1).toTimeSpec(d->mDateStart.timeSpec()); } Constraint interval(d->getPreviousValidDateInterval(prev, recurrenceType())); DateTimeList dts = d->datesForInterval(interval, recurrenceType()); int i = dts.findLT(prev); if (i >= 0) { return (dts[i] >= d->mDateStart) ? dts[i] : KDateTime(); } // Previous interval. As soon as we find an occurrence, we're done. while (interval.intervalDateTime(recurrenceType()) > d->mDateStart) { interval.increase(recurrenceType(), -int(frequency())); // The returned date list is sorted DateTimeList dts = d->datesForInterval(interval, recurrenceType()); // The list is sorted, so take the last one. if (!dts.isEmpty()) { prev = dts.last(); if (prev.isValid() && prev >= d->mDateStart) { return prev; } else { return KDateTime(); } } } return KDateTime(); } KDateTime RecurrenceRule::getNextDate(const KDateTime &preDate) const { // Convert to the time spec used by this recurrence rule KDateTime fromDate(preDate.toTimeSpec(d->mDateStart.timeSpec())); // Beyond end of recurrence if (d->mDuration >= 0 && endDt().isValid() && fromDate >= endDt()) { return KDateTime(); } // Start date is only included if it really matches if (fromDate < d->mDateStart) { fromDate = d->mDateStart.addSecs(-1); } if (d->mTimedRepetition) { // It's a simple sub-daily recurrence with no constraints int n = static_cast((d->mDateStart.secsTo_long(fromDate) + 1) % d->mTimedRepetition); KDateTime next = fromDate.addSecs(d->mTimedRepetition - n + 1); return d->mDuration < 0 || !endDt().isValid() || next <= endDt() ? next : KDateTime(); } if (d->mDuration > 0) { if (!d->mCached) { d->buildCache(); } int i = d->mCachedDates.findGT(fromDate); if (i >= 0) { return d->mCachedDates[i]; } } KDateTime end = endDt(); Constraint interval(d->getNextValidDateInterval(fromDate, recurrenceType())); DateTimeList dts = d->datesForInterval(interval, recurrenceType()); int i = dts.findGT(fromDate); if (i >= 0) { return (d->mDuration < 0 || dts[i] <= end) ? dts[i] : KDateTime(); } interval.increase(recurrenceType(), frequency()); if (d->mDuration >= 0 && interval.intervalDateTime(recurrenceType()) > end) { return KDateTime(); } // Increase the interval. The first occurrence that we find is the result (if // if's before the end date). // TODO: some validity checks to avoid infinite loops for contradictory constraints int loop = 0; do { DateTimeList dts = d->datesForInterval(interval, recurrenceType()); if (dts.count() > 0) { KDateTime ret(dts[0]); if (d->mDuration >= 0 && ret > end) { return KDateTime(); } else { return ret; } } interval.increase(recurrenceType(), frequency()); } while (++loop < LOOP_LIMIT && (d->mDuration < 0 || interval.intervalDateTime(recurrenceType()) < end)); return KDateTime(); } DateTimeList RecurrenceRule::timesInInterval(const KDateTime &dtStart, const KDateTime &dtEnd) const { const KDateTime start = dtStart.toTimeSpec(d->mDateStart.timeSpec()); const KDateTime end = dtEnd.toTimeSpec(d->mDateStart.timeSpec()); DateTimeList result; if (end < d->mDateStart) { return result; // before start of recurrence } KDateTime enddt = end; if (d->mDuration >= 0) { const KDateTime endRecur = endDt(); if (endRecur.isValid()) { if (start > endRecur) { return result; // beyond end of recurrence } if (end >= endRecur) { enddt = endRecur; // limit end time to end of recurrence rule } } } if (d->mTimedRepetition) { // It's a simple sub-daily recurrence with no constraints //Seconds to add to interval start, to get first occurrence which is within interval qint64 offsetFromNextOccurrence; if (d->mDateStart < start) { offsetFromNextOccurrence = d->mTimedRepetition - (d->mDateStart.secsTo_long(start) % d->mTimedRepetition); } else { offsetFromNextOccurrence = -(d->mDateStart.secsTo_long(start) % d->mTimedRepetition); } KDateTime dt = start.addSecs(offsetFromNextOccurrence); if (dt <= enddt) { int numberOfOccurrencesWithinInterval = static_cast(dt.secsTo_long(enddt) / d->mTimedRepetition) + 1; // limit n by a sane value else we can "explode". numberOfOccurrencesWithinInterval = qMin(numberOfOccurrencesWithinInterval, LOOP_LIMIT); for (int i = 0; i < numberOfOccurrencesWithinInterval; dt = dt.addSecs(d->mTimedRepetition), ++i) { result += dt; } } return result; } KDateTime st = start; bool done = false; if (d->mDuration > 0) { if (!d->mCached) { d->buildCache(); } if (d->mCachedDateEnd.isValid() && start > d->mCachedDateEnd) { return result; // beyond end of recurrence } int i = d->mCachedDates.findGE(start); if (i >= 0) { int iend = d->mCachedDates.findGT(enddt, i); if (iend < 0) { iend = d->mCachedDates.count(); } else { done = true; } while (i < iend) { result += d->mCachedDates[i++]; } } if (d->mCachedDateEnd.isValid()) { done = true; } else if (!result.isEmpty()) { result += KDateTime(); // indicate that the returned list is incomplete done = true; } if (done) { return result; } // We don't have any result yet, but we reached the end of the incomplete cache st = d->mCachedLastDate.addSecs(1); } Constraint interval(d->getNextValidDateInterval(st, recurrenceType())); int loop = 0; do { DateTimeList dts = d->datesForInterval(interval, recurrenceType()); int i = 0; int iend = dts.count(); if (loop == 0) { i = dts.findGE(st); if (i < 0) { i = iend; } } int j = dts.findGT(enddt, i); if (j >= 0) { iend = j; loop = LOOP_LIMIT; } while (i < iend) { result += dts[i++]; } // Increase the interval. interval.increase(recurrenceType(), frequency()); } while (++loop < LOOP_LIMIT && interval.intervalDateTime(recurrenceType()) < end); return result; } //@cond PRIVATE // Find the date/time of the occurrence at or before a date/time, // for a given period type. // Return a constraint whose value appropriate to 'type', is set to // the value contained in the date/time. Constraint RecurrenceRule::Private::getPreviousValidDateInterval(const KDateTime &dt, PeriodType type) const { long periods = 0; KDateTime start = mDateStart; KDateTime nextValid(start); int modifier = 1; KDateTime toDate(dt.toTimeSpec(start.timeSpec())); // for super-daily recurrences, don't care about the time part // Find the #intervals since the dtstart and round to the next multiple of // the frequency switch (type) { // Really fall through for sub-daily, since the calculations only differ // by the factor 60 and 60*60! Same for weekly and daily (factor 7) case rHourly: modifier *= 60; case rMinutely: modifier *= 60; case rSecondly: periods = static_cast(start.secsTo_long(toDate) / modifier); // round it down to the next lower multiple of frequency: if (mFrequency > 0) { periods = (periods / mFrequency) * mFrequency; } nextValid = start.addSecs(modifier * periods); break; case rWeekly: toDate = toDate.addDays(-(7 + toDate.date().dayOfWeek() - mWeekStart) % 7); start = start.addDays(-(7 + start.date().dayOfWeek() - mWeekStart) % 7); modifier *= 7; case rDaily: periods = start.daysTo(toDate) / modifier; // round it down to the next lower multiple of frequency: if (mFrequency > 0) { periods = (periods / mFrequency) * mFrequency; } nextValid = start.addDays(modifier * periods); break; case rMonthly: { periods = 12 * (toDate.date().year() - start.date().year()) + (toDate.date().month() - start.date().month()); // round it down to the next lower multiple of frequency: if (mFrequency > 0) { periods = (periods / mFrequency) * mFrequency; } // set the day to the first day of the month, so we don't have problems // with non-existent days like Feb 30 or April 31 start.setDate(QDate(start.date().year(), start.date().month(), 1)); nextValid.setDate(start.date().addMonths(periods)); break; } case rYearly: periods = (toDate.date().year() - start.date().year()); // round it down to the next lower multiple of frequency: if (mFrequency > 0) { periods = (periods / mFrequency) * mFrequency; } nextValid.setDate(start.date().addYears(periods)); break; default: break; } return Constraint(nextValid, type, mWeekStart); } // Find the date/time of the next occurrence at or after a date/time, // for a given period type. // Return a constraint whose value appropriate to 'type', is set to the // value contained in the date/time. Constraint RecurrenceRule::Private::getNextValidDateInterval(const KDateTime &dt, PeriodType type) const { // TODO: Simplify this! long periods = 0; KDateTime start = mDateStart; KDateTime nextValid(start); int modifier = 1; KDateTime toDate(dt.toTimeSpec(start.timeSpec())); // for super-daily recurrences, don't care about the time part // Find the #intervals since the dtstart and round to the next multiple of // the frequency switch (type) { // Really fall through for sub-daily, since the calculations only differ // by the factor 60 and 60*60! Same for weekly and daily (factor 7) case rHourly: modifier *= 60; case rMinutely: modifier *= 60; case rSecondly: periods = static_cast(start.secsTo_long(toDate) / modifier); periods = qMax(0L, periods); if (periods > 0 && mFrequency > 0) { periods += (mFrequency - 1 - ((periods - 1) % mFrequency)); } nextValid = start.addSecs(modifier * periods); break; case rWeekly: // correct both start date and current date to start of week toDate = toDate.addDays(-(7 + toDate.date().dayOfWeek() - mWeekStart) % 7); start = start.addDays(-(7 + start.date().dayOfWeek() - mWeekStart) % 7); modifier *= 7; case rDaily: periods = start.daysTo(toDate) / modifier; periods = qMax(0L, periods); if (periods > 0 && mFrequency > 0) { periods += (mFrequency - 1 - ((periods - 1) % mFrequency)); } nextValid = start.addDays(modifier * periods); break; case rMonthly: { periods = 12 * (toDate.date().year() - start.date().year()) + (toDate.date().month() - start.date().month()); periods = qMax(0L, periods); if (periods > 0 && mFrequency > 0) { periods += (mFrequency - 1 - ((periods - 1) % mFrequency)); } // set the day to the first day of the month, so we don't have problems // with non-existent days like Feb 30 or April 31 start.setDate(QDate(start.date().year(), start.date().month(), 1)); nextValid.setDate(start.date().addMonths(periods)); break; } case rYearly: periods = (toDate.date().year() - start.date().year()); periods = qMax(0L, periods); if (periods > 0 && mFrequency > 0) { periods += (mFrequency - 1 - ((periods - 1) % mFrequency)); } nextValid.setDate(start.date().addYears(periods)); break; default: break; } return Constraint(nextValid, type, mWeekStart); } DateTimeList RecurrenceRule::Private::datesForInterval(const Constraint &interval, PeriodType type) const { /* -) Loop through constraints, -) merge interval with each constraint -) if merged constraint is not consistent => ignore that constraint -) if complete => add that one date to the date list -) Loop through all missing fields => For each add the resulting */ DateTimeList lst; for (int i = 0, iend = mConstraints.count(); i < iend; ++i) { Constraint merged(interval); if (merged.merge(mConstraints[i])) { // If the information is incomplete, we can't use this constraint if (merged.year > 0 && merged.hour >= 0 && merged.minute >= 0 && merged.second >= 0) { // We have a valid constraint, so get all datetimes that match it andd // append it to all date/times of this interval QList lstnew = merged.dateTimes(type); lst += lstnew; } } } // Sort it so we can apply the BySetPos. Also some logic relies on this being sorted lst.sortUnique(); /*if ( lst.isEmpty() ) { kDebug() << " No Dates in Interval"; } else { kDebug() << " Dates:"; for ( int i = 0, iend = lst.count(); i < iend; ++i ) { kDebug()<< " -)" << dumpTime(lst[i]); } kDebug() << " ---------------------"; }*/ if (!mBySetPos.isEmpty()) { DateTimeList tmplst = lst; lst.clear(); for (int i = 0, iend = mBySetPos.count(); i < iend; ++i) { int pos = mBySetPos[i]; if (pos > 0) { --pos; } if (pos < 0) { pos += tmplst.count(); } if (pos >= 0 && pos < tmplst.count()) { lst.append(tmplst[pos]); } } lst.sortUnique(); } return lst; } //@endcond void RecurrenceRule::dump() const { #ifndef NDEBUG kDebug(); if (!d->mRRule.isEmpty()) { kDebug() << " RRULE=" << d->mRRule; } kDebug() << " Read-Only:" << isReadOnly(); kDebug() << " Period type:" << int(recurrenceType()) << ", frequency:" << frequency(); kDebug() << " #occurrences:" << duration(); kDebug() << " start date:" << dumpTime(startDt()) << ", end date:" << dumpTime(endDt()); #define dumpByIntList(list,label) \ if ( !list.isEmpty() ) {\ QStringList lst;\ for ( int i = 0, iend = list.count(); i < iend; ++i ) {\ lst.append( QString::number( list[i] ) );\ }\ kDebug() << " " << label << lst.join( QLatin1String(", ") );\ } dumpByIntList(d->mBySeconds, QLatin1String("BySeconds: ")); dumpByIntList(d->mByMinutes, QLatin1String("ByMinutes: ")); dumpByIntList(d->mByHours, QLatin1String("ByHours: ")); if (!d->mByDays.isEmpty()) { QStringList lst; for (int i = 0, iend = d->mByDays.count(); i < iend; ++i) { \ lst.append((d->mByDays[i].pos() ? QString::number(d->mByDays[i].pos()) : QLatin1String("")) + DateHelper::dayName(d->mByDays[i].day())); } kDebug() << " ByDays: " << lst.join(QLatin1String(", ")); } dumpByIntList(d->mByMonthDays, QLatin1String("ByMonthDays:")); dumpByIntList(d->mByYearDays, QLatin1String("ByYearDays: ")); dumpByIntList(d->mByWeekNumbers, QLatin1String("ByWeekNr: ")); dumpByIntList(d->mByMonths, QLatin1String("ByMonths: ")); dumpByIntList(d->mBySetPos, QLatin1String("BySetPos: ")); #undef dumpByIntList kDebug() << " Week start:" << DateHelper::dayName(d->mWeekStart); //krazy:exclude=kdebug kDebug() << " Constraints:"; // dump constraints for (int i = 0, iend = d->mConstraints.count(); i < iend; ++i) { d->mConstraints[i].dump(); } #endif } //@cond PRIVATE void Constraint::dump() const { kDebug() << " ~> Y=" << year << ", M=" << month << ", D=" << day << ", H=" << hour << ", m=" << minute << ", S=" << second << ", wd=" << weekday << ",#wd=" << weekdaynr << ", #w=" << weeknumber << ", yd=" << yearday; } //@endcond QString dumpTime(const KDateTime &dt) { #ifndef NDEBUG if (!dt.isValid()) { return QString(); } QString result; if (dt.isDateOnly()) { result = dt.toString(QLatin1String("%a %Y-%m-%d %:Z")); } else { result = dt.toString(QLatin1String("%a %Y-%m-%d %H:%M:%S %:Z")); if (dt.isSecondOccurrence()) { result += QLatin1String(" (2nd)"); } } if (dt.timeSpec() == KDateTime::Spec::ClockTime()) { result += QLatin1String("Clock"); } return result; #else Q_UNUSED(dt); return QString(); #endif } KDateTime RecurrenceRule::startDt() const { return d->mDateStart; } RecurrenceRule::PeriodType RecurrenceRule::recurrenceType() const { return d->mPeriod; } uint RecurrenceRule::frequency() const { return d->mFrequency; } int RecurrenceRule::duration() const { return d->mDuration; } QString RecurrenceRule::rrule() const { return d->mRRule; } void RecurrenceRule::setRRule(const QString &rrule) { d->mRRule = rrule; } bool RecurrenceRule::isReadOnly() const { return d->mIsReadOnly; } void RecurrenceRule::setReadOnly(bool readOnly) { d->mIsReadOnly = readOnly; } bool RecurrenceRule::recurs() const { return d->mPeriod != rNone; } bool RecurrenceRule::allDay() const { return d->mAllDay; } const QList &RecurrenceRule::bySeconds() const { return d->mBySeconds; } const QList &RecurrenceRule::byMinutes() const { return d->mByMinutes; } const QList &RecurrenceRule::byHours() const { return d->mByHours; } const QList &RecurrenceRule::byDays() const { return d->mByDays; } const QList &RecurrenceRule::byMonthDays() const { return d->mByMonthDays; } const QList &RecurrenceRule::byYearDays() const { return d->mByYearDays; } const QList &RecurrenceRule::byWeekNumbers() const { return d->mByWeekNumbers; } const QList &RecurrenceRule::byMonths() const { return d->mByMonths; } const QList &RecurrenceRule::bySetPos() const { return d->mBySetPos; } short RecurrenceRule::weekStart() const { return d->mWeekStart; } RecurrenceRule::RuleObserver::~RuleObserver() { } RecurrenceRule::WDayPos::WDayPos(int ps, short dy) : mDay(dy), mPos(ps) { } void RecurrenceRule::WDayPos::setDay(short dy) { mDay = dy; } short RecurrenceRule::WDayPos::day() const { return mDay; } void RecurrenceRule::WDayPos::setPos(int ps) { mPos = ps; } int RecurrenceRule::WDayPos::pos() const { return mPos; } QDataStream& operator<<(QDataStream &out, const Constraint &c) { out << c.year << c.month << c.day << c.hour << c.minute << c.second << c.weekday << c.weekdaynr << c.weeknumber << c.yearday << c.weekstart << c.timespec << c.secondOccurrence; return out; } QDataStream& operator>>(QDataStream &in, Constraint &c) { in >> c.year >> c.month >> c.day >> c.hour >> c.minute >> c.second >> c.weekday >> c.weekdaynr >> c.weeknumber >> c.yearday >> c.weekstart >> c.timespec >> c.secondOccurrence; return in; } KCALCORE_EXPORT QDataStream& KCalCore::operator<<(QDataStream &out, const KCalCore::RecurrenceRule::WDayPos &w) { out << w.mDay << w.mPos; return out; } KCALCORE_EXPORT QDataStream& KCalCore::operator>>(QDataStream &in, KCalCore::RecurrenceRule::WDayPos &w) { in >> w.mDay >> w.mPos; return in; } KCALCORE_EXPORT QDataStream& KCalCore::operator<<(QDataStream &out, const KCalCore::RecurrenceRule *r) { if (!r) return out; RecurrenceRule::Private *d = r->d; out << d->mRRule << static_cast(d->mPeriod) << d->mDateStart << d->mFrequency << d->mDuration << d->mDateEnd << d->mBySeconds << d->mByMinutes << d->mByHours << d->mByDays << d->mByMonthDays << d->mByYearDays << d->mByWeekNumbers << d->mByMonths << d->mBySetPos << d->mWeekStart << d->mConstraints << d->mAllDay << d->mNoByRules << d->mTimedRepetition << d->mIsReadOnly; return out; } KCALCORE_EXPORT QDataStream& KCalCore::operator>>(QDataStream &in, const KCalCore::RecurrenceRule *r) { if (!r) return in; RecurrenceRule::Private *d = r->d; quint32 period; in >> d->mRRule >> period >> d->mDateStart >> d->mFrequency >> d->mDuration >> d->mDateEnd >> d->mBySeconds >> d->mByMinutes >> d->mByHours >> d->mByDays >> d->mByMonthDays >> d->mByYearDays >> d->mByWeekNumbers >> d->mByMonths >> d->mBySetPos >> d->mWeekStart >> d->mConstraints >> d->mAllDay >> d->mNoByRules >> d->mTimedRepetition >> d->mIsReadOnly; d->mPeriod = static_cast(period); return in; } diff --git a/kcalcore/tests/CMakeLists.txt b/kcalcore/tests/CMakeLists.txt index d8c40eeae..c388fb0dc 100644 --- a/kcalcore/tests/CMakeLists.txt +++ b/kcalcore/tests/CMakeLists.txt @@ -1,139 +1,140 @@ set(EXECUTABLE_OUTPUT_PATH ${CMAKE_CURRENT_BINARY_DIR}) include_directories(${LIBICAL_INCLUDE_DIRS}) macro(macro_unit_tests) foreach(_testname ${ARGN}) kde4_add_unit_test(${_testname} NOGUI ${_testname}.cpp) target_link_libraries(${_testname} ${KDE4_KDECORE_LIBS} ${LIBICAL_LIBRARIES} ${QT_QTGUI_LIBRARY} ${QT_QTTEST_LIBRARY} kcalcore ) endforeach() endmacro() macro(macro_exec_tests) foreach(_testname ${ARGN}) kde4_add_executable(${_testname} NOGUI TEST ${_testname}.cpp) target_link_libraries(${_testname} ${KDE4_KDECORE_LIBS} ${QT_QTCORE_LIBRARY} ${QT_QTTEST_LIBRARY} kcalcore ) endforeach() endmacro() macro_unit_tests( testalarm testattachment testattendee testcalfilter testcustomproperties testduration testevent + testincidence testexception testfilestorage testfreebusy testincidencerelation testicalformat testjournal testmemorycalendar testperiod testfreebusyperiod testperson testrecurtodo testsortablelist testtodo testtimesininterval testcreateddatecompat testrecurrenceexception testoccurrenceiterator testreadrecurrenceid ) set_target_properties(testmemorycalendar PROPERTIES COMPILE_FLAGS -DICALTESTDATADIR="\\"${CMAKE_SOURCE_DIR}/kcalcore/tests/data/\\"" ) set_target_properties(testreadrecurrenceid PROPERTIES COMPILE_FLAGS -DICALTESTDATADIR="\\"${CMAKE_SOURCE_DIR}/kcalcore/tests/data/\\"" ) # this test cannot work with msvc because libical should not be altered # and therefore we can't add KCALCORE_EXPORT there # it should work fine with mingw because of the auto-import feature # KDAB_TODO: check if this can build with msvc now (and if it works on Windows) if(NOT MSVC) macro_unit_tests(testicaltimezones) endif() macro_exec_tests( incidencestest loadcalendar fbrecurring readandwrite testfb testrecurprevious testrecurrence testrecurrencetype testrecurson testtostring testvcalexport ) ########### Tests ####################### file(GLOB_RECURSE testFiles data/RecurrenceRule/*.ics) file(GLOB_RECURSE compatFiles data/Compat/*.ics) file(GLOB_RECURSE vCalFilesAsIcal data/vCalendar/*.ics) file(GLOB_RECURSE vCalFiles data/vCalendar/*.vcs) if(WIN32) find_program(PERL_EXECUTABLE perl) endif() macro(kcalcore_run_single_test _prefix _f _test _testarg) get_filename_component(_fn ${_f} NAME) # Write the output file in the build directory. string(REGEX REPLACE "${CMAKE_CURRENT_SOURCE_DIR}" "${CMAKE_CURRENT_BINARY_DIR}" _outFile ${_f}) get_filename_component(_outputPath ${_outFile} PATH) file(MAKE_DIRECTORY ${_outputPath}) set(_runner ${CMAKE_CURRENT_SOURCE_DIR}/runsingletestcase.pl) if(UNIX) set(_test_path ${EXECUTABLE_OUTPUT_PATH}/${_test}.shell) endif() if(WIN32) set(_test_path ${EXECUTABLE_OUTPUT_PATH}/${_test}.exe.bat) set(_runner ${PERL_EXECUTABLE} ${_runner}) endif() add_test(${_prefix}-${_fn} ${_runner} ${_test_path} ${_testarg} ${_f} ${_outFile}) endmacro() file(REMOVE ${CMAKE_CURRENT_BINARY_DIR}/FAILED.log) foreach(file ${testFiles}) kcalcore_run_single_test(RecurNext ${file} testrecurrence "next") endforeach() foreach(file ${testFiles}) kcalcore_run_single_test(RecurPrev ${file} testrecurprevious "prev") endforeach() foreach(file ${testFiles}) kcalcore_run_single_test(RecursOn ${file} testrecurson "recurson") endforeach() foreach(file ${compatFiles}) kcalcore_run_single_test(Compat ${file} readandwrite "ical") endforeach() foreach(file ${vCalFilesAsIcal}) kcalcore_run_single_test(VCalOut ${file} testvcalexport "vcal") endforeach() foreach(file ${vCalFiles}) kcalcore_run_single_test(VCalIn ${file} readandwrite "ical") endforeach() diff --git a/kcalcore/tests/testevent.cpp b/kcalcore/tests/testevent.cpp index 7981d2ba6..96ba5ea05 100644 --- a/kcalcore/tests/testevent.cpp +++ b/kcalcore/tests/testevent.cpp @@ -1,242 +1,265 @@ /* This file is part of the kcalcore library. Copyright (C) 2006,2008 Allen Winter This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "testevent.h" #include "../event.h" #include "../todo.h" #include QTEST_KDEMAIN(EventTest, NoGUI) Q_DECLARE_METATYPE(KCalCore::Incidence::DateTimeRole) using namespace KCalCore; void EventTest::testSetRoles_data() { QTest::addColumn("originalDtStart"); QTest::addColumn("originalDtEnd"); QTest::addColumn("setRole"); QTest::addColumn("dateTimeToSet"); QTest::addColumn("expectedDtStart"); QTest::addColumn("expectedDtEnd"); const KDateTime todayDate(QDate::currentDate()); // all day event const KDateTime todayDateTime = KDateTime::currentUtcDateTime(); QTest::newRow("dnd 0 duration") << todayDate << todayDate << KCalCore::Incidence::RoleDnD << todayDateTime << todayDateTime << todayDateTime.addSecs(3600); } void EventTest::testSetRoles() { QFETCH(KDateTime, originalDtStart); QFETCH(KDateTime, originalDtEnd); QFETCH(KCalCore::Incidence::DateTimeRole, setRole); QFETCH(KDateTime, dateTimeToSet); QFETCH(KDateTime, expectedDtStart); QFETCH(KDateTime, expectedDtEnd); Event::Ptr event = Event::Ptr(new Event()); event->setDtStart(originalDtStart); event->setDtEnd(originalDtEnd); event->setAllDay(originalDtStart.isDateOnly()); event->setDateTime(dateTimeToSet, setRole); QCOMPARE(event->dtStart(), expectedDtStart); QCOMPARE(event->dtEnd(), expectedDtEnd); } void EventTest::testValidity() { QDate dt = QDate::currentDate(); Event *event = new Event(); event->setDtStart(KDateTime(dt)); event->setDtEnd(KDateTime(dt).addDays(1)); event->setSummary("Event1 Summary"); event->setDescription("This is a description of the first event"); event->setLocation("the place"); //KDE5: QVERIFY( event->typeStr() == i18n( "event" ) ); QVERIFY(event->summary() == "Event1 Summary"); QVERIFY(event->location() == "the place"); QVERIFY(event->type() == Incidence::TypeEvent); } void EventTest::testCompare() { QDate dt = QDate::currentDate(); Event event1; event1.setDtStart(KDateTime(dt)); event1.setDtEnd(KDateTime(dt).addDays(1)); event1.setSummary("Event1 Summary"); event1.setDescription("This is a description of the first event"); event1.setLocation("the place"); Event event2; event2.setDtStart(KDateTime(dt).addDays(1)); event2.setDtEnd(KDateTime(dt).addDays(2)); event2.setSummary("Event2 Summary"); event2.setDescription("This is a description of the second event"); event2.setLocation("the other place"); QVERIFY(!(event1 == event2)); QVERIFY(event1.dtEnd() == event2.dtStart()); QVERIFY(event2.summary() == "Event2 Summary"); } void EventTest::testClone() { QDate dt = QDate::currentDate(); Event event1; event1.setDtStart(KDateTime(dt)); event1.setDtEnd(KDateTime(dt).addDays(1)); event1.setSummary("Event1 Summary"); event1.setDescription("This is a description of the first event"); event1.setLocation("the place"); Event *event2 = event1.clone(); QVERIFY(event1.summary() == event2->summary()); QVERIFY(event1.dtStart() == event2->dtStart()); QVERIFY(event1.dtEnd() == event2->dtEnd()); QVERIFY(event1.description() == event2->description()); QVERIFY(event1.location() == event2->location()); } void EventTest::testCopy() { QDate dt = QDate::currentDate(); Event event1; event1.setDtStart(KDateTime(dt)); event1.setDtEnd(KDateTime(dt).addDays(1)); event1.setSummary("Event1 Summary"); event1.setDescription("This is a description of the first event"); event1.setLocation("the place"); event1.setTransparency(Event::Transparent); Event event2 = event1; QVERIFY(event1.summary() == event2.summary()); QVERIFY(event1.dtStart() == event2.dtStart()); QVERIFY(event1.dtEnd() == event2.dtEnd()); QVERIFY(event1.description() == event2.description()); QVERIFY(event1.location() == event2.location()); } void EventTest::testCopyIncidence() { QDate dt = QDate::currentDate(); Todo todo; todo.setDtStart(KDateTime(dt)); todo.setSummary(QLatin1String("Event1 Summary")); todo.setDescription(QLatin1String("This is a description of the first event")); todo.setLocation(QLatin1String("the place")); Event event(todo); QCOMPARE(event.uid(), todo.uid()); QCOMPARE(event.dtStart(), todo.dtStart()); QCOMPARE(event.summary(), todo.summary()); QCOMPARE(event.description(), todo.description()); QCOMPARE(event.location(), todo.location()); } void EventTest::testAssign() { QDate dt = QDate::currentDate(); Event event1; event1.setDtStart(KDateTime(dt)); event1.setDtEnd(KDateTime(dt).addDays(1)); event1.setSummary("Event1 Summary"); event1.setDescription("This is a description of the first event"); event1.setLocation("the place"); event1.setTransparency(Event::Transparent); Event event2 = event1; QVERIFY(event1 == event2); } void EventTest::testSerializer_data() { QTest::addColumn("event"); KDateTime today = KDateTime::currentUtcDateTime(); KDateTime yesterday = today.addDays(-1); Event::Ptr event1 = Event::Ptr(new Event()); Attendee::Ptr attendee1(new Attendee("fred", "fred@flintstone.com")); event1->addAttendee(attendee1); event1->setDtStart(yesterday); event1->setDtEnd(today); Event::Ptr event2 = Event::Ptr(new Event()); Attendee::Ptr attendee2(new Attendee("fred", "fred@flintstone.com")); event2->addAttendee(attendee2); event2->setDtStart(yesterday); event2->setDtEnd(today); event2->setAllDay(true); event2->addComment("comment1"); event2->setUrl(QUrl("http://someurl")); event2->setCustomProperty("app", "key", "value"); // Remaining properties tested in testtodo.cpp QTest::newRow("event") << event1; QTest::newRow("event2") << event2; } void EventTest::testSerializer() { QFETCH(KCalCore::Event::Ptr, event); IncidenceBase::Ptr incidenceBase = event.staticCast(); QByteArray array; QDataStream stream(&array, QIODevice::WriteOnly); stream << incidenceBase; Event::Ptr event2 = Event::Ptr(new Event()); IncidenceBase::Ptr incidenceBase2 = event2.staticCast(); QVERIFY(*event != *event2); QDataStream stream2(&array, QIODevice::ReadOnly); stream2 >> incidenceBase2; QVERIFY(*event == *event2); } void EventTest::testDurationDtEnd() { const QDate dt = QDate::currentDate(); { Event event; event.setDtStart(KDateTime(dt)); event.setDtEnd(KDateTime(dt).addDays(1)); QCOMPARE(event.hasEndDate(), true); QCOMPARE(event.hasDuration(), false); } { Event event; event.setDtStart(KDateTime(dt)); event.setDuration(Duration(KDateTime(dt), KDateTime(dt).addDays(1))); QCOMPARE(event.hasDuration(), true); QCOMPARE(event.hasEndDate(), false); } } + +void EventTest::testDtEndChange() +{ + QDate dt = QDate::currentDate(); + Event event1; + event1.setDtStart(KDateTime(dt)); + event1.setDtEnd(KDateTime(dt).addDays(1)); + event1.resetDirtyFields(); + + event1.setDtEnd(KDateTime(dt).addDays(1)); + QVERIFY(event1.dirtyFields().empty()); + + event1.setDtEnd(KDateTime(dt).addDays(2)); + QCOMPARE(event1.dirtyFields(), QSet() << IncidenceBase::FieldDtEnd); + event1.resetDirtyFields(); + + event1.setDtEnd(KDateTime()); + QCOMPARE(event1.dirtyFields(), QSet() << IncidenceBase::FieldDtEnd); + event1.resetDirtyFields(); + + event1.setDtEnd(KDateTime(dt).addDays(2)); + QCOMPARE(event1.dirtyFields(), QSet() << IncidenceBase::FieldDtEnd); +} \ No newline at end of file diff --git a/kcalcore/tests/testevent.h b/kcalcore/tests/testevent.h index 509cb7146..edb5ef723 100644 --- a/kcalcore/tests/testevent.h +++ b/kcalcore/tests/testevent.h @@ -1,44 +1,45 @@ /* This file is part of the kcalcore library. Copyright (c) 2006-2008 Allen Winter This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef TESTEVENT_H #define TESTEVENT_H #include class EventTest : public QObject { Q_OBJECT private Q_SLOTS: void testSetRoles_data(); void testSetRoles(); void testValidity(); void testCompare(); void testClone(); void testCopy(); void testCopyIncidence(); void testAssign(); void testSerializer_data(); void testSerializer(); void testDurationDtEnd(); + void testDtEndChange(); }; #endif diff --git a/kcalcore/tests/testincidence.cpp b/kcalcore/tests/testincidence.cpp new file mode 100644 index 000000000..eb7e31447 --- /dev/null +++ b/kcalcore/tests/testincidence.cpp @@ -0,0 +1,276 @@ +/* + This file is part of the kcalcore library. + + Copyright (C) 2015 Sandro Knauß + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ +#include "testincidence.h" +#include "../event.h" + +#include + +QTEST_KDEMAIN(IncidenceTest, NoGUI) + +Q_DECLARE_METATYPE(KCalCore::Incidence::DateTimeRole) + +using namespace KCalCore; + +void IncidenceTest::testDtStartChange() +{ + QDate dt = QDate::currentDate(); + QTime t = QTime::currentTime(); + Event inc; + inc.setDtStart(KDateTime(dt)); + inc.recurrence()->setDaily(1); + inc.resetDirtyFields(); + + inc.setDtStart(KDateTime(dt)); + QVERIFY(inc.dirtyFields().empty()); + + inc.setDtStart(KDateTime(dt,t)); + QCOMPARE(inc.dirtyFields(), QSet() << IncidenceBase::FieldDtStart << IncidenceBase::FieldRecurrence); + QCOMPARE(inc.recurrence()->startDateTime().time(), t); + inc.resetDirtyFields(); + + inc.setDtStart(KDateTime(dt).addDays(1)); + QCOMPARE(inc.dirtyFields(), QSet() << IncidenceBase::FieldDtStart << IncidenceBase::FieldRecurrence); + QCOMPARE(inc.recurrence()->startDateTime(), KDateTime(dt).addDays(1)); + inc.resetDirtyFields(); + + inc.setDtStart(KDateTime()); + QCOMPARE(inc.dirtyFields(), QSet() << IncidenceBase::FieldDtStart << IncidenceBase::FieldRecurrence); + QCOMPARE(inc.recurrence()->startDateTime(), KDateTime()); + inc.resetDirtyFields(); + + inc.setDtStart(KDateTime(dt).addDays(1)); + QCOMPARE(inc.dirtyFields(), QSet() << IncidenceBase::FieldDtStart << IncidenceBase::FieldRecurrence); + QCOMPARE(inc.recurrence()->startDateTime(), KDateTime(dt).addDays(1)); +} + +void IncidenceTest::testSummaryChange() +{ + Event inc; + inc.setSummary(QLatin1String("bla"), false); + inc.resetDirtyFields(); + + inc.setSummary(QLatin1String("bla"), false); + QVERIFY(inc.dirtyFields().empty()); + + inc.setSummary(QLatin1String("bla2"), false); + QCOMPARE(inc.dirtyFields(), QSet() << IncidenceBase::FieldSummary); + inc.resetDirtyFields(); + + inc.setSummary(QLatin1String("bla2"), true); + QCOMPARE(inc.dirtyFields(), QSet() << IncidenceBase::FieldSummary); +} + +void IncidenceTest::testLocationChange() +{ + + Event inc; + inc.setLocation(QLatin1String("here"), false); + inc.resetDirtyFields(); + + inc.setLocation(QLatin1String("here"), false); + QVERIFY(inc.dirtyFields().empty()); + + inc.setLocation(QLatin1String("there"), false); + QCOMPARE(inc.dirtyFields(), QSet() << IncidenceBase::FieldLocation); + inc.resetDirtyFields(); + + inc.setLocation(QLatin1String("there"), true); + QCOMPARE(inc.dirtyFields(), QSet() << IncidenceBase::FieldLocation); +} + + +void IncidenceTest::testRecurrenceTypeChange() +{ + QDate dt = QDate::currentDate(); + Event inc; + inc.setDtStart(KDateTime(dt)); + KCalCore::Recurrence *r = inc.recurrence(); + r->setDaily(1); + inc.resetDirtyFields(); + + r->setDaily(1); + QVERIFY(inc.dirtyFields().empty()); + + r->setDaily(2); + QCOMPARE(inc.dirtyFields(), QSet() << IncidenceBase::FieldRecurrence); + inc.resetDirtyFields(); + + r->setMonthly(2); + QCOMPARE(inc.dirtyFields(), QSet() << IncidenceBase::FieldRecurrence); +} + +void IncidenceTest::testRecurrenceEndTimeChange() +{ + QDate dt = QDate::currentDate(); + Event inc; + inc.setDtStart(KDateTime(dt)); + KCalCore::Recurrence *r = inc.recurrence(); + r->setDaily(1); + r->setEndDateTime(KDateTime(dt).addDays(1)); + inc.resetDirtyFields(); + + r->setEndDateTime(KDateTime(dt).addDays(1)); + QVERIFY(inc.dirtyFields().empty()); + + r->setEndDateTime(KDateTime(dt).addDays(2)); + QCOMPARE(inc.dirtyFields(), QSet() << IncidenceBase::FieldRecurrence); +} + +void IncidenceTest::testRecurrenceEndTimeDurationChange() +{ + QDate dt = QDate::currentDate(); + Event inc; + inc.setDtStart(KDateTime(dt)); + KCalCore::Recurrence *r = inc.recurrence(); + r->setDaily(1); + inc.resetDirtyFields(); + QCOMPARE(r->duration(), -1); + QVERIFY(!r->endDateTime().isValid()); + + r->setDuration(5); + QVERIFY(r->endDateTime().isValid()); + inc.resetDirtyFields(); + + // duration is set and set enddate to inValid + r->setDuration(5); + r->setEndDateTime(KDateTime()); + QVERIFY(inc.dirtyFields().empty()); + + // now set valid enddate -> set duration to 0 by sideeffect + r->setEndDateTime(KDateTime(dt).addDays(1)); + QCOMPARE(inc.dirtyFields(), QSet() << IncidenceBase::FieldRecurrence); + QCOMPARE(r->duration(), 0); + QCOMPARE(r->endDateTime(), KDateTime(dt).addDays(1)); + + // with valid endDate, now setDuration and aftward set invalid endDate + r->setEndDateTime(KDateTime(dt).addDays(1)); + r->setDuration(5); + inc.resetDirtyFields(); + + r->setEndDateTime(KDateTime()); + QVERIFY(inc.dirtyFields().empty()); + QCOMPARE(r->endDate(), dt.addDays(4)); + QCOMPARE(r->duration(), 5); +} + +void IncidenceTest::testRecurrenceDurationChange() +{ + QDate dt = QDate::currentDate(); + Event inc; + inc.setDtStart(KDateTime(dt)); + KCalCore::Recurrence *r = inc.recurrence(); + r->setDuration(1); + inc.resetDirtyFields(); + + r->setDuration(1); + QVERIFY(inc.dirtyFields().empty()); + + r->setDuration(2); + QCOMPARE(inc.dirtyFields(), QSet() << IncidenceBase::FieldRecurrence); +} + +void IncidenceTest::testRecurrenceExDatesChange() +{ + QDate dt = QDate::currentDate(); + Event inc; + inc.setDtStart(KDateTime(dt)); + KCalCore::Recurrence *r = inc.recurrence(); + r->setExDates(DateList() << dt.addDays(1) << dt.addDays(2)); + inc.resetDirtyFields(); + + r->setExDates(DateList() << dt.addDays(2) << dt.addDays(1)); + QVERIFY(inc.dirtyFields().empty()); + + r->setExDates(DateList() << dt.addDays(1)); + QCOMPARE(inc.dirtyFields(), QSet() << IncidenceBase::FieldRecurrence); +} + +void IncidenceTest::testRecurrenceMonthlyDate() +{ + QDate dt = QDate::currentDate(); + Event inc; + inc.setDtStart(KDateTime(dt)); + KCalCore::Recurrence *r = inc.recurrence(); + r->setMonthly(1); + r->setMonthlyDate(QList() << 1 << 2 << 3); + inc.resetDirtyFields(); + + r->setMonthlyDate(QList() << 3 << 1 << 2); + QVERIFY(inc.dirtyFields().empty()); + + r->setMonthlyDate(QList() << 3 << 1); + QCOMPARE(inc.dirtyFields(), QSet() << IncidenceBase::FieldRecurrence); +} + +void IncidenceTest::testRecurrenceMonthlyPos() +{ + QDate dt = QDate::currentDate(); + RecurrenceRule::WDayPos pos1(1,2); + RecurrenceRule::WDayPos pos2(3,4); + RecurrenceRule::WDayPos pos3(1,2); + Event inc; + inc.setDtStart(KDateTime(dt)); + KCalCore::Recurrence *r = inc.recurrence(); + r->setYearly(1); + r->setMonthlyPos(QList() << pos1 << pos2); + inc.resetDirtyFields(); + + //TODO: test sorting + r->setMonthlyPos(QList() << pos1 << pos2); + QVERIFY(inc.dirtyFields().empty()); + + r->setMonthlyPos(QList() << pos3); + QCOMPARE(inc.dirtyFields(), QSet() << IncidenceBase::FieldRecurrence); +} + +void IncidenceTest::testRecurrenceYearlyDay() +{ + QDate dt = QDate::currentDate(); + Event inc; + inc.setDtStart(KDateTime(dt)); + KCalCore::Recurrence *r = inc.recurrence(); + r->setYearly(1); + r->setYearlyDay(QList() << 1 << 2 << 3); + inc.resetDirtyFields(); + + r->setYearlyDay(QList() << 3 << 1 << 2); + QVERIFY(inc.dirtyFields().empty()); + + r->setYearlyDay(QList() << 3 << 1); + QCOMPARE(inc.dirtyFields(), QSet() << IncidenceBase::FieldRecurrence); +} + +void IncidenceTest::testRecurrenceYearlyMonth() +{ + QDate dt = QDate::currentDate(); + Event inc; + inc.setDtStart(KDateTime(dt)); + KCalCore::Recurrence *r = inc.recurrence(); + r->setYearly(1); + r->setYearlyMonth(QList() << 1 << 2 << 3); + inc.resetDirtyFields(); + + r->setYearlyMonth(QList() << 3 << 1 << 2); + QVERIFY(inc.dirtyFields().empty()); + + r->setYearlyMonth(QList() << 3 << 1); + QCOMPARE(inc.dirtyFields(), QSet() << IncidenceBase::FieldRecurrence); +} diff --git a/kcalcore/tests/testevent.h b/kcalcore/tests/testincidence.h similarity index 59% copy from kcalcore/tests/testevent.h copy to kcalcore/tests/testincidence.h index 509cb7146..b423eb989 100644 --- a/kcalcore/tests/testevent.h +++ b/kcalcore/tests/testincidence.h @@ -1,44 +1,46 @@ /* This file is part of the kcalcore library. - Copyright (c) 2006-2008 Allen Winter + Copyright (C) 2015 Sandro Knauß This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -#ifndef TESTEVENT_H -#define TESTEVENT_H +#ifndef TESTINCIDENCE_H +#define TESTINCIDENCE_H #include -class EventTest : public QObject +class IncidenceTest : public QObject { Q_OBJECT private Q_SLOTS: - void testSetRoles_data(); - void testSetRoles(); - void testValidity(); - void testCompare(); - void testClone(); - void testCopy(); - void testCopyIncidence(); - void testAssign(); - void testSerializer_data(); - void testSerializer(); - void testDurationDtEnd(); + void testDtStartChange(); + void testSummaryChange(); + void testLocationChange(); + + void testRecurrenceTypeChange(); + void testRecurrenceEndTimeChange(); + void testRecurrenceEndTimeDurationChange(); + void testRecurrenceDurationChange(); + void testRecurrenceExDatesChange(); + void testRecurrenceMonthlyPos(); + void testRecurrenceMonthlyDate(); + void testRecurrenceYearlyDay(); + void testRecurrenceYearlyMonth(); }; #endif