diff --git a/src/calendar.cpp b/src/calendar.cpp index 6470edfd0..f4273dfba 100644 --- a/src/calendar.cpp +++ b/src/calendar.cpp @@ -1,1370 +1,1370 @@ /* This file is part of the kcalcore library. Copyright (c) 1998 Preston Brown Copyright (c) 2000-2004 Cornelius Schumacher Copyright (C) 2003-2004 Reinhold Kainhofer Copyright (c) 2006 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. */ /** @file This file is part of the API for handling calendar data and defines the Calendar class. @brief Represents the main calendar class. @author Preston Brown \ @author Cornelius Schumacher \ @author Reinhold Kainhofer \ @author David Jarvie \ */ #include "calendar.h" #include "calendar_p.h" #include "calfilter.h" #include "icaltimezones_p.h" #include "sorting.h" #include "visitor.h" #include "utils.h" #include "kcalcore_debug.h" #include extern "C" { #include } #include // for std::remove() using namespace KCalCore; /** Make a QHash::value that returns a QVector. */ template QVector values(const QMultiHash &c) { QVector v; v.reserve(c.size()); for (typename QMultiHash::const_iterator it = c.begin(), end = c.end(); it != end; ++it) { v.push_back(it.value()); } return v; } template QVector values(const QMultiHash &c, const K &x) { QVector v; typename QMultiHash::const_iterator it = c.find(x); while (it != c.end() && it.key() == x) { v.push_back(it.value()); ++it; } return v; } /** Template for a class that implements a visitor for adding an Incidence to a resource supporting addEvent(), addTodo() and addJournal() calls. */ template class AddVisitor : public Visitor { public: AddVisitor(T *r) : mResource(r) {} bool visit(const Event::Ptr &e) override { return mResource->addEvent(e); } bool visit(const Todo::Ptr &t) override { return mResource->addTodo(t); } bool visit(const Journal::Ptr &j) override { return mResource->addJournal(j); } bool visit(const FreeBusy::Ptr &) override { return false; } private: T *mResource; }; /** Template for a class that implements a visitor for deleting an Incidence from a resource supporting deleteEvent(), deleteTodo() and deleteJournal() calls. */ template class DeleteVisitor : public Visitor { public: DeleteVisitor(T *r) : mResource(r) {} bool visit(const Event::Ptr &e) override { mResource->deleteEvent(e); return true; } bool visit(const Todo::Ptr &t) override { mResource->deleteTodo(t); return true; } bool visit(const Journal::Ptr &j) override { mResource->deleteJournal(j); return true; } bool visit(const FreeBusy::Ptr &) override { return false; } private: T *mResource; }; //@endcond Calendar::Calendar(const QTimeZone &timeZone) : d(new KCalCore::Calendar::Private) { d->mTimeZone = timeZone; } Calendar::Calendar(const QByteArray &timeZoneId) : d(new KCalCore::Calendar::Private) { setTimeZoneId(timeZoneId); } Calendar::~Calendar() { delete d; } Person::Ptr Calendar::owner() const { return d->mOwner; } void Calendar::setOwner(const Person::Ptr &owner) { Q_ASSERT(owner); d->mOwner = owner; setModified(true); } void Calendar::setTimeZone(const QTimeZone &timeZone) { d->mTimeZone = timeZone; doSetTimeZone(d->mTimeZone); } QTimeZone Calendar::timeZone() const { return d->mTimeZone; } void Calendar::setTimeZoneId(const QByteArray &timeZoneId) { d->mTimeZone = d->timeZoneIdSpec(timeZoneId); - doSetTimeZone(d->mTimeZone); + doSetTimeZone(d->mTimeZone); //NOLINT false clang-analyzer-optin.cplusplus.VirtualCall } //@cond PRIVATE QTimeZone Calendar::Private::timeZoneIdSpec(const QByteArray &timeZoneId) { if (timeZoneId == QByteArrayLiteral("UTC")) { return QTimeZone::utc(); } auto tz = QTimeZone(timeZoneId); if (tz.isValid()) { return tz; } return QTimeZone::systemTimeZone(); } //@endcond QByteArray Calendar::timeZoneId() const { return d->mTimeZone.id(); } void Calendar::shiftTimes(const QTimeZone &oldZone, const QTimeZone &newZone) { setTimeZone(newZone); int i, end; Event::List ev = events(); for (i = 0, end = ev.count(); i < end; ++i) { ev[i]->shiftTimes(oldZone, newZone); } Todo::List to = todos(); for (i = 0, end = to.count(); i < end; ++i) { to[i]->shiftTimes(oldZone, newZone); } Journal::List jo = journals(); for (i = 0, end = jo.count(); i < end; ++i) { jo[i]->shiftTimes(oldZone, newZone); } } void Calendar::setFilter(CalFilter *filter) { if (filter) { d->mFilter = filter; } else { d->mFilter = d->mDefaultFilter; } emit filterChanged(); } CalFilter *Calendar::filter() const { return d->mFilter; } QStringList Calendar::categories() const { Incidence::List rawInc(rawIncidences()); QStringList cats, thisCats; // @TODO: For now just iterate over all incidences. In the future, // the list of categories should be built when reading the file. for (Incidence::List::ConstIterator i = rawInc.constBegin(); i != rawInc.constEnd(); ++i) { thisCats = (*i)->categories(); for (QStringList::ConstIterator si = thisCats.constBegin(); si != thisCats.constEnd(); ++si) { if (!cats.contains(*si)) { cats.append(*si); } } } return cats; } Incidence::List Calendar::incidences(const QDate &date) const { return mergeIncidenceList(events(date), todos(date), journals(date)); } Incidence::List Calendar::incidences() const { return mergeIncidenceList(events(), todos(), journals()); } Incidence::List Calendar::rawIncidences() const { return mergeIncidenceList(rawEvents(), rawTodos(), rawJournals()); } Incidence::List Calendar::instances(const Incidence::Ptr &incidence) const { if (incidence) { Event::List elist; Todo::List tlist; Journal::List jlist; if (incidence->type() == Incidence::TypeEvent) { elist = eventInstances(incidence); } else if (incidence->type() == Incidence::TypeTodo) { tlist = todoInstances(incidence); } else if (incidence->type() == Incidence::TypeJournal) { jlist = journalInstances(incidence); } return mergeIncidenceList(elist, tlist, jlist); } else { return Incidence::List(); } } Incidence::List Calendar::duplicates(const Incidence::Ptr &incidence) { if (incidence) { Incidence::List list; Incidence::List vals = values(d->mNotebookIncidences); Incidence::List::const_iterator it; for (it = vals.constBegin(); it != vals.constEnd(); ++it) { if (((incidence->dtStart() == (*it)->dtStart()) || (!incidence->dtStart().isValid() && !(*it)->dtStart().isValid())) && (incidence->summary() == (*it)->summary())) { list.append(*it); } } return list; } else { return Incidence::List(); } } bool Calendar::addNotebook(const QString ¬ebook, bool isVisible) { if (d->mNotebooks.contains(notebook)) { return false; } else { d->mNotebooks.insert(notebook, isVisible); return true; } } bool Calendar::updateNotebook(const QString ¬ebook, bool isVisible) { if (!d->mNotebooks.contains(notebook)) { return false; } else { d->mNotebooks.insert(notebook, isVisible); return true; } } bool Calendar::deleteNotebook(const QString ¬ebook) { if (!d->mNotebooks.contains(notebook)) { return false; } else { return d->mNotebooks.remove(notebook); } } bool Calendar::setDefaultNotebook(const QString ¬ebook) { if (!d->mNotebooks.contains(notebook)) { return false; } else { d->mDefaultNotebook = notebook; return true; } } QString Calendar::defaultNotebook() const { return d->mDefaultNotebook; } bool Calendar::hasValidNotebook(const QString ¬ebook) const { return d->mNotebooks.contains(notebook); } bool Calendar::isVisible(const Incidence::Ptr &incidence) const { if (d->mIncidenceVisibility.contains(incidence)) { return d->mIncidenceVisibility[incidence]; } const QString nuid = notebook(incidence); bool rv; if (d->mNotebooks.contains(nuid)) { rv = d->mNotebooks.value(nuid); } else { // NOTE returns true also for nonexisting notebooks for compatibility rv = true; } d->mIncidenceVisibility[incidence] = rv; return rv; } void Calendar::clearNotebookAssociations() { d->mNotebookIncidences.clear(); d->mUidToNotebook.clear(); d->mIncidenceVisibility.clear(); } bool Calendar::setNotebook(const Incidence::Ptr &inc, const QString ¬ebook) { if (!inc) { return false; } if (!notebook.isEmpty() && !incidence(inc->uid(), inc->recurrenceId())) { qCWarning(KCALCORE_LOG) << "cannot set notebook until incidence has been added"; return false; } if (d->mUidToNotebook.contains(inc->uid())) { QString old = d->mUidToNotebook.value(inc->uid()); if (!old.isEmpty() && notebook != old) { if (inc->hasRecurrenceId()) { qCWarning(KCALCORE_LOG) << "cannot set notebook for child incidences"; return false; } // Move all possible children also. Incidence::List list = instances(inc); Incidence::List::Iterator it; for (it = list.begin(); it != list.end(); ++it) { d->mNotebookIncidences.remove(old, *it); d->mNotebookIncidences.insert(notebook, *it); } notifyIncidenceChanged(inc); // for removing from old notebook // don not remove from mUidToNotebook to keep deleted incidences d->mNotebookIncidences.remove(old, inc); } } if (!notebook.isEmpty()) { d->mUidToNotebook.insert(inc->uid(), notebook); d->mNotebookIncidences.insert(notebook, inc); qCDebug(KCALCORE_LOG) << "setting notebook" << notebook << "for" << inc->uid(); notifyIncidenceChanged(inc); // for inserting into new notebook } return true; } QString Calendar::notebook(const Incidence::Ptr &incidence) const { if (incidence) { return d->mUidToNotebook.value(incidence->uid()); } else { return QString(); } } QString Calendar::notebook(const QString &uid) const { return d->mUidToNotebook.value(uid); } QStringList Calendar::notebooks() const { return d->mNotebookIncidences.uniqueKeys(); } Incidence::List Calendar::incidences(const QString ¬ebook) const { if (notebook.isEmpty()) { return values(d->mNotebookIncidences); } else { return values(d->mNotebookIncidences, notebook); } } /** static */ Event::List Calendar::sortEvents(const Event::List &eventList, EventSortField sortField, SortDirection sortDirection) { if (eventList.isEmpty()) { return Event::List(); } Event::List eventListSorted; // Notice we alphabetically presort Summaries first. // We do this so comparison "ties" stay in a nice order. eventListSorted = eventList; switch (sortField) { case EventSortUnsorted: break; case EventSortStartDate: if (sortDirection == SortDirectionAscending) { std::sort(eventListSorted.begin(), eventListSorted.end(), Events::startDateLessThan); } else { std::sort(eventListSorted.begin(), eventListSorted.end(), Events::startDateMoreThan); } break; case EventSortEndDate: if (sortDirection == SortDirectionAscending) { std::sort(eventListSorted.begin(), eventListSorted.end(), Events::endDateLessThan); } else { std::sort(eventListSorted.begin(), eventListSorted.end(), Events::endDateMoreThan); } break; case EventSortSummary: if (sortDirection == SortDirectionAscending) { std::sort(eventListSorted.begin(), eventListSorted.end(), Events::summaryLessThan); } else { std::sort(eventListSorted.begin(), eventListSorted.end(), Events::summaryMoreThan); } break; } return eventListSorted; } Event::List Calendar::events(const QDate &date, const QTimeZone &timeZone, EventSortField sortField, SortDirection sortDirection) const { Event::List el = rawEventsForDate(date, timeZone, sortField, sortDirection); d->mFilter->apply(&el); return el; } Event::List Calendar::events(const QDateTime &dt) const { Event::List el = rawEventsForDate(dt); d->mFilter->apply(&el); return el; } Event::List Calendar::events(const QDate &start, const QDate &end, const QTimeZone &timeZone, bool inclusive) const { Event::List el = rawEvents(start, end, timeZone, inclusive); d->mFilter->apply(&el); return el; } Event::List Calendar::events(EventSortField sortField, SortDirection sortDirection) const { Event::List el = rawEvents(sortField, sortDirection); d->mFilter->apply(&el); return el; } bool Calendar::addIncidence(const Incidence::Ptr &incidence) { if (!incidence) { return false; } AddVisitor v(this); return incidence->accept(v, incidence); } bool Calendar::deleteIncidence(const Incidence::Ptr &incidence) { if (!incidence) { return false; } if (beginChange(incidence)) { DeleteVisitor v(this); const bool result = incidence->accept(v, incidence); endChange(incidence); return result; } else { return false; } } Incidence::Ptr Calendar::createException(const Incidence::Ptr &incidence, const QDateTime &recurrenceId, bool thisAndFuture) { Q_ASSERT(recurrenceId.isValid()); if (!incidence || !incidence->recurs() || !recurrenceId.isValid()) { return Incidence::Ptr(); } Incidence::Ptr newInc(incidence->clone()); newInc->setCreated(QDateTime::currentDateTimeUtc()); newInc->setRevision(0); //Recurring exceptions are not support for now newInc->clearRecurrence(); newInc->setRecurrenceId(recurrenceId); newInc->setThisAndFuture(thisAndFuture); newInc->setDtStart(recurrenceId); // Calculate and set the new end of the incidence QDateTime end = incidence->dateTime(IncidenceBase::RoleEnd); if (end.isValid()) { if (incidence->allDay()) { qint64 offset = incidence->dtStart().daysTo(recurrenceId); end = end.addDays(offset); } else { qint64 offset = incidence->dtStart().secsTo(recurrenceId); end = end.addSecs(offset); } newInc->setDateTime(end, IncidenceBase::RoleEnd); } return newInc; } Incidence::Ptr Calendar::incidence(const QString &uid, const QDateTime &recurrenceId) const { Incidence::Ptr i = event(uid, recurrenceId); if (i) { return i; } i = todo(uid, recurrenceId); if (i) { return i; } i = journal(uid, recurrenceId); return i; } Incidence::Ptr Calendar::deleted(const QString &uid, const QDateTime &recurrenceId) const { Incidence::Ptr i = deletedEvent(uid, recurrenceId); if (i) { return i; } i = deletedTodo(uid, recurrenceId); if (i) { return i; } i = deletedJournal(uid, recurrenceId); return i; } Incidence::List Calendar::incidencesFromSchedulingID(const QString &sid) const { Incidence::List result; const Incidence::List incidences = rawIncidences(); Incidence::List::const_iterator it = incidences.begin(); for (; it != incidences.end(); ++it) { if ((*it)->schedulingID() == sid) { result.append(*it); } } return result; } Incidence::Ptr Calendar::incidenceFromSchedulingID(const QString &uid) const { const Incidence::List incidences = rawIncidences(); Incidence::List::const_iterator it = incidences.begin(); for (; it != incidences.end(); ++it) { if ((*it)->schedulingID() == uid) { // Touchdown, and the crowd goes wild return *it; } } // Not found return Incidence::Ptr(); } /** static */ Todo::List Calendar::sortTodos(const Todo::List &todoList, TodoSortField sortField, SortDirection sortDirection) { if (todoList.isEmpty()) { return Todo::List(); } Todo::List todoListSorted; // Notice we alphabetically presort Summaries first. // We do this so comparison "ties" stay in a nice order. // Note that To-dos may not have Start DateTimes nor due DateTimes. todoListSorted = todoList; switch (sortField) { case TodoSortUnsorted: break; case TodoSortStartDate: if (sortDirection == SortDirectionAscending) { std::sort(todoListSorted.begin(), todoListSorted.end(), Todos::startDateLessThan); } else { std::sort(todoListSorted.begin(), todoListSorted.end(), Todos::startDateMoreThan); } break; case TodoSortDueDate: if (sortDirection == SortDirectionAscending) { std::sort(todoListSorted.begin(), todoListSorted.end(), Todos::dueDateLessThan); } else { std::sort(todoListSorted.begin(), todoListSorted.end(), Todos::dueDateMoreThan); } break; case TodoSortPriority: if (sortDirection == SortDirectionAscending) { std::sort(todoListSorted.begin(), todoListSorted.end(), Todos::priorityLessThan); } else { std::sort(todoListSorted.begin(), todoListSorted.end(), Todos::priorityMoreThan); } break; case TodoSortPercentComplete: if (sortDirection == SortDirectionAscending) { std::sort(todoListSorted.begin(), todoListSorted.end(), Todos::percentLessThan); } else { std::sort(todoListSorted.begin(), todoListSorted.end(), Todos::percentMoreThan); } break; case TodoSortSummary: if (sortDirection == SortDirectionAscending) { std::sort(todoListSorted.begin(), todoListSorted.end(), Todos::summaryLessThan); } else { std::sort(todoListSorted.begin(), todoListSorted.end(), Todos::summaryMoreThan); } break; case TodoSortCreated: if (sortDirection == SortDirectionAscending) { std::sort(todoListSorted.begin(), todoListSorted.end(), Todos::createdLessThan); } else { std::sort(todoListSorted.begin(), todoListSorted.end(), Todos::createdMoreThan); } break; } return todoListSorted; } Todo::List Calendar::todos(TodoSortField sortField, SortDirection sortDirection) const { Todo::List tl = rawTodos(sortField, sortDirection); d->mFilter->apply(&tl); return tl; } Todo::List Calendar::todos(const QDate &date) const { Todo::List el = rawTodosForDate(date); d->mFilter->apply(&el); return el; } Todo::List Calendar::todos(const QDate &start, const QDate &end, const QTimeZone &timeZone, bool inclusive) const { Todo::List tl = rawTodos(start, end, timeZone, inclusive); d->mFilter->apply(&tl); return tl; } /** static */ Journal::List Calendar::sortJournals(const Journal::List &journalList, JournalSortField sortField, SortDirection sortDirection) { if (journalList.isEmpty()) { return Journal::List(); } Journal::List journalListSorted = journalList; switch (sortField) { case JournalSortUnsorted: break; case JournalSortDate: if (sortDirection == SortDirectionAscending) { std::sort(journalListSorted.begin(), journalListSorted.end(), Journals::dateLessThan); } else { std::sort(journalListSorted.begin(), journalListSorted.end(), Journals::dateMoreThan); } break; case JournalSortSummary: if (sortDirection == SortDirectionAscending) { std::sort(journalListSorted.begin(), journalListSorted.end(), Journals::summaryLessThan); } else { std::sort(journalListSorted.begin(), journalListSorted.end(), Journals::summaryMoreThan); } break; } return journalListSorted; } Journal::List Calendar::journals(JournalSortField sortField, SortDirection sortDirection) const { Journal::List jl = rawJournals(sortField, sortDirection); d->mFilter->apply(&jl); return jl; } Journal::List Calendar::journals(const QDate &date) const { Journal::List el = rawJournalsForDate(date); d->mFilter->apply(&el); return el; } // When this is called, the to-dos have already been added to the calendar. // This method is only about linking related to-dos. void Calendar::setupRelations(const Incidence::Ptr &forincidence) { if (!forincidence) { return; } const QString uid = forincidence->uid(); // First, go over the list of orphans and see if this is their parent Incidence::List l = values(d->mOrphans, uid); d->mOrphans.remove(uid); if (!l.isEmpty()) { Incidence::List &relations = d->mIncidenceRelations[uid]; relations.reserve(relations.count() + l.count()); for (int i = 0, end = l.count(); i < end; ++i) { relations.append(l[i]); d->mOrphanUids.remove(l[i]->uid()); } } // Now see about this incidences parent if (forincidence->relatedTo().isEmpty() && !forincidence->relatedTo().isEmpty()) { // Incidence has a uid it is related to but is not registered to it yet. // Try to find it Incidence::Ptr parent = incidence(forincidence->relatedTo()); if (parent) { // Found it // look for hierarchy loops if (isAncestorOf(forincidence, parent)) { forincidence->setRelatedTo(QString()); qCWarning(KCALCORE_LOG) << "hierarchy loop between " << forincidence->uid() << " and " << parent->uid(); } else { d->mIncidenceRelations[parent->uid()].append(forincidence); } } else { // Not found, put this in the mOrphans list // Note that the mOrphans dict might contain multiple entries with the // same key! which are multiple children that wait for the parent // incidence to be inserted. d->mOrphans.insert(forincidence->relatedTo(), forincidence); d->mOrphanUids.insert(forincidence->uid(), forincidence); } } } // If a to-do with sub-to-dos is deleted, move it's sub-to-dos to the orphan list void Calendar::removeRelations(const Incidence::Ptr &incidence) { if (!incidence) { qCDebug(KCALCORE_LOG) << "Warning: incidence is 0"; return; } const QString uid = incidence->uid(); for (const Incidence::Ptr &i : qAsConst(d->mIncidenceRelations[uid])) { if (!d->mOrphanUids.contains(i->uid())) { d->mOrphans.insert(uid, i); d->mOrphanUids.insert(i->uid(), i); i->setRelatedTo(uid); } } const QString parentUid = incidence->relatedTo(); // If this incidence is related to something else, tell that about it if (!parentUid.isEmpty()) { Incidence::List &relations = d->mIncidenceRelations[parentUid]; relations.erase( std::remove(relations.begin(), relations.end(), incidence), relations.end()); } // Remove this one from the orphans list if (d->mOrphanUids.remove(uid)) { // This incidence is located in the orphans list - it should be removed // Since the mOrphans dict might contain the same key (with different // child incidence pointers!) multiple times, take care that we remove // the correct one. So we need to remove all items with the given // parent UID, and readd those that are not for this item. Also, there // might be other entries with differnet UID that point to this // incidence (this might happen when the relatedTo of the item is // changed before its parent is inserted. This might happen with // groupware servers....). Remove them, too QStringList relatedToUids; // First, create a list of all keys in the mOrphans list which point // to the removed item relatedToUids << incidence->relatedTo(); for (QMultiHash::Iterator it = d->mOrphans.begin(); it != d->mOrphans.end(); ++it) { if (it.value()->uid() == uid) { relatedToUids << it.key(); } } // now go through all uids that have one entry that point to the incidence for (QStringList::const_iterator uidit = relatedToUids.constBegin(); uidit != relatedToUids.constEnd(); ++uidit) { Incidence::List tempList; // Remove all to get access to the remaining entries const Incidence::List l = values(d->mOrphans, *uidit); d->mOrphans.remove(*uidit); for (const Incidence::Ptr &i : l) { if (i != incidence) { tempList.append(i); } } // Readd those that point to a different orphan incidence for (Incidence::List::Iterator incit = tempList.begin(); incit != tempList.end(); ++incit) { d->mOrphans.insert(*uidit, *incit); } } } // Make sure the deleted incidence doesn't relate to a non-deleted incidence, // since that would cause trouble in MemoryCalendar::close(), as the deleted // incidences are destroyed after the non-deleted incidences. The destructor // of the deleted incidences would then try to access the already destroyed // non-deleted incidence, which would segfault. // // So in short: Make sure dead incidences don't point to alive incidences // via the relation. // // This crash is tested in MemoryCalendarTest::testRelationsCrash(). // incidence->setRelatedTo( Incidence::Ptr() ); } bool Calendar::isAncestorOf(const Incidence::Ptr &ancestor, const Incidence::Ptr &incidence) const { if (!incidence || incidence->relatedTo().isEmpty()) { return false; } else if (incidence->relatedTo() == ancestor->uid()) { return true; } else { return isAncestorOf(ancestor, this->incidence(incidence->relatedTo())); } } Incidence::List Calendar::relations(const QString &uid) const { return d->mIncidenceRelations[uid]; } Calendar::CalendarObserver::~CalendarObserver() { } void Calendar::CalendarObserver::calendarModified(bool modified, Calendar *calendar) { Q_UNUSED(modified); Q_UNUSED(calendar); } void Calendar::CalendarObserver::calendarIncidenceAdded(const Incidence::Ptr &incidence) { Q_UNUSED(incidence); } void Calendar::CalendarObserver::calendarIncidenceChanged(const Incidence::Ptr &incidence) { Q_UNUSED(incidence); } void Calendar::CalendarObserver::calendarIncidenceAboutToBeDeleted(const Incidence::Ptr &incidence) { Q_UNUSED(incidence); } void Calendar::CalendarObserver::calendarIncidenceDeleted(const Incidence::Ptr &incidence, const Calendar *calendar) { Q_UNUSED(incidence); Q_UNUSED(calendar); } void Calendar::CalendarObserver::calendarIncidenceAdditionCanceled(const Incidence::Ptr &incidence) { Q_UNUSED(incidence); } void Calendar::registerObserver(CalendarObserver *observer) { if (!observer) { return; } if (!d->mObservers.contains(observer)) { d->mObservers.append(observer); } else { d->mNewObserver = true; } } void Calendar::unregisterObserver(CalendarObserver *observer) { if (!observer) { return; } else { d->mObservers.removeAll(observer); } } bool Calendar::isSaving() const { return false; } void Calendar::setModified(bool modified) { if (modified != d->mModified || d->mNewObserver) { d->mNewObserver = false; for (CalendarObserver *observer : qAsConst(d->mObservers)) { observer->calendarModified(modified, this); } d->mModified = modified; } } bool Calendar::isModified() const { return d->mModified; } bool Calendar::save() { return true; } bool Calendar::reload() { return true; } void Calendar::incidenceUpdated(const QString &uid, const QDateTime &recurrenceId) { Incidence::Ptr inc = incidence(uid, recurrenceId); if (!inc) { return; } inc->setLastModified(QDateTime::currentDateTimeUtc()); // we should probably update the revision number here, // or internally in the Event itself when certain things change. // need to verify with ical documentation. notifyIncidenceChanged(inc); setModified(true); } void Calendar::doSetTimeZone(const QTimeZone &timeZone) { Q_UNUSED(timeZone); } void Calendar::notifyIncidenceAdded(const Incidence::Ptr &incidence) { if (!incidence) { return; } if (!d->mObserversEnabled) { return; } for (CalendarObserver *observer : qAsConst(d->mObservers)) { observer->calendarIncidenceAdded(incidence); } for (auto role : { IncidenceBase::RoleStartTimeZone, IncidenceBase::RoleEndTimeZone }) { const auto dt = incidence->dateTime(role); if (dt.isValid() && dt.timeZone() != QTimeZone::utc()) { if (!d->mTimeZones.contains(dt.timeZone())) { d->mTimeZones.push_back(dt.timeZone()); } } } } void Calendar::notifyIncidenceChanged(const Incidence::Ptr &incidence) { if (!incidence) { return; } if (!d->mObserversEnabled) { return; } for (CalendarObserver *observer : qAsConst(d->mObservers)) { observer->calendarIncidenceChanged(incidence); } } void Calendar::notifyIncidenceAboutToBeDeleted(const Incidence::Ptr &incidence) { if (!incidence) { return; } if (!d->mObserversEnabled) { return; } for (CalendarObserver *observer : qAsConst(d->mObservers)) { observer->calendarIncidenceAboutToBeDeleted(incidence); } } void Calendar::notifyIncidenceDeleted(const Incidence::Ptr &incidence) { if (!incidence) { return; } if (!d->mObserversEnabled) { return; } for (CalendarObserver *observer : qAsConst(d->mObservers)) { observer->calendarIncidenceDeleted(incidence, this); } } void Calendar::notifyIncidenceAdditionCanceled(const Incidence::Ptr &incidence) { if (!incidence) { return; } if (!d->mObserversEnabled) { return; } for (CalendarObserver *observer : qAsConst(d->mObservers)) { observer->calendarIncidenceAdditionCanceled(incidence); } } void Calendar::customPropertyUpdated() { setModified(true); } void Calendar::setProductId(const QString &id) { d->mProductId = id; } QString Calendar::productId() const { return d->mProductId; } /** static */ Incidence::List Calendar::mergeIncidenceList(const Event::List &events, const Todo::List &todos, const Journal::List &journals) { Incidence::List incidences; incidences.reserve(events.count() + todos.count() + journals.count()); int i, end; for (i = 0, end = events.count(); i < end; ++i) { incidences.append(events[i]); } for (i = 0, end = todos.count(); i < end; ++i) { incidences.append(todos[i]); } for (i = 0, end = journals.count(); i < end; ++i) { incidences.append(journals[i]); } return incidences; } bool Calendar::beginChange(const Incidence::Ptr &incidence) { Q_UNUSED(incidence); return true; } bool Calendar::endChange(const Incidence::Ptr &incidence) { Q_UNUSED(incidence); return true; } void Calendar::setObserversEnabled(bool enabled) { d->mObserversEnabled = enabled; } void Calendar::appendAlarms(Alarm::List &alarms, const Incidence::Ptr &incidence, const QDateTime &from, const QDateTime &to) const { QDateTime preTime = from.addSecs(-1); Alarm::List alarmlist = incidence->alarms(); for (int i = 0, iend = alarmlist.count(); i < iend; ++i) { if (alarmlist[i]->enabled()) { QDateTime dt = alarmlist[i]->nextRepetition(preTime); if (dt.isValid() && dt <= to) { qCDebug(KCALCORE_LOG) << incidence->summary() << "':" << dt.toString(); alarms.append(alarmlist[i]); } } } } void Calendar::appendRecurringAlarms(Alarm::List &alarms, const Incidence::Ptr &incidence, const QDateTime &from, const QDateTime &to) const { QDateTime dt; bool endOffsetValid = false; Duration endOffset(0); Duration period(from, to); Alarm::List alarmlist = incidence->alarms(); for (int i = 0, iend = alarmlist.count(); i < iend; ++i) { Alarm::Ptr a = alarmlist[i]; if (a->enabled()) { if (a->hasTime()) { // The alarm time is defined as an absolute date/time dt = a->nextRepetition(from.addSecs(-1)); if (!dt.isValid() || dt > to) { continue; } } else { // Alarm time is defined by an offset from the event start or end time. // Find the offset from the event start time, which is also used as the // offset from the recurrence time. Duration offset(0); if (a->hasStartOffset()) { offset = a->startOffset(); } else if (a->hasEndOffset()) { offset = a->endOffset(); if (!endOffsetValid) { endOffset = Duration(incidence->dtStart(), incidence->dateTime(Incidence::RoleAlarmEndOffset)); endOffsetValid = true; } } // Find the incidence's earliest alarm QDateTime alarmStart = offset.end(a->hasEndOffset() ? incidence->dateTime(Incidence::RoleAlarmEndOffset) : incidence->dtStart()); if (alarmStart > to) { continue; } QDateTime baseStart = incidence->dtStart(); if (from > alarmStart) { alarmStart = from; // don't look earlier than the earliest alarm baseStart = (-offset).end((-endOffset).end(alarmStart)); } // Adjust the 'alarmStart' date/time and find the next recurrence at or after it. // Treate the two offsets separately in case one is daily and the other not. dt = incidence->recurrence()->getNextDateTime(baseStart.addSecs(-1)); if (!dt.isValid() || (dt = endOffset.end(offset.end(dt))) > to) { // adjust 'dt' to get the alarm time // The next recurrence is too late. if (!a->repeatCount()) { continue; } // The alarm has repetitions, so check whether repetitions of previous // recurrences fall within the time period. bool found = false; Duration alarmDuration = a->duration(); for (QDateTime base = baseStart; (dt = incidence->recurrence()->getPreviousDateTime(base)).isValid(); base = dt) { if (a->duration().end(dt) < base) { break; // this recurrence's last repetition is too early, so give up } // The last repetition of this recurrence is at or after 'alarmStart' time. // Check if a repetition occurs between 'alarmStart' and 'to'. int snooze = a->snoozeTime().value(); // in seconds or days if (a->snoozeTime().isDaily()) { Duration toFromDuration(dt, base); int toFrom = toFromDuration.asDays(); if (a->snoozeTime().end(from) <= to || (toFromDuration.isDaily() && toFrom % snooze == 0) || (toFrom / snooze + 1) * snooze <= toFrom + period.asDays()) { found = true; #ifndef NDEBUG // for debug output dt = offset.end(dt).addDays(((toFrom - 1) / snooze + 1) * snooze); #endif break; } } else { int toFrom = dt.secsTo(base); if (period.asSeconds() >= snooze || toFrom % snooze == 0 || (toFrom / snooze + 1) * snooze <= toFrom + period.asSeconds()) { found = true; #ifndef NDEBUG // for debug output dt = offset.end(dt).addSecs(((toFrom - 1) / snooze + 1) * snooze); #endif break; } } } if (!found) { continue; } } } qCDebug(KCALCORE_LOG) << incidence->summary() << "':" << dt.toString(); alarms.append(a); } } } void Calendar::startBatchAdding() { d->batchAddingInProgress = true; } void Calendar::endBatchAdding() { d->batchAddingInProgress = false; } bool Calendar::batchAdding() const { return d->batchAddingInProgress; } void Calendar::setDeletionTracking(bool enable) { d->mDeletionTracking = enable; } bool Calendar::deletionTracking() const { return d->mDeletionTracking; } void Calendar::virtual_hook(int id, void *data) { Q_UNUSED(id); Q_UNUSED(data); Q_ASSERT(false); } diff --git a/src/freebusy.cpp b/src/freebusy.cpp index 0fc0d00ba..6ffd7b6ab 100644 --- a/src/freebusy.cpp +++ b/src/freebusy.cpp @@ -1,434 +1,433 @@ /* This file is part of the kcalcore library. Copyright (c) 2001 Cornelius Schumacher Copyright (C) 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 FreeBusy class. @brief Provides information about the free/busy time of a calendar user. @author Cornelius Schumacher \ @author Reinhold Kainhofer \ */ #include "freebusy.h" #include "visitor.h" #include "utils.h" #include "icalformat.h" #include "kcalcore_debug.h" #include using namespace KCalCore; //@cond PRIVATE class Q_DECL_HIDDEN KCalCore::FreeBusy::Private { private: FreeBusy *q; public: Private(FreeBusy *qq) : q(qq) {} Private(const KCalCore::FreeBusy::Private &other, FreeBusy *qq) : q(qq) { init(other); } Private(const FreeBusyPeriod::List &busyPeriods, FreeBusy *qq) : q(qq), mBusyPeriods(busyPeriods) {} void init(const KCalCore::FreeBusy::Private &other); void init(const Event::List &events, const QDateTime &start, const QDateTime &end); QDateTime mDtEnd; // end datetime FreeBusyPeriod::List mBusyPeriods; // list of periods // This is used for creating a freebusy object for the current user bool addLocalPeriod(FreeBusy *fb, const QDateTime &start, const QDateTime &end); }; void KCalCore::FreeBusy::Private::init(const KCalCore::FreeBusy::Private &other) { mDtEnd = other.mDtEnd; mBusyPeriods = other.mBusyPeriods; } //@endcond FreeBusy::FreeBusy() : d(new KCalCore::FreeBusy::Private(this)) { } FreeBusy::FreeBusy(const FreeBusy &other) : IncidenceBase(other), d(new KCalCore::FreeBusy::Private(*other.d, this)) { } FreeBusy::FreeBusy(const QDateTime &start, const QDateTime &end) : d(new KCalCore::FreeBusy::Private(this)) { - setDtStart(start); - setDtEnd(end); + setDtStart(start); //NOLINT false clang-analyzer-optin.cplusplus.VirtualCall + setDtEnd(end); //NOLINT false clang-analyzer-optin.cplusplus.VirtualCall } FreeBusy::FreeBusy(const Event::List &events, const QDateTime &start, const QDateTime &end) : d(new KCalCore::FreeBusy::Private(this)) { - setDtStart(start); - setDtEnd(end); + setDtStart(start); //NOLINT false clang-analyzer-optin.cplusplus.VirtualCall + setDtEnd(end); //NOLINT false clang-analyzer-optin.cplusplus.VirtualCall d->init(events, start, end); } //@cond PRIVATE void FreeBusy::Private::init(const Event::List &eventList, const QDateTime &start, const QDateTime &end) { qint64 extraDays; int i; int x; qint64 duration = start.daysTo(end); QDate day; QDateTime tmpStart; QDateTime tmpEnd; // Loops through every event in the calendar Event::List::ConstIterator it; for (it = eventList.constBegin(); it != eventList.constEnd(); ++it) { Event::Ptr event = *it; // If this event is transparent it shouldn't be in the freebusy list. if (event->transparency() == Event::Transparent) { continue; } // The code below can not handle all-day events. Fixing this resulted // in a lot of duplicated code. Instead, make a copy of the event and // set the period to the full day(s). This trick works for recurring, // multiday, and single day all-day events. Event::Ptr allDayEvent; if (event->allDay()) { // addDay event. Do the hack qCDebug(KCALCORE_LOG) << "All-day event"; allDayEvent = Event::Ptr(new Event(*event)); // Set the start and end times to be on midnight QDateTime st = allDayEvent->dtStart(); st.setTime(QTime(0, 0)); QDateTime nd = allDayEvent->dtEnd(); nd.setTime(QTime(23, 59, 59, 999)); allDayEvent->setAllDay(false); allDayEvent->setDtStart(st); allDayEvent->setDtEnd(nd); qCDebug(KCALCORE_LOG) << "Use:" << st.toString() << "to" << nd.toString(); // Finally, use this event for the setting below event = allDayEvent; } // This whole for loop is for recurring events, it loops through // each of the days of the freebusy request for (i = 0; i <= duration; ++i) { day = start.addDays(i).date(); tmpStart.setDate(day); tmpEnd.setDate(day); if (event->recurs()) { if (event->isMultiDay()) { // FIXME: This doesn't work for sub-daily recurrences or recurrences with // a different time than the original event. extraDays = event->dtStart().daysTo(event->dtEnd()); for (x = 0; x <= extraDays; ++x) { if (event->recursOn(day.addDays(-x), start.timeZone())) { tmpStart.setDate(day.addDays(-x)); tmpStart.setTime(event->dtStart().time()); tmpEnd = event->duration().end(tmpStart); addLocalPeriod(q, tmpStart, tmpEnd); break; } } } else { if (event->recursOn(day, start.timeZone())) { tmpStart.setTime(event->dtStart().time()); tmpEnd.setTime(event->dtEnd().time()); addLocalPeriod(q, tmpStart, tmpEnd); } } } } // Non-recurring events addLocalPeriod(q, event->dtStart(), event->dtEnd()); } q->sortList(); } //@endcond FreeBusy::FreeBusy(const Period::List &busyPeriods) : d(new KCalCore::FreeBusy::Private(this)) { addPeriods(busyPeriods); } FreeBusy::FreeBusy(const FreeBusyPeriod::List &busyPeriods) : d(new KCalCore::FreeBusy::Private(busyPeriods, this)) { } FreeBusy::~FreeBusy() { delete d; } IncidenceBase::IncidenceType FreeBusy::type() const { return TypeFreeBusy; } QByteArray FreeBusy::typeStr() const { return QByteArrayLiteral("FreeBusy"); } void FreeBusy::setDtStart(const QDateTime &start) { IncidenceBase::setDtStart(start.toUTC()); updated(); } void FreeBusy::setDtEnd(const QDateTime &end) { d->mDtEnd = end; } QDateTime FreeBusy::dtEnd() const { return d->mDtEnd; } Period::List FreeBusy::busyPeriods() const { Period::List res; res.reserve(d->mBusyPeriods.count()); for (const FreeBusyPeriod &p : qAsConst(d->mBusyPeriods)) { res << p; } return res; } FreeBusyPeriod::List FreeBusy::fullBusyPeriods() const { return d->mBusyPeriods; } void FreeBusy::sortList() { std::sort(d->mBusyPeriods.begin(), d->mBusyPeriods.end()); - return; } void FreeBusy::addPeriods(const Period::List &list) { d->mBusyPeriods.reserve(d->mBusyPeriods.count() + list.count()); for (const Period &p : qAsConst(list)) { d->mBusyPeriods << FreeBusyPeriod(p); } sortList(); } void FreeBusy::addPeriods(const FreeBusyPeriod::List &list) { d->mBusyPeriods += list; sortList(); } void FreeBusy::addPeriod(const QDateTime &start, const QDateTime &end) { d->mBusyPeriods.append(FreeBusyPeriod(start, end)); sortList(); } void FreeBusy::addPeriod(const QDateTime &start, const Duration &duration) { d->mBusyPeriods.append(FreeBusyPeriod(start, duration)); sortList(); } void FreeBusy::merge(const FreeBusy::Ptr &freeBusy) { if (freeBusy->dtStart() < dtStart()) { setDtStart(freeBusy->dtStart()); } if (freeBusy->dtEnd() > dtEnd()) { setDtEnd(freeBusy->dtEnd()); } Period::List periods = freeBusy->busyPeriods(); Period::List::ConstIterator it; d->mBusyPeriods.reserve(d->mBusyPeriods.count() + periods.count()); for (it = periods.constBegin(); it != periods.constEnd(); ++it) { d->mBusyPeriods.append(FreeBusyPeriod((*it).start(), (*it).end())); } sortList(); } void FreeBusy::shiftTimes(const QTimeZone &oldZone, const QTimeZone &newZone) { if (oldZone.isValid() && newZone.isValid() && oldZone != newZone) { IncidenceBase::shiftTimes(oldZone, newZone); d->mDtEnd = d->mDtEnd.toTimeZone(oldZone); d->mDtEnd.setTimeZone(newZone); for (FreeBusyPeriod p : qAsConst(d->mBusyPeriods)) { p.shiftTimes(oldZone, newZone); } } } IncidenceBase &FreeBusy::assign(const IncidenceBase &other) { if (&other != this) { IncidenceBase::assign(other); const FreeBusy *f = static_cast(&other); d->init(*(f->d)); } return *this; } bool FreeBusy::equals(const IncidenceBase &freeBusy) const { if (!IncidenceBase::equals(freeBusy)) { return false; } else { // If they weren't the same type IncidenceBase::equals would had returned false already const FreeBusy *fb = static_cast(&freeBusy); return dtEnd() == fb->dtEnd() && d->mBusyPeriods == fb->d->mBusyPeriods; } } bool FreeBusy::accept(Visitor &v, const IncidenceBase::Ptr &incidence) { return v.visit(incidence.staticCast()); } QDateTime FreeBusy::dateTime(DateTimeRole role) const { Q_UNUSED(role); // No roles affecting freeBusy yet return QDateTime(); } void FreeBusy::setDateTime(const QDateTime &dateTime, DateTimeRole role) { Q_UNUSED(dateTime); Q_UNUSED(role); } void FreeBusy::virtual_hook(VirtualHook id, void *data) { Q_UNUSED(id); Q_UNUSED(data); Q_ASSERT(false); } //@cond PRIVATE bool FreeBusy::Private::addLocalPeriod(FreeBusy *fb, const QDateTime &eventStart, const QDateTime &eventEnd) { QDateTime tmpStart; QDateTime tmpEnd; //Check to see if the start *or* end of the event is //between the start and end of the freebusy dates. QDateTime start = fb->dtStart(); if (!(((start.secsTo(eventStart) >= 0) && (eventStart.secsTo(mDtEnd) >= 0)) || ((start.secsTo(eventEnd) >= 0) && (eventEnd.secsTo(mDtEnd) >= 0)))) { return false; } if (eventStart.secsTo(start) >= 0) { tmpStart = start; } else { tmpStart = eventStart; } if (eventEnd.secsTo(mDtEnd) <= 0) { tmpEnd = mDtEnd; } else { tmpEnd = eventEnd; } FreeBusyPeriod p(tmpStart, tmpEnd); mBusyPeriods.append(p); return true; } //@endcond QLatin1String FreeBusy::mimeType() const { return FreeBusy::freeBusyMimeType(); } QLatin1String KCalCore::FreeBusy::freeBusyMimeType() { return QLatin1String("application/x-vnd.akonadi.calendar.freebusy"); } QDataStream &KCalCore::operator<<(QDataStream &stream, const KCalCore::FreeBusy::Ptr &freebusy) { KCalCore::ICalFormat format; QString data = format.createScheduleMessage(freebusy, iTIPPublish); return stream << data; } QDataStream &KCalCore::operator>>(QDataStream &stream, KCalCore::FreeBusy::Ptr &freebusy) { QString freeBusyVCal; stream >> freeBusyVCal; KCalCore::ICalFormat format; freebusy = format.parseFreeBusy(freeBusyVCal); if (!freebusy) { qCDebug(KCALCORE_LOG) << "Error parsing free/busy"; qCDebug(KCALCORE_LOG) << freeBusyVCal; } return stream; } diff --git a/src/incidence.cpp b/src/incidence.cpp index 8d6137bdd..7c13859d9 100644 --- a/src/incidence.cpp +++ b/src/incidence.cpp @@ -1,1217 +1,1217 @@ /* 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 "utils.h" #include #include #include // for .toHtmlEscaped() and Qt::mightBeRichText() #include #include #include #include using namespace KCalCore; /** Private class that helps to provide binary compatibility between releases. @internal */ //@cond PRIVATE class Q_DECL_HIDDEN KCalCore::Incidence::Private { public: Private() : mGeoLatitude(INVALID_LATLON) , mGeoLongitude(INVALID_LATLON) , mRecurrence(nullptr) , mRevision(0) , mPriority(0) , mStatus(StatusNone) , mSecrecy(SecrecyPublic) , mDescriptionIsRich(false) , mSummaryIsRich(false) , mLocationIsRich(false) , mHasGeo(false) , mThisAndFuture(false) , mLocalOnly(false) { } Private(const Private &p) : mCreated(p.mCreated) , mDescription(p.mDescription) , mSummary(p.mSummary) , mLocation(p.mLocation) , mCategories(p.mCategories) , mResources(p.mResources) , mStatusString(p.mStatusString) , mSchedulingID(p.mSchedulingID) , mRelatedToUid(p.mRelatedToUid) , mRecurrenceId(p.mRecurrenceId) , mGeoLatitude(p.mGeoLatitude) , mGeoLongitude(p.mGeoLongitude) , mRecurrence(nullptr) , mRevision(p.mRevision) , mPriority(p.mPriority) , mStatus(p.mStatus) , mSecrecy(p.mSecrecy) , mDescriptionIsRich(p.mDescriptionIsRich) , mSummaryIsRich(p.mSummaryIsRich) , mLocationIsRich(p.mLocationIsRich) , mHasGeo(p.mHasGeo) , mThisAndFuture(p.mThisAndFuture) , mLocalOnly(false) { } void clear() { mAlarms.clear(); mAttachments.clear(); delete mRecurrence; mRecurrence = nullptr; } 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...) mAlarms.reserve(src.d->mAlarms.count()); for (const Alarm::Ptr &alarm : qAsConst(src.d->mAlarms)) { Alarm::Ptr b(new Alarm(*alarm.data())); b->setParent(dest); mAlarms.append(b); } mAttachments.reserve(src.d->mAttachments.count()); for (const Attachment::Ptr &attachment : qAsConst(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 = nullptr; } } QDateTime mCreated; // creation datetime QString mDescription; // description string QString mSummary; // summary string QString mLocation; // location string QStringList mCategories; // category list Attachment::List mAttachments; // attachments list Alarm::List mAlarms; // alarms list QStringList mResources; // resources list (not calendar resources) QString mStatusString; // status string, for custom status QString mSchedulingID; // ID for scheduling mails QMap mRelatedToUid; // incidence uid this is related to, for each relType QHash mTempFiles; // Temporary files for writing attachments to. QDateTime mRecurrenceId; // recurrenceId float mGeoLatitude; // Specifies latitude in decimal degrees float mGeoLongitude; // Specifies longitude in decimal degrees mutable Recurrence *mRecurrence; // recurrence int mRevision; // revision number int mPriority; // priority: 1 = highest, 2 = less, etc. Status mStatus; // status Secrecy mSecrecy; // secrecy bool mDescriptionIsRich = false; // description string is richtext. bool mSummaryIsRich = false; // summary string is richtext. bool mLocationIsRich = false; // location string is richtext. bool mHasGeo = false; // if incidence has geo data bool mThisAndFuture = false; bool mLocalOnly = false; // 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 for (const Alarm::Ptr &alarm : qAsConst(d->mAlarms)) { alarm->setParent(nullptr); } clearTempFiles(); 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); const auto alarmList = alarms(); const auto otherAlarmsList = i2->alarms(); if (alarmList.count() != otherAlarmsList.count()) { return false; } Alarm::List::ConstIterator a1 = alarmList.constBegin(); Alarm::List::ConstIterator a1end = alarmList.constEnd(); Alarm::List::ConstIterator a2 = otherAlarmsList.constBegin(); Alarm::List::ConstIterator a2end = otherAlarmsList.constEnd(); for (; a1 != a1end && a2 != a2end; ++a1, ++a2) { if (**a1 == **a2) { continue; } else { return false; } } const auto attachmentList = attachments(); const auto otherAttachmentList = i2->attachments(); if (attachmentList.count() != otherAttachmentList.count()) { return false; } Attachment::List::ConstIterator att1 = attachmentList.constBegin(); const Attachment::List::ConstIterator att1end = attachmentList.constEnd(); Attachment::List::ConstIterator att2 = otherAttachmentList.constBegin(); const Attachment::List::ConstIterator att2end = otherAttachmentList.constEnd(); for (; att1 != att1end && att2 != att2end; ++att1, ++att2) { if (**att1 == **att2) { continue; } else { return false; } } bool recurrenceEqual = (d->mRecurrence == nullptr && i2->d->mRecurrence == nullptr); if (!recurrenceEqual) { recurrence(); // create if doesn't exist i2->recurrence(); // create if doesn't exist recurrenceEqual = d->mRecurrence != nullptr && i2->d->mRecurrence != nullptr && *d->mRecurrence == *i2->d->mRecurrence; } if (d->mHasGeo == i2->d->mHasGeo) { if (d->mHasGeo && (!qFuzzyCompare(d->mGeoLatitude, i2->d->mGeoLatitude) || !qFuzzyCompare(d->mGeoLongitude, i2->d->mGeoLongitude))) { return false; } } else { return false; } 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(Qt::ISODate); } return uid(); } void Incidence::recreate() { const QDateTime nowUTC = QDateTime::currentDateTimeUtc(); setCreated(nowUTC); setSchedulingID(QString(), CalFormat::createUniqueId()); setRevision(0); - setLastModified(nowUTC); + setLastModified(nowUTC); //NOLINT false clang-analyzer-optin.cplusplus.VirtualCall } void Incidence::setLastModified(const QDateTime &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 QDateTime &created) { if (mReadOnly || d->mLocalOnly) { return; } d->mCreated = created.toUTC(); const auto ct = d->mCreated.time(); // Remove milliseconds d->mCreated.setTime(QTime(ct.hour(), ct.minute(), ct.second())); setFieldDirty(FieldCreated); // FIXME: Shouldn't we call updated for the creation date, too? // updated(); } QDateTime 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 QDateTime &dt) { IncidenceBase::setDtStart(dt); if (d->mRecurrence && dirtyFields().contains(FieldDtStart)) { d->mRecurrence->setStartDateTime(dt, allDay()); } } void Incidence::shiftTimes(const QTimeZone &oldZone, const QTimeZone &newZone) { IncidenceBase::shiftTimes(oldZone, newZone); if (d->mRecurrence) { d->mRecurrence->shiftTimes(oldZone, newZone); } for (int i = 0, end = d->mAlarms.count(); i < end; ++i) { d->mAlarms[i]->shiftTimes(oldZone, newZone); } } 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 d->mDescription.toHtmlEscaped().replace(QLatin1Char('\n'), QStringLiteral("
")); } } bool Incidence::descriptionIsRich() const { return d->mDescriptionIsRich; } void Incidence::setSummary(const QString &summary, bool isRich) { if (mReadOnly) { return; } 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 d->mSummary.toHtmlEscaped().replace(QLatin1Char('\n'), QStringLiteral("
")); } } 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(QLatin1Char(',')); } 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), allDay()); 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 = nullptr; } 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 QTimeZone &timeZone) const { return d->mRecurrence && d->mRecurrence->recursOn(date, timeZone); } bool Incidence::recursAt(const QDateTime &qdt) const { return d->mRecurrence && d->mRecurrence->recursAt(qdt); } QList Incidence::startDateTimesForDate(const QDate &date, const QTimeZone &timeZone) const { QDateTime start = dtStart(); QDateTime 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, QDateTime kdate(date, {}, timeZone); if (!recurs()) { if (!(start > kdate || end < kdate)) { result << start; } return result; } qint64 days = start.daysTo(end); // Account for possible recurrences going over midnight, while the original event doesn't QDate tmpday(date.addDays(-days - 1)); QDateTime tmp; while (tmpday <= date) { if (recurrence()->recursOn(tmpday, timeZone)) { const QList times = recurrence()->recurTimesOn(tmpday, timeZone); for (const QTime &time : times) { tmp = QDateTime(tmpday, time, start.timeZone()); if (endDateForStart(tmp) >= kdate) { result << tmp; } } } tmpday = tmpday.addDays(1); } return result; } QList Incidence::startDateTimesForDateTime(const QDateTime &datetime) const { QDateTime start = dtStart(); QDateTime 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; } qint64 days = start.daysTo(end); // Account for possible recurrences going over midnight, while the original event doesn't QDate tmpday(datetime.date().addDays(-days - 1)); QDateTime tmp; while (tmpday <= datetime.date()) { if (recurrence()->recursOn(tmpday, datetime.timeZone())) { // Get the times during the day (in start date's time zone) when recurrences happen const QList times = recurrence()->recurTimesOn(tmpday, start.timeZone()); for (const QTime &time : times) { tmp = QDateTime(tmpday, time, start.timeZone()); if (!(tmp > datetime || endDateForStart(tmp) < datetime)) { result << tmp; } } } tmpday = tmpday.addDays(1); } return result; } QDateTime Incidence::endDateForStart(const QDateTime &startDt) const { QDateTime start = dtStart(); QDateTime 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; for (const Attachment::Ptr &attachment : qAsConst(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 { const QString attachementPath = d->mTempFiles.value(attachment); if (!attachementPath.isEmpty()) { return attachementPath; } QTemporaryFile file; QMimeDatabase mimeDb; QStringList patterns = mimeDb.mimeTypeForName(attachment->mimeType()).globPatterns(); if (!patterns.empty()) { file.setFileTemplate(file.fileTemplate() + QString(patterns.first()).remove(QLatin1Char('*'))); } file.setAutoRemove(false); 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 f(it.value()); // On Windows the file must be writeable before we can remove it f.setPermissions(QFile::WriteUser); f.remove(); } 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 { for (const Alarm::Ptr &alarm : qAsConst(d->mAlarms)) { if (alarm->enabled()) { return true; } } return false; } void Incidence::setLocation(const QString &location, bool isRich) { if (mReadOnly) { return; } 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 d->mLocation.toHtmlEscaped().replace(QLatin1Char('\n'), QStringLiteral("
")); } } 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 (allDay() && d->mRecurrenceId.date().isValid()) || d->mRecurrenceId.isValid(); } QDateTime 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 QDateTime &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 QStringLiteral("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); } } /** static */ QStringList Incidence::mimeTypes() { return QStringList() << QStringLiteral("text/calendar") << KCalCore::Event::eventMimeType() << KCalCore::Todo::todoMimeType() << KCalCore::Journal::journalMimeType(); } void Incidence::serialize(QDataStream &out) { serializeQDateTimeAsKDateTime(out, d->mCreated); out << 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; serializeQDateTimeAsKDateTime(out, d->mRecurrenceId); out << 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; } for (const Attachment::Ptr &attachment : qAsConst(d->mAttachments)) { out << attachment; } for (const Alarm::Ptr &alarm : qAsConst(d->mAlarms)) { out << alarm; } } void Incidence::deserialize(QDataStream &in) { quint32 status, secrecy; bool hasRecurrence; int attachmentCount, alarmCount; QMap relatedToUid; deserializeKDateTimeAsQDateTime(in, d->mCreated); in >> 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; deserializeKDateTimeAsQDateTime(in, d->mRecurrenceId); in >> 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(); d->mAttachments.reserve(attachmentCount); for (int i = 0; i < attachmentCount; ++i) { Attachment::Ptr attachment = Attachment::Ptr(new Attachment(QString())); in >> attachment; d->mAttachments.append(attachment); } d->mAlarms.reserve(alarmCount); for (int i = 0; i < alarmCount; ++i) { Alarm::Ptr alarm = Alarm::Ptr(new Alarm(this)); in >> alarm; d->mAlarms.append(alarm); } d->mStatus = static_cast(status); d->mSecrecy = static_cast(secrecy); d->mRelatedToUid.clear(); auto it = relatedToUid.cbegin(), end = relatedToUid.cend(); for (; it != end; ++it) { d->mRelatedToUid.insert(static_cast(it.key()), it.value()); } } diff --git a/src/memorycalendar.cpp b/src/memorycalendar.cpp index 5e9ec346c..f68e9a99c 100644 --- a/src/memorycalendar.cpp +++ b/src/memorycalendar.cpp @@ -1,872 +1,872 @@ /* This file is part of the kcalcore library. Copyright (c) 1998 Preston Brown Copyright (c) 2001,2003,2004 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 MemoryCalendar class. @brief This class provides a calendar stored as a local file. @author Preston Brown \ @author Cornelius Schumacher \ */ #include "memorycalendar.h" #include "kcalcore_debug.h" #include "utils.h" #include "calformat.h" #include template static QVector values(const QMultiHash &c) { QVector v; v.reserve(c.size()); for (typename QMultiHash::const_iterator it = c.begin(), end = c.end(); it != end; ++it) { v.push_back(it.value()); } return v; } template static QVector values(const QMultiHash &c, const K &x) { QVector v; typename QMultiHash::const_iterator it = c.find(x); while (it != c.end() && it.key() == x) { v.push_back(it.value()); ++it; } return v; } using namespace KCalCore; /** Private class that helps to provide binary compatibility between releases. @internal */ //@cond PRIVATE class Q_DECL_HIDDEN KCalCore::MemoryCalendar::Private { public: Private(MemoryCalendar *qq) : q(qq), mFormat(nullptr) { } ~Private() { } MemoryCalendar *q; CalFormat *mFormat; // calendar format QString mIncidenceBeingUpdated; // Instance identifier of Incidence currently being updated /** * List of all incidences. * First indexed by incidence->type(), then by incidence->uid(); */ QMap > mIncidences; /** * Has all incidences, indexed by identifier. */ QHash mIncidencesByIdentifier; /** * List of all deleted incidences. * First indexed by incidence->type(), then by incidence->uid(); */ QMap > mDeletedIncidences; /** * Contains incidences ( to-dos; non-recurring, non-multiday events; journals; ) * indexed by start/due date. * * The QMap key is the incidence->type(). * The QMultiHash key is the dtStart/dtDue().toString() * * Note: We had 3 variables, mJournalsForDate, mTodosForDate and mEventsForDate * but i merged them into one (indexed by type) because it simplifies code using * it. No need to if else based on type. */ QMap > mIncidencesForDate; void insertIncidence(const Incidence::Ptr &incidence); Incidence::Ptr incidence(const QString &uid, - const IncidenceBase::IncidenceType type, + IncidenceBase::IncidenceType type, const QDateTime &recurrenceId = {}) const; Incidence::Ptr deletedIncidence(const QString &uid, const QDateTime &recurrenceId, - const IncidenceBase::IncidenceType type) const; + IncidenceBase::IncidenceType type) const; - void deleteAllIncidences(const IncidenceBase::IncidenceType type); + void deleteAllIncidences(IncidenceBase::IncidenceType type); }; //@endcond MemoryCalendar::MemoryCalendar(const QTimeZone &timeZone) : Calendar(timeZone), d(new KCalCore::MemoryCalendar::Private(this)) { } MemoryCalendar::MemoryCalendar(const QByteArray &timeZoneId) : Calendar(timeZoneId), d(new KCalCore::MemoryCalendar::Private(this)) { } MemoryCalendar::~MemoryCalendar() { - close(); + close(); //NOLINT false clang-analyzer-optin.cplusplus.VirtualCall delete d; } void MemoryCalendar::close() { setObserversEnabled(false); // Don't call the virtual function deleteEvents() etc, the base class might have // other ways of deleting the data. d->deleteAllIncidences(Incidence::TypeEvent); d->deleteAllIncidences(Incidence::TypeTodo); d->deleteAllIncidences(Incidence::TypeJournal); d->mIncidencesByIdentifier.clear(); d->mDeletedIncidences.clear(); setModified(false); setObserversEnabled(true); } bool MemoryCalendar::deleteIncidence(const Incidence::Ptr &incidence) { // Handle orphaned children // relations is an Incidence's property, not a Todo's, so // we remove relations in deleteIncidence, not in deleteTodo. removeRelations(incidence); const Incidence::IncidenceType type = incidence->type(); const QString uid = incidence->uid(); if (d->mIncidences[type].contains(uid, incidence)) { // Notify while the incidence is still available, // this is necessary so korganizer still has time to query for exceptions notifyIncidenceAboutToBeDeleted(incidence); d->mIncidences[type].remove(uid, incidence); d->mIncidencesByIdentifier.remove(incidence->instanceIdentifier()); setModified(true); if (deletionTracking()) { d->mDeletedIncidences[type].insert(uid, incidence); } const QDateTime dt = incidence->dateTime(Incidence::RoleCalendarHashing); if (dt.isValid()) { d->mIncidencesForDate[type].remove(dt.date().toString(), incidence); } // Delete child-incidences. if (!incidence->hasRecurrenceId()) { deleteIncidenceInstances(incidence); } notifyIncidenceDeleted(incidence); return true; } else { qCWarning(KCALCORE_LOG) << incidence->typeStr() << " not found. uid=" << uid; return false; } } bool MemoryCalendar::deleteIncidenceInstances(const Incidence::Ptr &incidence) { const Incidence::IncidenceType type = incidence->type(); Incidence::List values = ::values(d->mIncidences[type], incidence->uid()); for (auto it = values.constBegin(); it != values.constEnd(); ++it) { Incidence::Ptr i = *it; if (i->hasRecurrenceId()) { qCDebug(KCALCORE_LOG) << "deleting child" << ", type=" << int(type) << ", uid=" << i->uid() // << ", start=" << i->dtStart() << " from calendar"; deleteIncidence(i); } } return true; } //@cond PRIVATE -void MemoryCalendar::Private::deleteAllIncidences(const Incidence::IncidenceType incidenceType) +void MemoryCalendar::Private::deleteAllIncidences(Incidence::IncidenceType incidenceType) { QHashIteratori(mIncidences[incidenceType]); while (i.hasNext()) { i.next(); q->notifyIncidenceAboutToBeDeleted(i.value()); i.value()->unRegisterObserver(q); } mIncidences[incidenceType].clear(); mIncidencesForDate[incidenceType].clear(); } Incidence::Ptr MemoryCalendar::Private::incidence(const QString &uid, - const Incidence::IncidenceType type, + Incidence::IncidenceType type, const QDateTime &recurrenceId) const { Incidence::List values = ::values(mIncidences[type], uid); for (auto it = values.constBegin(); it != values.constEnd(); ++it) { Incidence::Ptr i = *it; if (recurrenceId.isNull()) { if (!i->hasRecurrenceId()) { return i; } } else { if (i->hasRecurrenceId() && i->recurrenceId() == recurrenceId) { return i; } } } return Incidence::Ptr(); } Incidence::Ptr MemoryCalendar::Private::deletedIncidence(const QString &uid, - const QDateTime &recurrenceId, - const IncidenceBase::IncidenceType type) const + const QDateTime &recurrenceId, + IncidenceBase::IncidenceType type) const { if (!q->deletionTracking()) { return Incidence::Ptr(); } Incidence::List values = ::values(mDeletedIncidences[type], uid); for (auto it = values.constBegin(); it != values.constEnd(); ++it) { Incidence::Ptr i = *it; if (recurrenceId.isNull()) { if (!i->hasRecurrenceId()) { return i; } } else { if (i->hasRecurrenceId() && i->recurrenceId() == recurrenceId) { return i; } } } return Incidence::Ptr(); } void MemoryCalendar::Private::insertIncidence(const Incidence::Ptr &incidence) { const QString uid = incidence->uid(); const Incidence::IncidenceType type = incidence->type(); if (!mIncidences[type].contains(uid, incidence)) { mIncidences[type].insert(uid, incidence); mIncidencesByIdentifier.insert(incidence->instanceIdentifier(), incidence); const QDateTime dt = incidence->dateTime(Incidence::RoleCalendarHashing); if (dt.isValid()) { mIncidencesForDate[type].insert(dt.date().toString(), incidence); } } else { #ifndef NDEBUG // if we already have an to-do with this UID, it must be the same incidence, // otherwise something's really broken Q_ASSERT(mIncidences[type].value(uid) == incidence); #endif } } //@endcond bool MemoryCalendar::addIncidence(const Incidence::Ptr &incidence) { d->insertIncidence(incidence); notifyIncidenceAdded(incidence); incidence->registerObserver(this); setupRelations(incidence); setModified(true); return true; } bool MemoryCalendar::addEvent(const Event::Ptr &event) { return addIncidence(event); } bool MemoryCalendar::deleteEvent(const Event::Ptr &event) { return deleteIncidence(event); } bool MemoryCalendar::deleteEventInstances(const Event::Ptr &event) { return deleteIncidenceInstances(event); } Event::Ptr MemoryCalendar::event(const QString &uid, const QDateTime &recurrenceId) const { return d->incidence(uid, Incidence::TypeEvent, recurrenceId).staticCast(); } Event::Ptr MemoryCalendar::deletedEvent(const QString &uid, const QDateTime &recurrenceId) const { return d->deletedIncidence(uid, recurrenceId, Incidence::TypeEvent).staticCast(); } bool MemoryCalendar::addTodo(const Todo::Ptr &todo) { return addIncidence(todo); } bool MemoryCalendar::deleteTodo(const Todo::Ptr &todo) { return deleteIncidence(todo); } bool MemoryCalendar::deleteTodoInstances(const Todo::Ptr &todo) { return deleteIncidenceInstances(todo); } Todo::Ptr MemoryCalendar::todo(const QString &uid, const QDateTime &recurrenceId) const { return d->incidence(uid, Incidence::TypeTodo, recurrenceId).staticCast(); } Todo::Ptr MemoryCalendar::deletedTodo(const QString &uid, const QDateTime &recurrenceId) const { return d->deletedIncidence(uid, recurrenceId, Incidence::TypeTodo).staticCast(); } Todo::List MemoryCalendar::rawTodos(TodoSortField sortField, SortDirection sortDirection) const { Todo::List todoList; todoList.reserve(d->mIncidences[Incidence::TypeTodo].count()); QHashIteratori(d->mIncidences[Incidence::TypeTodo]); while (i.hasNext()) { i.next(); todoList.append(i.value().staticCast()); } return Calendar::sortTodos(todoList, sortField, sortDirection); } Todo::List MemoryCalendar::deletedTodos(TodoSortField sortField, SortDirection sortDirection) const { if (!deletionTracking()) { return Todo::List(); } Todo::List todoList; todoList.reserve(d->mDeletedIncidences[Incidence::TypeTodo].count()); QHashIteratori(d->mDeletedIncidences[Incidence::TypeTodo]); while (i.hasNext()) { i.next(); todoList.append(i.value().staticCast()); } return Calendar::sortTodos(todoList, sortField, sortDirection); } Todo::List MemoryCalendar::todoInstances(const Incidence::Ptr &todo, TodoSortField sortField, SortDirection sortDirection) const { Todo::List list; Incidence::List values = ::values(d->mIncidences[Incidence::TypeTodo], todo->uid()); for (auto it = values.constBegin(); it != values.constEnd(); ++it) { Todo::Ptr t = (*it).staticCast(); if (t->hasRecurrenceId()) { list.append(t); } } return Calendar::sortTodos(list, sortField, sortDirection); } Todo::List MemoryCalendar::rawTodosForDate(const QDate &date) const { Todo::List todoList; Todo::Ptr t; const QString dateStr = date.toString(); QMultiHash::const_iterator it = d->mIncidencesForDate[Incidence::TypeTodo].constFind(dateStr); while (it != d->mIncidencesForDate[Incidence::TypeTodo].constEnd() && it.key() == dateStr) { t = it.value().staticCast(); todoList.append(t); ++it; } // Iterate over all todos. Look for recurring todoss that occur on this date QHashIteratori(d->mIncidences[Incidence::TypeTodo]); while (i.hasNext()) { i.next(); t = i.value().staticCast(); if (t->recurs()) { if (t->recursOn(date, timeZone())) { todoList.append(t); } } } return todoList; } Todo::List MemoryCalendar::rawTodos(const QDate &start, const QDate &end, const QTimeZone &timeZone, bool inclusive) const { Q_UNUSED(inclusive); // use only exact dtDue/dtStart, not dtStart and dtEnd Todo::List todoList; const auto ts = timeZone.isValid() ? timeZone : this->timeZone(); QDateTime st(start, QTime(0, 0, 0), ts); QDateTime nd(end, QTime(23, 59, 59, 999), ts); // Get todos QHashIteratori(d->mIncidences[Incidence::TypeTodo]); Todo::Ptr todo; while (i.hasNext()) { i.next(); todo = i.value().staticCast(); if (!isVisible(todo)) { continue; } QDateTime rStart = todo->hasDueDate() ? todo->dtDue() : todo->hasStartDate() ? todo->dtStart() : QDateTime(); if (!rStart.isValid()) { continue; } if (!todo->recurs()) { // non-recurring todos if (nd.isValid() && nd < rStart) { continue; } if (st.isValid() && rStart < st) { continue; } } else { // recurring events switch (todo->recurrence()->duration()) { case -1: // infinite break; case 0: // end date given default: // count given QDateTime rEnd(todo->recurrence()->endDate(), QTime(23, 59, 59, 999), ts); if (!rEnd.isValid()) { continue; } if (st.isValid() && rEnd < st) { continue; } break; } // switch(duration) } //if(recurs) todoList.append(todo); } return todoList; } Alarm::List MemoryCalendar::alarmsTo(const QDateTime &to) const { return alarms(QDateTime(QDate(1900, 1, 1), QTime(0, 0, 0)), to); } Alarm::List MemoryCalendar::alarms(const QDateTime &from, const QDateTime &to, bool excludeBlockedAlarms) const { Q_UNUSED(excludeBlockedAlarms); Alarm::List alarmList; QHashIteratorie(d->mIncidences[Incidence::TypeEvent]); Event::Ptr e; while (ie.hasNext()) { ie.next(); e = ie.value().staticCast(); if (e->recurs()) { appendRecurringAlarms(alarmList, e, from, to); } else { appendAlarms(alarmList, e, from, to); } } QHashIteratorit(d->mIncidences[Incidence::TypeTodo]); Todo::Ptr t; while (it.hasNext()) { it.next(); t = it.value().staticCast(); if (!t->isCompleted()) { appendAlarms(alarmList, t, from, to); if (t->recurs()) { appendRecurringAlarms(alarmList, t, from, to); } else { appendAlarms(alarmList, t, from, to); } } } return alarmList; } void MemoryCalendar::incidenceUpdate(const QString &uid, const QDateTime &recurrenceId) { Incidence::Ptr inc = incidence(uid, recurrenceId); if (inc) { if (!d->mIncidenceBeingUpdated.isEmpty()) { qCWarning(KCALCORE_LOG) << "Incidence::update() called twice without an updated() call in between."; } // Save it so we can detect changes to uid or recurringId. d->mIncidenceBeingUpdated = inc->instanceIdentifier(); const QDateTime dt = inc->dateTime(Incidence::RoleCalendarHashing); if (dt.isValid()) { const Incidence::IncidenceType type = inc->type(); d->mIncidencesForDate[type].remove(dt.date().toString(), inc); } } } void MemoryCalendar::incidenceUpdated(const QString &uid, const QDateTime &recurrenceId) { Incidence::Ptr inc = incidence(uid, recurrenceId); if (inc) { if (d->mIncidenceBeingUpdated.isEmpty()) { qCWarning(KCALCORE_LOG) << "Incidence::updated() called twice without an update() call in between."; } else if (inc->instanceIdentifier() != d->mIncidenceBeingUpdated) { // Instance identifier changed, update our hash table d->mIncidencesByIdentifier.remove(d->mIncidenceBeingUpdated); d->mIncidencesByIdentifier.insert(inc->instanceIdentifier(), inc); } d->mIncidenceBeingUpdated = QString(); inc->setLastModified(QDateTime::currentDateTimeUtc()); // we should probably update the revision number here, // or internally in the Event itself when certain things change. // need to verify with ical documentation. const QDateTime dt = inc->dateTime(Incidence::RoleCalendarHashing); if (dt.isValid()) { const Incidence::IncidenceType type = inc->type(); d->mIncidencesForDate[type].insert(dt.date().toString(), inc); } notifyIncidenceChanged(inc); setModified(true); } } Event::List MemoryCalendar::rawEventsForDate(const QDate &date, const QTimeZone &timeZone, EventSortField sortField, SortDirection sortDirection) const { Event::List eventList; if (!date.isValid()) { // There can't be events on invalid dates return eventList; } Event::Ptr ev; // Find the hash for the specified date const QString dateStr = date.toString(); QMultiHash::const_iterator it = d->mIncidencesForDate[Incidence::TypeEvent].constFind(dateStr); // Iterate over all non-recurring, single-day events that start on this date const auto ts = timeZone.isValid() ? timeZone : this->timeZone(); while (it != d->mIncidencesForDate[Incidence::TypeEvent].constEnd() && it.key() == dateStr) { ev = it.value().staticCast(); QDateTime end(ev->dtEnd().toTimeZone(ev->dtStart().timeZone())); if (ev->allDay()) { end.setTime(QTime()); } else { end = end.addSecs(-1); } if (end.date() >= date) { eventList.append(ev); } ++it; } // Iterate over all events. Look for recurring events that occur on this date QHashIteratori(d->mIncidences[Incidence::TypeEvent]); while (i.hasNext()) { i.next(); ev = i.value().staticCast(); if (ev->recurs()) { if (ev->isMultiDay()) { int extraDays = ev->dtStart().date().daysTo(ev->dtEnd().date()); for (int i = 0; i <= extraDays; ++i) { if (ev->recursOn(date.addDays(-i), ts)) { eventList.append(ev); break; } } } else { if (ev->recursOn(date, ts)) { eventList.append(ev); } } } else { if (ev->isMultiDay()) { if (ev->dtStart().date() <= date && ev->dtEnd().date() >= date) { eventList.append(ev); } } } } return Calendar::sortEvents(eventList, sortField, sortDirection); } Event::List MemoryCalendar::rawEvents(const QDate &start, const QDate &end, const QTimeZone &timeZone, bool inclusive) const { Event::List eventList; const auto ts = timeZone.isValid() ? timeZone : this->timeZone(); QDateTime st(start, QTime(0, 0, 0), ts); QDateTime nd(end, QTime(23, 59, 59, 999), ts); // Get non-recurring events QHashIteratori(d->mIncidences[Incidence::TypeEvent]); Event::Ptr event; while (i.hasNext()) { i.next(); event = i.value().staticCast(); QDateTime rStart = event->dtStart(); if (nd < rStart) { continue; } if (inclusive && rStart < st) { continue; } if (!event->recurs()) { // non-recurring events QDateTime rEnd = event->dtEnd(); if (rEnd < st) { continue; } if (inclusive && nd < rEnd) { continue; } } else { // recurring events switch (event->recurrence()->duration()) { case -1: // infinite if (inclusive) { continue; } break; case 0: // end date given default: // count given QDateTime rEnd(event->recurrence()->endDate(), QTime(23, 59, 59, 999), ts); if (!rEnd.isValid()) { continue; } if (rEnd < st) { continue; } if (inclusive && nd < rEnd) { continue; } break; } // switch(duration) } //if(recurs) eventList.append(event); } return eventList; } Event::List MemoryCalendar::rawEventsForDate(const QDateTime &kdt) const { return rawEventsForDate(kdt.date(), kdt.timeZone()); } Event::List MemoryCalendar::rawEvents(EventSortField sortField, SortDirection sortDirection) const { Event::List eventList; eventList.reserve(d->mIncidences[Incidence::TypeEvent].count()); QHashIterator i(d->mIncidences[Incidence::TypeEvent]); while (i.hasNext()) { i.next(); eventList.append(i.value().staticCast()); } return Calendar::sortEvents(eventList, sortField, sortDirection); } Event::List MemoryCalendar::deletedEvents(EventSortField sortField, SortDirection sortDirection) const { if (!deletionTracking()) { return Event::List(); } Event::List eventList; eventList.reserve(d->mDeletedIncidences[Incidence::TypeEvent].count()); QHashIteratori(d->mDeletedIncidences[Incidence::TypeEvent]); while (i.hasNext()) { i.next(); eventList.append(i.value().staticCast()); } return Calendar::sortEvents(eventList, sortField, sortDirection); } Event::List MemoryCalendar::eventInstances(const Incidence::Ptr &event, EventSortField sortField, SortDirection sortDirection) const { Event::List list; Incidence::List values = ::values(d->mIncidences[Incidence::TypeEvent], event->uid()); for (auto it = values.constBegin(); it != values.constEnd(); ++it) { Event::Ptr ev = (*it).staticCast(); if (ev->hasRecurrenceId()) { list.append(ev); } } return Calendar::sortEvents(list, sortField, sortDirection); } bool MemoryCalendar::addJournal(const Journal::Ptr &journal) { return addIncidence(journal); } bool MemoryCalendar::deleteJournal(const Journal::Ptr &journal) { return deleteIncidence(journal); } bool MemoryCalendar::deleteJournalInstances(const Journal::Ptr &journal) { return deleteIncidenceInstances(journal); } Journal::Ptr MemoryCalendar::journal(const QString &uid, const QDateTime &recurrenceId) const { return d->incidence(uid, Incidence::TypeJournal, recurrenceId).staticCast(); } Journal::Ptr MemoryCalendar::deletedJournal(const QString &uid, const QDateTime &recurrenceId) const { return d->deletedIncidence(uid, recurrenceId, Incidence::TypeJournal).staticCast(); } Journal::List MemoryCalendar::rawJournals(JournalSortField sortField, SortDirection sortDirection) const { Journal::List journalList; QHashIteratori(d->mIncidences[Incidence::TypeJournal]); while (i.hasNext()) { i.next(); journalList.append(i.value().staticCast()); } return Calendar::sortJournals(journalList, sortField, sortDirection); } Journal::List MemoryCalendar::deletedJournals(JournalSortField sortField, SortDirection sortDirection) const { if (!deletionTracking()) { return Journal::List(); } Journal::List journalList; journalList.reserve(d->mDeletedIncidences[Incidence::TypeJournal].count()); QHashIteratori(d->mDeletedIncidences[Incidence::TypeJournal]); while (i.hasNext()) { i.next(); journalList.append(i.value().staticCast()); } return Calendar::sortJournals(journalList, sortField, sortDirection); } Journal::List MemoryCalendar::journalInstances(const Incidence::Ptr &journal, JournalSortField sortField, SortDirection sortDirection) const { Journal::List list; Incidence::List values = ::values(d->mIncidences[Incidence::TypeJournal], journal->uid()); for (auto it = values.constBegin(); it != values.constEnd(); ++it) { Journal::Ptr j = (*it).staticCast(); if (j->hasRecurrenceId()) { list.append(j); } } return Calendar::sortJournals(list, sortField, sortDirection); } Journal::List MemoryCalendar::rawJournalsForDate(const QDate &date) const { Journal::List journalList; Journal::Ptr j; QString dateStr = date.toString(); QMultiHash::const_iterator it = d->mIncidencesForDate[Incidence::TypeJournal].constFind(dateStr); while (it != d->mIncidencesForDate[Incidence::TypeJournal].constEnd() && it.key() == dateStr) { j = it.value().staticCast(); journalList.append(j); ++it; } return journalList; } Incidence::Ptr MemoryCalendar::instance(const QString &identifier) const { return d->mIncidencesByIdentifier.value(identifier); } void MemoryCalendar::virtual_hook(int id, void *data) { Q_UNUSED(id); Q_UNUSED(data); Q_ASSERT(false); } diff --git a/src/recurrencerule.cpp b/src/recurrencerule.cpp index 4c3d6256a..8d0f61c06 100644 --- a/src/recurrencerule.cpp +++ b/src/recurrencerule.cpp @@ -1,2332 +1,2331 @@ /* 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 "utils.h" #include "kcalcore_debug.h" #include "recurrencehelper_p.h" #include #include #include #include #include using namespace KCalCore; // Maximum number of intervals to process const int LOOP_LIMIT = 10000; #ifndef NDEBUG static QString dumpTime(const QDateTime &dt, bool allDay); // for debugging #endif /*========================================================================= = = = 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 = nullptr); static int getWeekNumberNeg(const QDate &date, short weekstart, int *year = nullptr); // 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 QStringLiteral("MO"); case 2: return QStringLiteral("TU"); case 3: return QStringLiteral("WE"); case 4: return QStringLiteral("TH"); case 5: return QStringLiteral("FR"); case 6: return QStringLiteral("SA"); case 7: return QStringLiteral("SU"); default: return QStringLiteral("??"); } } #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 qint64 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); qint64 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 QVector List; Constraint() {} explicit Constraint(const QTimeZone &, int wkst = 1); Constraint(const QDateTime &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; } 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.) QTimeZone timeZone; // time zone etc. to use bool readDateTime(const QDateTime &dt, RecurrenceRule::PeriodType type); bool matches(const QDate &dt, RecurrenceRule::PeriodType type) const; bool matches(const QDateTime &dt, RecurrenceRule::PeriodType type) const; bool merge(const Constraint &interval); bool isConsistent(RecurrenceRule::PeriodType period) const; bool increase(RecurrenceRule::PeriodType type, int freq); QDateTime 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 QDateTime cachedDt; }; Constraint::Constraint(const QTimeZone &timeZone, int wkst) : weekstart(wkst), timeZone(timeZone) { clear(); } Constraint::Constraint(const QDateTime &dt, RecurrenceRule::PeriodType type, int wkst) : weekstart(wkst), timeZone(dt.timeZone()) { 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; 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 = 0; 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 QDateTime &dt, RecurrenceRule::PeriodType type) const { if ((hour >= 0 && hour != dt.time().hour()) || (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). QDateTime 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.setDate(year, month, 1); subdaily = false; break; case RecurrenceRule::rYearly: d.setDate(year, 1, 1); subdaily = false; break; default: break; } if (subdaily) { d = DateHelper::getDate(year, (month > 0) ? month : 1, day ? day : 1); } cachedDt = QDateTime(d, t, timeZone); 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; if (!isConsistent(type)) { return result; } // TODO_Recurrence: Handle all-day QTime tm(hour, minute, second); bool done = false; if (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 { QDateTime dt(date, time, timeZone); if (dt.isValid()) { list.append(dt); } } bool Constraint::increase(RecurrenceRule::PeriodType type, int freq) { // convert the first day of the interval to QDateTime 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 QDateTime 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 QDateTime &dt, RecurrenceRule::PeriodType type) { switch (type) { // Really fall through! Only weekly needs to be treated differently! case RecurrenceRule::rSecondly: second = dt.time().second(); Q_FALLTHROUGH(); case RecurrenceRule::rMinutely: minute = dt.time().minute(); Q_FALLTHROUGH(); case RecurrenceRule::rHourly: hour = dt.time().hour(); Q_FALLTHROUGH(); case RecurrenceRule::rDaily: day = dt.date().day(); Q_FALLTHROUGH(); case RecurrenceRule::rMonthly: month = dt.date().month(); Q_FALLTHROUGH(); 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 Q_DECL_HIDDEN 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 QDateTime &preDate, PeriodType type) const; Constraint getPreviousValidDateInterval(const QDateTime &afterDate, PeriodType type) const; QList datesForInterval(const Constraint &interval, PeriodType type) const; RecurrenceRule *mParent; QString mRRule; // RRULE string PeriodType mPeriod; QDateTime 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; QDateTime 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 QList mCachedDates; mutable QDateTime mCachedDateEnd; mutable QDateTime 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) { d->mObservers.removeAll(observer); } void RecurrenceRule::setRecurrenceType(PeriodType period) { if (isReadOnly()) { return; } d->mPeriod = period; d->setDirty(); } QDateTime RecurrenceRule::endDt(bool *result) const { if (result) { *result = false; } if (d->mPeriod == rNone) { return QDateTime(); } if (d->mDuration < 0) { return QDateTime(); } 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 QDateTime(); } } if (result) { *result = true; } return d->mCachedDateEnd; } void RecurrenceRule::setEndDt(const QDateTime &dateTime) { if (isReadOnly()) { return; } d->mDateEnd = dateTime; 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 QDateTime &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 QTimeZone &oldTz, const QTimeZone &newTz) { d->mDateStart = d->mDateStart.toTimeZone(oldTz); d->mDateStart.setTimeZone(newTz); if (d->mDuration == 0) { d->mDateEnd = d->mDateEnd.toTimeZone(oldTz); d->mDateEnd.setTimeZone(newTz); } 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.timeZone()); 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 { \ tmp.reserve(mConstraints.count() * iend); \ 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; tmp.reserve(mConstraints.count() * mByDays.count()); 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()); } Q_FALLTHROUGH(); case rMonthly: if (mByDays.isEmpty() && mByWeekNumbers.isEmpty() && mByYearDays.isEmpty() && mByMonthDays.isEmpty()) { fixConstraint(setDay, mDateStart.date().day()); } Q_FALLTHROUGH(); case rWeekly: case rDaily: if (mByHours.isEmpty()) { fixConstraint(setHour, mDateStart.time().hour()); } Q_FALLTHROUGH(); case rHourly: if (mByMinutes.isEmpty()) { fixConstraint(setMinute, mDateStart.time().minute()); } Q_FALLTHROUGH(); case rMinutely: if (mBySeconds.isEmpty()) { fixConstraint(setSecond, mDateStart.time().second()); } Q_FALLTHROUGH(); 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)); auto dts = datesForInterval(interval, mPeriod); // Only use dates after the event has started (start date is only included // if it matches) const auto it = strictLowerBound(dts.begin(), dts.end(), mDateStart); if (it != dts.end()) { dts.erase(dts.begin(), it + 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() ) { // qCDebug(KCALCORE_LOG) << " -=>" << dumpTime(*it); // ++it; // } if (int(dts.count()) == mDuration) { mCachedDateEnd = dts.last(); return true; } else { // The cached date list is incomplete mCachedDateEnd = QDateTime(); mCachedLastDate = interval.intervalDateTime(mPeriod); return false; } } //@endcond bool RecurrenceRule::dateMatchesRules(const QDateTime &kdt) const { QDateTime dt = kdt.toTimeZone(d->mDateStart.timeZone()); 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 QTimeZone &timeZone) 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; } QDateTime start(qd, QTime(0, 0, 0), timeZone); //d->mDateStart.timeZone()); 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! QDateTime end = start.addDays(1); do { auto 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. QDateTime start(qd, QTime(0, 0, 0), timeZone); QDateTime end = start.addDays(1).toTimeZone(d->mDateStart.timeZone()); start = start.toTimeZone(d->mDateStart.timeZone()); 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) { QDateTime 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(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()).isValid() && 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 { auto dts = d->datesForInterval(interval, recurrenceType()); const auto it = std::lower_bound(dts.constBegin(), dts.constEnd(), start); if (it != dts.constEnd()) { return *it <= end; } interval.increase(recurrenceType(), frequency()); } while (interval.intervalDateTime(recurrenceType()).isValid() && interval.intervalDateTime(recurrenceType()) < end); return false; } bool RecurrenceRule::recursAt(const QDateTime &kdt) const { // Convert to the time spec used by this recurrence rule QDateTime dt(kdt.toTimeZone(d->mDateStart.timeZone())); if (allDay()) { return recursOn(dt.date(), dt.timeZone()); } 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(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 QTimeZone &timeZone) const { TimeList lst; if (allDay()) { return lst; } QDateTime start(date, QTime(0, 0, 0), timeZone); QDateTime end = start.addDays(1).addSecs(-1); auto dts = timesInInterval(start, end); // returns between start and end inclusive for (int i = 0, iend = dts.count(); i < iend; ++i) { lst += dts[i].toTimeZone(timeZone).time(); } return lst; } /** Returns the number of recurrences up to and including the date/time specified. */ int RecurrenceRule::durationTo(const QDateTime &dt) const { // Convert to the time spec used by this recurrence rule QDateTime toDate(dt.toTimeZone(d->mDateStart.timeZone())); // 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(toDate) / d->mTimedRepetition); } return timesInInterval(d->mDateStart, toDate).count(); } int RecurrenceRule::durationTo(const QDate &date) const { return durationTo(QDateTime(date, QTime(23, 59, 59), d->mDateStart.timeZone())); } QDateTime RecurrenceRule::getPreviousDate(const QDateTime &afterDate) const { // Convert to the time spec used by this recurrence rule QDateTime toDate(afterDate.toTimeZone(d->mDateStart.timeZone())); // Invalid starting point, or beyond end of recurrence if (!toDate.isValid() || toDate < d->mDateStart) { return QDateTime(); } if (d->mTimedRepetition) { // It's a simple sub-daily recurrence with no constraints QDateTime prev = toDate; if (d->mDuration >= 0 && endDt().isValid() && toDate > endDt()) { prev = endDt().addSecs(1).toTimeZone(d->mDateStart.timeZone()); } int n = static_cast((d->mDateStart.secsTo(prev) - 1) % d->mTimedRepetition); if (n < 0) { return QDateTime(); // before recurrence start } prev = prev.addSecs(-n - 1); return prev >= d->mDateStart ? prev : QDateTime(); } // If we have a cache (duration given), use that if (d->mDuration > 0) { if (!d->mCached) { d->buildCache(); } const auto it = strictLowerBound(d->mCachedDates.constBegin(), d->mCachedDates.constEnd(), toDate); if (it != d->mCachedDates.constEnd()) { return *it; } return QDateTime(); } QDateTime prev = toDate; if (d->mDuration >= 0 && endDt().isValid() && toDate > endDt()) { prev = endDt().addSecs(1).toTimeZone(d->mDateStart.timeZone()); } Constraint interval(d->getPreviousValidDateInterval(prev, recurrenceType())); const auto dts = d->datesForInterval(interval, recurrenceType()); const auto it = strictLowerBound(dts.begin(), dts.end(), prev); if (it != dts.end()) { return ((*it) >= d->mDateStart) ? (*it) : QDateTime(); } // 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 auto 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 QDateTime(); } } } return QDateTime(); } QDateTime RecurrenceRule::getNextDate(const QDateTime &preDate) const { // Convert to the time spec used by this recurrence rule QDateTime fromDate(preDate.toTimeZone(d->mDateStart.timeZone())); // Beyond end of recurrence if (d->mDuration >= 0 && endDt().isValid() && fromDate >= endDt()) { return QDateTime(); } // 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(fromDate) + 1) % d->mTimedRepetition); QDateTime next = fromDate.addSecs(d->mTimedRepetition - n + 1); return d->mDuration < 0 || !endDt().isValid() || next <= endDt() ? next : QDateTime(); } if (d->mDuration > 0) { if (!d->mCached) { d->buildCache(); } const auto it = std::upper_bound(d->mCachedDates.constBegin(), d->mCachedDates.constEnd(), fromDate); if (it != d->mCachedDates.constEnd()) { return *it; } } QDateTime end = endDt(); Constraint interval(d->getNextValidDateInterval(fromDate, recurrenceType())); const auto dts = d->datesForInterval(interval, recurrenceType()); const auto it = std::upper_bound(dts.begin(), dts.end(), fromDate); if (it != dts.end()) { return (d->mDuration < 0 || *it <= end) ? *it : QDateTime(); } interval.increase(recurrenceType(), frequency()); if (d->mDuration >= 0 && interval.intervalDateTime(recurrenceType()) > end) { return QDateTime(); } // 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 { auto dts = d->datesForInterval(interval, recurrenceType()); if (!dts.isEmpty()) { QDateTime ret(dts[0]); if (d->mDuration >= 0 && ret > end) { return QDateTime(); } else { return ret; } } interval.increase(recurrenceType(), frequency()); } while (++loop < LOOP_LIMIT && (d->mDuration < 0 || interval.intervalDateTime(recurrenceType()) < end)); return QDateTime(); } QList RecurrenceRule::timesInInterval(const QDateTime &dtStart, const QDateTime &dtEnd) const { const QDateTime start = dtStart.toTimeZone(d->mDateStart.timeZone()); const QDateTime end = dtEnd.toTimeZone(d->mDateStart.timeZone()); QList result; if (end < d->mDateStart) { return result; // before start of recurrence } QDateTime enddt = end; if (d->mDuration >= 0) { const QDateTime 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(start) % d->mTimedRepetition); } else { offsetFromNextOccurrence = -(d->mDateStart.secsTo(start) % d->mTimedRepetition); } QDateTime dt = start.addSecs(offsetFromNextOccurrence); if (dt <= enddt) { int numberOfOccurrencesWithinInterval = static_cast(dt.secsTo(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; } QDateTime 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 } const auto it = std::lower_bound(d->mCachedDates.constBegin(), d->mCachedDates.constEnd(), start); if (it != d->mCachedDates.constEnd()) { const auto itEnd = std::upper_bound(it, d->mCachedDates.constEnd(), enddt); if (itEnd != d->mCachedDates.constEnd()) { done = true; } std::copy(it, itEnd, std::back_inserter(result)); } if (d->mCachedDateEnd.isValid()) { done = true; } else if (!result.isEmpty()) { result += QDateTime(); // 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 { auto dts = d->datesForInterval(interval, recurrenceType()); auto it = dts.begin(); auto itEnd = dts.end(); if (loop == 0) { it = std::lower_bound(dts.begin(), dts.end(), st); } itEnd = std::upper_bound(it, dts.end(), enddt); if (itEnd != dts.end()) { loop = LOOP_LIMIT; } std::copy(it, itEnd, std::back_inserter(result)); // 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 QDateTime &dt, PeriodType type) const { long periods = 0; QDateTime start = mDateStart; QDateTime nextValid(start); int modifier = 1; QDateTime toDate(dt.toTimeZone(start.timeZone())); // 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; Q_FALLTHROUGH(); case rMinutely: modifier *= 60; Q_FALLTHROUGH(); case rSecondly: periods = static_cast(start.secsTo(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; Q_FALLTHROUGH(); 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 QDateTime &dt, PeriodType type) const { // TODO: Simplify this! long periods = 0; QDateTime start = mDateStart; QDateTime nextValid(start); int modifier = 1; QDateTime toDate(dt.toTimeZone(start.timeZone())); // 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; Q_FALLTHROUGH(); case rMinutely: modifier *= 60; Q_FALLTHROUGH(); case rSecondly: periods = static_cast(start.secsTo(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; Q_FALLTHROUGH(); 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); } QList 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 */ QList 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 sortAndRemoveDuplicates(lst); /*if ( lst.isEmpty() ) { qCDebug(KCALCORE_LOG) << " No Dates in Interval"; } else { qCDebug(KCALCORE_LOG) << " Dates:"; for ( int i = 0, iend = lst.count(); i < iend; ++i ) { qCDebug(KCALCORE_LOG)<< " -)" << dumpTime(lst[i]); } qCDebug(KCALCORE_LOG) << " ---------------------"; }*/ if (!mBySetPos.isEmpty()) { auto 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]); } } sortAndRemoveDuplicates(lst); } return lst; } //@endcond void RecurrenceRule::dump() const { #ifndef NDEBUG if (!d->mRRule.isEmpty()) { qCDebug(KCALCORE_LOG) << " RRULE=" << d->mRRule; } qCDebug(KCALCORE_LOG) << " Read-Only:" << isReadOnly(); qCDebug(KCALCORE_LOG) << " Period type:" << int(recurrenceType()) << ", frequency:" << frequency(); qCDebug(KCALCORE_LOG) << " #occurrences:" << duration(); qCDebug(KCALCORE_LOG) << " start date:" << dumpTime(startDt(), allDay()) << ", end date:" << dumpTime(endDt(), allDay()); #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] ) );\ }\ qCDebug(KCALCORE_LOG) << " " << label << lst.join( QStringLiteral(", ") );\ } dumpByIntList(d->mBySeconds, QStringLiteral("BySeconds: ")); dumpByIntList(d->mByMinutes, QStringLiteral("ByMinutes: ")); dumpByIntList(d->mByHours, QStringLiteral("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())); } qCDebug(KCALCORE_LOG) << " ByDays: " << lst.join(QStringLiteral(", ")); } dumpByIntList(d->mByMonthDays, QStringLiteral("ByMonthDays:")); dumpByIntList(d->mByYearDays, QStringLiteral("ByYearDays: ")); dumpByIntList(d->mByWeekNumbers, QStringLiteral("ByWeekNr: ")); dumpByIntList(d->mByMonths, QStringLiteral("ByMonths: ")); dumpByIntList(d->mBySetPos, QStringLiteral("BySetPos: ")); #undef dumpByIntList qCDebug(KCALCORE_LOG) << " Week start:" << DateHelper::dayName(d->mWeekStart); qCDebug(KCALCORE_LOG) << " 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 { qCDebug(KCALCORE_LOG) << " ~> Y=" << year << ", M=" << month << ", D=" << day << ", H=" << hour << ", m=" << minute << ", S=" << second << ", wd=" << weekday << ",#wd=" << weekdaynr << ", #w=" << weeknumber << ", yd=" << yearday; } //@endcond QString dumpTime(const QDateTime &dt, bool isAllDay) { #ifndef NDEBUG if (!dt.isValid()) { return QString(); } QString result; if (isAllDay) { result = dt.toString(QStringLiteral("ddd yyyy-MM-dd t")); } else { result = dt.toString(QStringLiteral("ddd yyyy-MM-dd hh:mm:ss t")); } return result; #else Q_UNUSED(dt); Q_UNUSED(isAllDay); return QString(); #endif } QDateTime 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; serializeQTimeZoneAsSpec(out, c.timeZone); out << false; // for backwards compatibility return out; } QDataStream &operator>>(QDataStream &in, Constraint &c) { bool secondOccurrence; // no longer used in >> c.year >> c.month >> c.day >> c.hour >> c.minute >> c.second >> c.weekday >> c.weekdaynr >> c.weeknumber >> c.yearday >> c.weekstart; deserializeSpecAsQTimeZone(in, c.timeZone); in >> 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); serializeQDateTimeAsKDateTime(out, d->mDateStart); out << d->mFrequency << d->mDuration; serializeQDateTimeAsKDateTime(out, d->mDateEnd); out << 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; deserializeKDateTimeAsQDateTime(in, d->mDateStart); in >> d->mFrequency >> d->mDuration; deserializeKDateTimeAsQDateTime(in, d->mDateEnd); in >> 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; }