diff --git a/libkcal/recurrence.cpp b/libkcal/recurrence.cpp index 5d1505df29..3c38a6d3a0 100644 --- a/libkcal/recurrence.cpp +++ b/libkcal/recurrence.cpp @@ -1,1079 +1,1144 @@ /* This file is part of libkcal. Copyright (c) 1998 Preston Brown Copyright (c) 2001 Cornelius Schumacher Copyright (c) 2002 David Jarvie Copyright (C) 2005 Reinhold Kainhofer This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include #include #include #include #include #include "recurrence.h" #include "recurrencerule.h" using namespace KCal; Recurrence::Recurrence() : mFloating( false ), mRecurReadOnly(false), mCachedType(rMax) { mExRules.setAutoDelete( true ); mRRules.setAutoDelete( true ); } Recurrence::Recurrence( const Recurrence &r ) : RecurrenceRule::Observer(), mRDateTimes( r.mRDateTimes ), mRDates( r.mRDates ), mExDateTimes( r.mExDateTimes ), mExDates( r.mExDates ), mStartDateTime( r.mStartDateTime ), mFloating( r.mFloating ), mRecurReadOnly(r.mRecurReadOnly), mCachedType( r.mCachedType ) { mExRules.setAutoDelete( true ); mRRules.setAutoDelete( true ); RecurrenceRule::List::ConstIterator rr; for ( rr = r.mRRules.begin(); rr != r.mRRules.end(); ++rr ) { RecurrenceRule *rule = new RecurrenceRule( *(*rr) ); mRRules.append( rule ); rule->addObserver( this ); } for ( rr = r.mExRules.begin(); rr != r.mExRules.end(); ++rr ) { RecurrenceRule *rule = new RecurrenceRule( *(*rr) ); mExRules.append( rule ); rule->addObserver( this ); } } Recurrence::~Recurrence() { } bool Recurrence::operator==( const Recurrence& r2 ) const { if ( mStartDateTime != r2.mStartDateTime || mFloating != r2.mFloating || mRecurReadOnly != r2.mRecurReadOnly ) return false; if ( mExDates != r2.mExDates ) return false; if ( mExDateTimes != r2.mExDateTimes ) return false; if ( mRDates != r2.mRDates ) return false; if ( mRDateTimes != r2.mRDateTimes ) return false; // Compare the rrules, exrules! Assume they have the same order... This only // matters if we have more than one rule (which shouldn't be the default anyway) if ( mRRules.count() != r2.mRRules.count() ) return false; RecurrenceRule::List::ConstIterator rit1 = mRRules.begin(); RecurrenceRule::List::ConstIterator rit2 = r2.mRRules.begin(); while ( rit1 != mRRules.end() && rit2 != r2.mRRules.end() ) { // dereference the iterator to the RecurrenceRule*, and that once again // to RecurrenceRule... if ( *(*rit1) != *(*rit2) ) return false; ++rit1; ++rit2; } RecurrenceRule::List::ConstIterator exit1 = mExRules.begin(); RecurrenceRule::List::ConstIterator exit2 = r2.mExRules.begin(); while ( exit1 != mExRules.end() && exit2 != r2.mExRules.end() ) { // dereference the iterator to the RecurrenceRule*, and that once again // to RecurrenceRule... if ( *(*exit1) != *(*exit2) ) return false; ++exit1; ++exit2; } return true; } void Recurrence::addObserver( Observer *observer ) { if ( !mObservers.contains( observer ) ) mObservers.append( observer ); } void Recurrence::removeObserver( Observer *observer ) { if ( mObservers.contains( observer ) ) mObservers.remove( observer ); } QDateTime Recurrence::startDateTime() const { if ( mFloating ) return QDateTime( mStartDateTime.date(), QTime( 0, 0, 0 ) ); else return mStartDateTime; } void Recurrence::setFloats( bool floats ) { if ( mRecurReadOnly ) return; if ( floats == mFloating ) return; mFloating = floats; RecurrenceRule::List::ConstIterator it; for ( it = mRRules.begin(); it != mRRules.end(); ++it ) { (*it)->setFloats( floats ); } RecurrenceRule::List::ConstIterator it1; for ( it1 = mExRules.begin(); it1 != mExRules.end(); ++it1 ) { (*it1)->setFloats( floats ); } updated(); } RecurrenceRule *Recurrence::defaultRRule( bool create ) const { if ( mRRules.isEmpty() ) { if ( !create || mRecurReadOnly ) return 0; RecurrenceRule *rrule = new RecurrenceRule(); rrule->setStartDt( startDateTime() ); const_cast(this)->addRRule( rrule ); return rrule; } else { return mRRules.first(); } } RecurrenceRule *Recurrence::defaultRRuleConst() const { if ( mRRules.isEmpty() ) { return 0; } else { return mRRules.first(); } } void Recurrence::updated() { // recurrenceType() re-calculates the type if it's rMax mCachedType = rMax; for ( QValueList::ConstIterator it = mObservers.begin(); it != mObservers.end(); ++it ) { if ( (*it) ) (*it)->recurrenceUpdated( this ); } } bool Recurrence::doesRecur() const { return !mRRules.isEmpty() || !mRDates.isEmpty() || !mRDateTimes.isEmpty(); } ushort Recurrence::recurrenceType() const { if ( mCachedType == rMax ) { mCachedType = recurrenceType( defaultRRuleConst() ); } return mCachedType; } ushort Recurrence::recurrenceType( const RecurrenceRule *rrule ) { if ( !rrule ) return rNone; RecurrenceRule::PeriodType type = rrule->recurrenceType(); // BYSETPOS, BYWEEKNUMBER and BYSECOND were not supported in old versions if ( !rrule->bySetPos().isEmpty() ) return rOther; if ( !rrule->bySeconds().isEmpty() ) return rOther; if ( !rrule->byWeekNumbers().isEmpty() ) return rOther; // It wasn't possible to set BYMINUTES, BYHOUR etc. by the old code. So if // it's set, it's none of the old types if ( !rrule->byMinutes().isEmpty() ) return rOther; if ( !rrule->byHours().isEmpty() ) return rOther; // Possible combinations were: // BYDAY: with WEEKLY, MONTHLY, YEARLY // BYMONTHDAY: with MONTHLY, YEARLY // BYMONTH: with YEARLY // BYYEARDAY: with YEARLY if ( !rrule->byYearDays().isEmpty() && type != RecurrenceRule::rYearly ) return rOther; if ( !rrule->byMonths().isEmpty() && type != RecurrenceRule::rYearly ) return rOther; if ( !rrule->byDays().isEmpty() ) { if ( type != RecurrenceRule::rYearly && type != RecurrenceRule::rMonthly && type != RecurrenceRule::rWeekly ) return rOther; } switch ( type ) { case RecurrenceRule::rNone: return rNone; case RecurrenceRule::rMinutely: return rMinutely; case RecurrenceRule::rHourly: return rHourly; case RecurrenceRule::rDaily: return rDaily; case RecurrenceRule::rWeekly: return rWeekly; case RecurrenceRule::rMonthly: { if ( rrule->byDays().isEmpty() ) return rMonthlyDay; else if ( rrule->byMonthDays().isEmpty() ) return rMonthlyPos; else return rOther; // both position and date specified } case RecurrenceRule::rYearly: { // Possible combinations: // rYearlyMonth: [BYMONTH &] BYMONTHDAY // rYearlyDay: BYYEARDAY // rYearlyPos: [BYMONTH &] BYDAY if ( !rrule->byDays().isEmpty() ) { // can only by rYearlyPos if ( rrule->byMonthDays().isEmpty() && rrule->byYearDays().isEmpty() ) return rYearlyPos; else return rOther; } else if ( !rrule->byYearDays().isEmpty() ) { // Can only be rYearlyDay if ( rrule->byMonths().isEmpty() && rrule->byMonthDays().isEmpty() ) return rYearlyDay; else return rOther; } else { return rYearlyMonth; } break; } default: return rOther; } return rOther; } bool Recurrence::recursOn(const QDate &qd) const { TimeList tms; // First handle dates. Exrules override if ( mExDates.contains( qd ) ) return false; // For all-day events a matching exrule excludes the whole day // since exclusions take precedence over inclusions, we know it can't occur on that day. if ( doesFloat() ) { for ( RecurrenceRule::List::ConstIterator rr = mExRules.begin(); rr != mExRules.end(); ++rr ) { if ( (*rr)->recursOn( qd ) ) return false; } } if ( mRDates.contains( qd ) ) return true; // Check if it might recur today at all. bool recurs = false; if ( startDate() == qd ) recurs = true; for ( RecurrenceRule::List::ConstIterator rr = mRRules.begin(); rr != mRRules.end(); ++rr ) { recurs = recurs || (*rr)->recursOn( qd ); } // If we already know it recurs, no need to check the rdate list too. if ( !recurs ) { for ( DateTimeList::ConstIterator rit = mRDateTimes.begin(); rit != mRDateTimes.end(); ++rit ) { if ( (*rit).date() == qd ) { recurs = true; break; } } } // If the event wouldn't recur at all, simply return false, don't check ex* if ( !recurs ) return false; // Check if there are any times for this day excluded, either by exdate or exrule: bool exon = false; for ( DateTimeList::ConstIterator exit = mExDateTimes.begin(); exit != mExDateTimes.end(); ++exit ) { if ( (*exit).date() == qd ) { exon = true; break; } } if ( !doesFloat() ) { // we have already checked floating times above for ( RecurrenceRule::List::ConstIterator rr = mExRules.begin(); rr != mExRules.end(); ++rr ) { exon = exon || (*rr)->recursOn( qd ); } } if ( !exon ) { // Simple case, nothing on that day excluded, return the value from before return recurs; } else { // Harder part: I don't think there is any way other than to calculate the // whole list of items for that day. TimeList timesForDay( recurTimesOn( qd ) ); return !timesForDay.isEmpty(); } } bool Recurrence::recursAt( const QDateTime &dt ) const { // if it's excluded anyway, don't bother to check if it recurs at all. if ( mExDateTimes.contains( dt )) return false; if ( mExDates.contains( dt.date() )) return false; for ( RecurrenceRule::List::ConstIterator rr = mExRules.begin(); rr != mExRules.end(); ++rr ) { if ( (*rr)->recursAt( dt ) ) return false; } // Check explicit recurrences, then rrules. bool occurs = ( startDateTime() == dt ) || mRDateTimes.contains( dt ); if ( occurs ) return true; for ( RecurrenceRule::List::ConstIterator rr = mRRules.begin(); rr != mRRules.end(); ++rr ) { if ( (*rr)->recursAt( dt ) ) return true; } return false; } /** Calculates the cumulative end of the whole recurrence (rdates and rrules). If any rrule is infinite, or the recurrence doesn't have any rrules or rdates, an invalid date is returned. */ QDateTime Recurrence::endDateTime() const { DateTimeList dts; dts << startDateTime(); if ( !mRDates.isEmpty() ) dts << QDateTime( mRDates.last(), QTime( 0, 0, 0 ) ); if ( !mRDateTimes.isEmpty() ) dts << mRDateTimes.last(); for ( RecurrenceRule::List::ConstIterator rr = mRRules.begin(); rr != mRRules.end(); ++rr ) { QDateTime rl( (*rr)->endDt() ); // if any of the rules is infinite, the whole recurrence is if ( !rl.isValid() ) return QDateTime(); dts << rl; } qSortUnique( dts ); if ( dts.isEmpty() ) return QDateTime(); else return dts.last(); } /** Calculates the cumulative end of the whole recurrence (rdates and rrules). If any rrule is infinite, or the recurrence doesn't have any rrules or rdates, an invalid date is returned. */ QDate Recurrence::endDate() const { QDateTime end( endDateTime() ); if ( end.isValid() ) { return end.date(); } else return QDate(); } void Recurrence::setEndDate( const QDate &date ) { if ( doesFloat() ) setEndDateTime( QDateTime( date, QTime( 23, 59, 59 ) ) ); else setEndDateTime( QDateTime( date, mStartDateTime.time() ) ); } void Recurrence::setEndDateTime( const QDateTime &dateTime ) { if ( mRecurReadOnly ) return; RecurrenceRule *rrule = defaultRRule( true ); if ( !rrule ) return; rrule->setEndDt( dateTime ); updated(); } int Recurrence::duration() const { RecurrenceRule *rrule = defaultRRuleConst(); if ( rrule ) return rrule->duration(); else return 0; } // int Recurrence::durationTo( const QDate &/*date*/ ) const // { // return 0; // } int Recurrence::durationTo( const QDateTime &datetime ) const { // Emulate old behavior: This is just an interface to the first rule! RecurrenceRule *rrule = defaultRRuleConst(); if ( !rrule ) return 0; else return rrule->durationTo( datetime ); } void Recurrence::setDuration( int duration ) { if ( mRecurReadOnly ) return; RecurrenceRule *rrule = defaultRRule( true ); if ( !rrule ) return; rrule->setDuration( duration ); updated(); } void Recurrence::unsetRecurs() { if ( mRecurReadOnly ) return; mRRules.clearAll(); updated(); } void Recurrence::clear() { if ( mRecurReadOnly ) return; mRRules.clearAll(); mExRules.clearAll(); mRDates.clear(); mRDateTimes.clear(); mExDates.clear(); mExDateTimes.clear(); mCachedType = rMax; updated(); } void Recurrence::setStartDateTime( const QDateTime &start ) { if ( mRecurReadOnly ) return; mStartDateTime = start; setFloats( false ); // set all RRULEs and EXRULEs for ( RecurrenceRule::List::ConstIterator rr = mRRules.begin(); rr != mRRules.end(); ++rr ) { (*rr)->setStartDt( start ); } for ( RecurrenceRule::List::ConstIterator rr = mExRules.begin(); rr != mExRules.end(); ++rr ) { (*rr)->setStartDt( start ); } updated(); } void Recurrence::setStartDate( const QDate &start ) { setStartDateTime( QDateTime( start, QTime(0,0,0) ) ); setFloats( true ); } int Recurrence::frequency() const { RecurrenceRule *rrule = defaultRRuleConst(); if ( rrule ) return rrule->frequency(); else return 0; } // Emulate the old behaviour. Make this methods just an interface to the // first rrule void Recurrence::setFrequency( int freq ) { if ( mRecurReadOnly || freq <= 0 ) return; RecurrenceRule *rrule = defaultRRule( true ); if ( rrule ) rrule->setFrequency( freq ); updated(); } // WEEKLY int Recurrence::weekStart() const { RecurrenceRule *rrule = defaultRRuleConst(); if ( rrule ) return rrule->weekStart(); else return 1; } // Emulate the old behavior QBitArray Recurrence::days() const { QBitArray days( 7 ); days.fill( 0 ); RecurrenceRule *rrule = defaultRRuleConst(); if ( rrule ) { QValueList bydays = rrule->byDays(); for ( QValueListConstIterator it = bydays.begin(); it != bydays.end(); ++it ) { if ( (*it).pos() == 0 ) { days.setBit( (*it).day() - 1 ); } } } return days; } // MONTHLY // Emulate the old behavior QValueList Recurrence::monthDays() const { RecurrenceRule *rrule = defaultRRuleConst(); if ( rrule ) return rrule->byMonthDays(); else return QValueList(); } // Emulate the old behavior QValueList Recurrence::monthPositions() const { RecurrenceRule *rrule = defaultRRuleConst(); if ( rrule ) return rrule->byDays(); else return QValueList(); } // YEARLY QValueList Recurrence::yearDays() const { RecurrenceRule *rrule = defaultRRuleConst(); if ( rrule ) return rrule->byYearDays(); else return QValueList(); } QValueList Recurrence::yearDates() const { return monthDays(); } QValueList Recurrence::yearMonths() const { RecurrenceRule *rrule = defaultRRuleConst(); if ( rrule ) return rrule->byMonths(); else return QValueList(); } QValueList Recurrence::yearPositions() const { return monthPositions(); } RecurrenceRule *Recurrence::setNewRecurrenceType( RecurrenceRule::PeriodType type, int freq ) { if ( mRecurReadOnly || freq <= 0 ) return 0; mRRules.clearAll(); updated(); RecurrenceRule *rrule = defaultRRule( true ); if ( !rrule ) return 0; rrule->setRecurrenceType( type ); rrule->setFrequency( freq ); rrule->setDuration( -1 ); return rrule; } void Recurrence::setMinutely( int _rFreq ) { if ( setNewRecurrenceType( RecurrenceRule::rMinutely, _rFreq ) ) updated(); } void Recurrence::setHourly( int _rFreq ) { if ( setNewRecurrenceType( RecurrenceRule::rHourly, _rFreq ) ) updated(); } void Recurrence::setDaily( int _rFreq ) { if ( setNewRecurrenceType( RecurrenceRule::rDaily, _rFreq ) ) updated(); } void Recurrence::setWeekly( int freq, int weekStart ) { RecurrenceRule *rrule = setNewRecurrenceType( RecurrenceRule::rWeekly, freq ); if ( !rrule ) return; rrule->setWeekStart( weekStart ); updated(); } void Recurrence::setWeekly( int freq, const QBitArray &days, int weekStart ) { setWeekly( freq, weekStart ); addMonthlyPos( 0, days ); } void Recurrence::addWeeklyDays( const QBitArray &days ) { addMonthlyPos( 0, days ); } void Recurrence::setMonthly( int freq ) { if ( setNewRecurrenceType( RecurrenceRule::rMonthly, freq ) ) updated(); } void Recurrence::addMonthlyPos( short pos, const QBitArray &days ) { // Allow 53 for yearly! if ( mRecurReadOnly || pos > 53 || pos < -53 ) return; RecurrenceRule *rrule = defaultRRule( false ); if ( !rrule ) return; bool changed = false; QValueList positions = rrule->byDays(); for ( int i = 0; i < 7; ++i ) { if ( days.testBit(i) ) { RecurrenceRule::WDayPos p( pos, i + 1 ); if ( !positions.contains( p ) ) { changed = true; positions.append( p ); } } } if ( changed ) { rrule->setByDays( positions ); updated(); } } void Recurrence::addMonthlyPos( short pos, ushort day ) { // Allow 53 for yearly! if ( mRecurReadOnly || pos > 53 || pos < -53 ) return; RecurrenceRule *rrule = defaultRRule( false ); if ( !rrule ) return; QValueList positions = rrule->byDays(); RecurrenceRule::WDayPos p( pos, day ); if ( !positions.contains( p ) ) { positions.append( p ); rrule->setByDays( positions ); updated(); } } void Recurrence::addMonthlyDate( short day ) { if ( mRecurReadOnly || day > 31 || day < -31 ) return; RecurrenceRule *rrule = defaultRRule( true ); if ( !rrule ) return; QValueList monthDays = rrule->byMonthDays(); if ( !monthDays.contains( day ) ) { monthDays.append( day ); rrule->setByMonthDays( monthDays ); updated(); } } void Recurrence::setYearly( int freq ) { if ( setNewRecurrenceType( RecurrenceRule::rYearly, freq ) ) updated(); } // Daynumber within year void Recurrence::addYearlyDay( int day ) { RecurrenceRule *rrule = defaultRRule( false ); // It must already exist! if ( !rrule ) return; QValueList days = rrule->byYearDays(); if ( !days.contains( day ) ) { days << day; rrule->setByYearDays( days ); updated(); } } // day part of date within year void Recurrence::addYearlyDate( int day ) { addMonthlyDate( day ); } // day part of date within year, given as position (n-th weekday) void Recurrence::addYearlyPos( short pos, const QBitArray &days ) { addMonthlyPos( pos, days ); } // month part of date within year void Recurrence::addYearlyMonth( short month ) { if ( mRecurReadOnly || month < 1 || month > 12 ) return; RecurrenceRule *rrule = defaultRRule( false ); if ( !rrule ) return; QValueList months = rrule->byMonths(); if ( !months.contains(month) ) { months << month; rrule->setByMonths( months ); updated(); } } TimeList Recurrence::recurTimesOn( const QDate &date ) const { TimeList times; // The whole day is excepted if ( mExDates.contains( date ) ) return times; // EXRULE takes precedence over RDATE entries, so for floating events, // a matching excule also excludes the whole day automatically if ( doesFloat() ) { for ( RecurrenceRule::List::ConstIterator rr = mExRules.begin(); rr != mExRules.end(); ++rr ) { if ( (*rr)->recursOn( date ) ) return times; } } if ( startDate() == date ) times << startDateTime().time(); bool foundDate = false; for ( DateTimeList::ConstIterator it = mRDateTimes.begin(); it != mRDateTimes.end(); ++it ) { if ( (*it).date() == date ) { times << (*it).time(); foundDate = true; } else if (foundDate) break; // <= Assume that the rdatetime list is sorted } for ( RecurrenceRule::List::ConstIterator rr = mRRules.begin(); rr != mRRules.end(); ++rr ) { times += (*rr)->recurTimesOn( date ); } qSortUnique( times ); foundDate = false; TimeList extimes; for ( DateTimeList::ConstIterator it = mExDateTimes.begin(); it != mExDateTimes.end(); ++it ) { if ( (*it).date() == date ) { extimes << (*it).time(); foundDate = true; } else if (foundDate) break; } if ( !doesFloat() ) { // we have already checked floating times above for ( RecurrenceRule::List::ConstIterator rr = mExRules.begin(); rr != mExRules.end(); ++rr ) { extimes += (*rr)->recurTimesOn( date ); } } qSortUnique( extimes ); for ( TimeList::Iterator it = extimes.begin(); it != extimes.end(); ++it ) { times.remove( (*it) ); } return times; } +DateTimeList Recurrence::timesInInterval( const QDateTime &start, const QDateTime &end ) const +{ + int i, count; + DateTimeList times; + for ( i = 0, count = mRRules.count(); i < count; ++i ) { + times += mRRules[i]->timesInInterval( start, end ); + } + + // add rdatetimes that fit in the interval + for ( i = 0, count = mRDateTimes.count(); i < count; ++i ) { + if ( mRDateTimes[i] >= start && mRDateTimes[i] <= end ) { + times += mRDateTimes[i]; + } + } + + // add rdates that fit in the interval + QDateTime qdt( mStartDateTime ); + for ( i = 0, count = mRDates.count(); i < count; ++i ) { + qdt.setDate( mRDates[i] ); + if ( qdt >= start && qdt <= end ) { + times += qdt; + } + } + + // Recurrence::timesInInterval(...) doesn't explicitly add mStartDateTime to the list + // of times to be returned. It calls mRRules[i]->timesInInterval(...) which include + // mStartDateTime. + // So, If we have rdates/rdatetimes but don't have any rrule we must explicitly + // add mStartDateTime to the list, otherwise we won't see the first occurrence. + if ( ( !mRDates.isEmpty() || !mRDateTimes.isEmpty() ) && + mRRules.isEmpty() && + start <= mStartDateTime && + end >= mStartDateTime ) { + times += mStartDateTime; + } + + qSortUnique( times ); + + // Remove excluded times + int idt = 0; + int enddt = times.count(); + for ( i = 0, count = mExDates.count(); i < count && idt < enddt; ++i ) { + while ( idt < enddt && times[idt].date() < mExDates[i] ) ++idt; + while ( idt < enddt && times[idt].date() == mExDates[i] ) { + times.remove( times.at( idt ) ); + --enddt; + } + } + DateTimeList extimes; + for ( i = 0, count = mExRules.count(); i < count; ++i ) { + extimes += mExRules[i]->timesInInterval( start, end ); + } + extimes += mExDateTimes; + qSortUnique( extimes ); + + int st = 0; + for ( i = 0, count = extimes.count(); i < count; ++i ) { + int j = removeSorted( times, extimes[i], st ); + if ( j >= 0 ) { + st = j; + } + } + + return times; +} QDateTime Recurrence::getNextDateTime( const QDateTime &preDateTime ) const { //kdDebug(5800) << " Recurrence::getNextDateTime after " << preDateTime << endl; QDateTime nextDT = preDateTime; // prevent infinite loops, e.g. when an exrule extinguishes an rrule (e.g. // the exrule is identical to the rrule). If an occurrence is found, break // out of the loop by returning that QDateTime // TODO_Recurrence: Is a loop counter of 1000 really okay? I mean for secondly // recurrence, an exdate might exclude more than 1000 intervals! int loop = 0; while ( loop < 1000 ) { // Outline of the algo: // 1) Find the next date/time after preDateTime when the event could recur // 1.0) Add the start date if it's after preDateTime // 1.1) Use the next occurrence from the explicit RDATE lists // 1.2) Add the next recurrence for each of the RRULEs // 2) Take the earliest recurrence of these = QDateTime nextDT // 3) If that date/time is not excluded, either explicitly by an EXDATE or // by an EXRULE, return nextDT as the next date/time of the recurrence // 4) If it's excluded, start all at 1), but starting at nextDT (instead // of preDateTime). Loop at most 1000 times. ++loop; // First, get the next recurrence from the RDate lists DateTimeList dates; if ( nextDT < startDateTime() ) dates << startDateTime(); DateTimeList::ConstIterator it = mRDateTimes.begin(); // Assume that the rdatetime list is sorted while ( it != mRDateTimes.end() && (*it) <= nextDT ) ++it; if ( it != mRDateTimes.end() ) dates << (*it); /*kdDebug(5800) << " nextDT: " << nextDT << ", startDT: " << startDateTime() << endl; kdDebug(5800) << " getNextDateTime: found " << dates.count() << " RDATES and DTSTART in loop " << loop << endl;*/ DateList::ConstIterator dit = mRDates.begin(); while ( dit != mRDates.end() && QDateTime( (*dit), startDateTime().time() ) <= nextDT ) ++dit; if ( dit != mRDates.end() ) dates << QDateTime( (*dit), startDateTime().time() ); // Add the next occurrences from all RRULEs. for ( RecurrenceRule::List::ConstIterator rr = mRRules.begin(); rr != mRRules.end(); ++rr ) { QDateTime dt = (*rr)->getNextDate( nextDT ); if ( dt.isValid() ) dates << dt; } // Take the first of these (all others can't be used later on) qSortUnique( dates ); // kdDebug(5800) << " getNextDateTime: found " << dates.count() << " dates in loop " << loop << endl; if ( dates.isEmpty() ) return QDateTime(); nextDT = dates.first(); // Check if that date/time is excluded explicitly or by an exrule: if ( !mExDates.contains( nextDT.date() ) && !mExDateTimes.contains( nextDT ) ) { // kdDebug(5800) << " NextDT" << nextDT << " not excluded by EXDATE " << endl; bool allowed = true; for ( RecurrenceRule::List::ConstIterator rr = mExRules.begin(); rr != mExRules.end(); ++rr ) { allowed = allowed && !( (*rr)->recursAt( nextDT ) ); } // kdDebug(5800) << " NextDT " << nextDT << ", allowed=" << allowed << endl; if ( allowed ) return nextDT; } } // Couldn't find a valid occurrences in 1000 loops, something is wrong! return QDateTime(); } QDateTime Recurrence::getPreviousDateTime( const QDateTime &afterDateTime ) const { QDateTime prevDT = afterDateTime; // prevent infinite loops, e.g. when an exrule extinguishes an rrule (e.g. // the exrule is identical to the rrule). If an occurrence is found, break // out of the loop by returning that QDateTime int loop = 0; while ( loop < 1000 ) { // Outline of the algo: // 1) Find the next date/time after preDateTime when the event could recur // 1.1) Use the next occurrence from the explicit RDATE lists // 1.2) Add the next recurrence for each of the RRULEs // 2) Take the earliest recurrence of these = QDateTime nextDT // 3) If that date/time is not excluded, either explicitly by an EXDATE or // by an EXRULE, return nextDT as the next date/time of the recurrence // 4) If it's excluded, start all at 1), but starting at nextDT (instead // of preDateTime). Loop at most 1000 times. ++loop; // First, get the next recurrence from the RDate lists DateTimeList dates; if ( prevDT > startDateTime() ) dates << startDateTime(); DateTimeList::ConstIterator dtit = mRDateTimes.end(); if ( dtit != mRDateTimes.begin() ) { do { --dtit; } while ( dtit != mRDateTimes.begin() && (*dtit) >= prevDT ); if ( (*dtit) < prevDT ) dates << (*dtit); } DateList::ConstIterator dit = mRDates.end(); if ( dit != mRDates.begin() ) { do { --dit; } while ( dit != mRDates.begin() && QDateTime((*dit), startDateTime().time()) >= prevDT ); if ( QDateTime((*dit), startDateTime().time()) < prevDT ) dates << QDateTime( (*dit), startDateTime().time() ); } // Add the previous occurrences from all RRULEs. for ( RecurrenceRule::List::ConstIterator rr = mRRules.begin(); rr != mRRules.end(); ++rr ) { QDateTime dt = (*rr)->getPreviousDate( prevDT ); if ( dt.isValid() ) dates << dt; } //kdDebug(5800) << " getPreviousDateTime: found " << dates.count() << " dates in loop " << loop << endl; // Take the last of these (all others can't be used later on) qSortUnique( dates ); if ( dates.isEmpty() ) return QDateTime(); prevDT = dates.last(); // Check if that date/time is excluded explicitly or by an exrule: if ( !mExDates.contains( prevDT.date() ) && !mExDateTimes.contains( prevDT ) ) { bool allowed = true; for ( RecurrenceRule::List::ConstIterator rr = mExRules.begin(); rr != mExRules.end(); ++rr ) { allowed = allowed && !( (*rr)->recursAt( prevDT ) ); } if ( allowed ) return prevDT; } } // Couldn't find a valid occurrences in 1000 loops, something is wrong! return QDateTime(); } /***************************** PROTECTED FUNCTIONS ***************************/ RecurrenceRule::List Recurrence::rRules() const { return mRRules; } void Recurrence::addRRule( RecurrenceRule *rrule ) { if ( mRecurReadOnly || !rrule ) return; rrule->setFloats( mFloating ); mRRules.append( rrule ); rrule->addObserver( this ); updated(); } void Recurrence::removeRRule( RecurrenceRule *rrule ) { if (mRecurReadOnly) return; mRRules.remove( rrule ); rrule->removeObserver( this ); updated(); } RecurrenceRule::List Recurrence::exRules() const { return mExRules; } void Recurrence::addExRule( RecurrenceRule *exrule ) { if ( mRecurReadOnly || !exrule ) return; exrule->setFloats( mFloating ); mExRules.append( exrule ); exrule->addObserver( this ); updated(); } void Recurrence::removeExRule( RecurrenceRule *exrule ) { if (mRecurReadOnly) return; mExRules.remove( exrule ); exrule->removeObserver( this ); updated(); } DateTimeList Recurrence::rDateTimes() const { return mRDateTimes; } void Recurrence::setRDateTimes( const DateTimeList &rdates ) { if ( mRecurReadOnly ) return; mRDateTimes = rdates; qSortUnique( mRDateTimes ); updated(); } void Recurrence::addRDateTime( const QDateTime &rdate ) { if ( mRecurReadOnly ) return; mRDateTimes.append( rdate ); qSortUnique( mRDateTimes ); updated(); } DateList Recurrence::rDates() const { return mRDates; } void Recurrence::setRDates( const DateList &rdates ) { if ( mRecurReadOnly ) return; mRDates = rdates; qSortUnique( mRDates ); updated(); } void Recurrence::addRDate( const QDate &rdate ) { if ( mRecurReadOnly ) return; mRDates.append( rdate ); qSortUnique( mRDates ); updated(); } DateTimeList Recurrence::exDateTimes() const { return mExDateTimes; } void Recurrence::setExDateTimes( const DateTimeList &exdates ) { if ( mRecurReadOnly ) return; mExDateTimes = exdates; qSortUnique( mExDateTimes ); } void Recurrence::addExDateTime( const QDateTime &exdate ) { if ( mRecurReadOnly ) return; mExDateTimes.append( exdate ); qSortUnique( mExDateTimes ); updated(); } DateList Recurrence::exDates() const { return mExDates; } void Recurrence::setExDates( const DateList &exdates ) { if ( mRecurReadOnly ) return; mExDates = exdates; qSortUnique( mExDates ); updated(); } void Recurrence::addExDate( const QDate &exdate ) { if ( mRecurReadOnly ) return; mExDates.append( exdate ); qSortUnique( mExDates ); updated(); } void Recurrence::recurrenceChanged( RecurrenceRule * ) { updated(); } // %%%%%%%%%%%%%%%%%% end:Recurrencerule %%%%%%%%%%%%%%%%%% void Recurrence::dump() const { kdDebug(5800) << "Recurrence::dump():" << endl; kdDebug(5800) << " -) " << mRRules.count() << " RRULEs: " << endl; for ( RecurrenceRule::List::ConstIterator rr = mRRules.begin(); rr != mRRules.end(); ++rr ) { kdDebug(5800) << " -) RecurrenceRule : " << endl; (*rr)->dump(); } kdDebug(5800) << " -) " << mExRules.count() << " EXRULEs: " << endl; for ( RecurrenceRule::List::ConstIterator rr = mExRules.begin(); rr != mExRules.end(); ++rr ) { kdDebug(5800) << " -) ExceptionRule : " << endl; (*rr)->dump(); } kdDebug(5800) << endl << " -) " << mRDates.count() << " Recurrence Dates: " << endl; for ( DateList::ConstIterator it = mRDates.begin(); it != mRDates.end(); ++it ) { kdDebug(5800) << " " << (*it) << endl; } kdDebug(5800) << endl << " -) " << mRDateTimes.count() << " Recurrence Date/Times: " << endl; for ( DateTimeList::ConstIterator it = mRDateTimes.begin(); it != mRDateTimes.end(); ++it ) { kdDebug(5800) << " " << (*it) << endl; } kdDebug(5800) << endl << " -) " << mExDates.count() << " Exceptions Dates: " << endl; for ( DateList::ConstIterator it = mExDates.begin(); it != mExDates.end(); ++it ) { kdDebug(5800) << " " << (*it) << endl; } kdDebug(5800) << endl << " -) " << mExDateTimes.count() << " Exception Date/Times: " << endl; for ( DateTimeList::ConstIterator it = mExDateTimes.begin(); it != mExDateTimes.end(); ++it ) { kdDebug(5800) << " " << (*it) << endl; } } diff --git a/libkcal/recurrence.h b/libkcal/recurrence.h index 773a9d6358..d45240aed4 100644 --- a/libkcal/recurrence.h +++ b/libkcal/recurrence.h @@ -1,513 +1,528 @@ /* This file is part of libkcal. Copyright (c) 1998 Preston Brown Copyright (c) 2001,2003 Cornelius Schumacher Copyright (c) 2002 David Jarvie Copyright (C) 2005 Reinhold Kainhofer This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KCAL_RECURRENCE_H #define KCAL_RECURRENCE_H #include #include #include #include "libkcal_export.h" #include "recurrencerule.h" namespace KCal { class RecurrenceRule; /** This class represents a recurrence rule for a calendar incidence. It manages all recurrence rules, recurrence date/times, exception rules and exception date times that can appear inside calendar items. Each recurrence rule and exception rule is represented as an object of type RecurrenceRule. For the simple case where at most one recurrence rule is present, this class provides shortcut methods to set the type: setMinutely() setHourly() setDaily() setWeekly() setMonthly() setYearly() to set/get general information about the recurrence: setEndDate() setEndDateTime() duration() durationTo() setDuration() frequency() setFrequency() and to set/get specific information about the recurrence within the interval: days() monthDays() monthPositions() yearDays() yearDates() yearMonths() yearPositions() addMonthlyPos() addMonthlyDate() addYearlyDay() addYearlyDate() addYearlyPos() addYearlyMonth() These are all available so that you don't have to work on the RecurrenceRule objects themselves. In other words, in that simple situation the interface stays almost the same compared to the old Recurrence class, which allowed only one recurrence rule. As soon as your recurrence consists of multiple recurrence rules or exception rules, you cannot use the methods mentioned above any more (since each rule will have a different type and different settings). If you still call any of them, the set*ly methods will remove all rules and add one rule with the specified type. The add* and the other set* methods will change only the first recurrence rule, but leave the others untouched. */ class LIBKCAL_EXPORT Recurrence : public RecurrenceRule::Observer { public: class Observer { public: virtual ~Observer() {} /** This method will be called on each change of the recurrence object */ virtual void recurrenceUpdated( Recurrence * ) = 0; }; /** enumeration for describing how an event recurs, if at all. */ enum { rNone = 0, rMinutely = 0x001, rHourly = 0x0002, rDaily = 0x0003, rWeekly = 0x0004, rMonthlyPos = 0x0005, rMonthlyDay = 0x0006, rYearlyMonth = 0x0007, rYearlyDay = 0x0008, rYearlyPos = 0x0009, rOther = 0x000A, rMax=0x00FF }; Recurrence(); Recurrence( const Recurrence& ); virtual ~Recurrence(); bool operator==( const Recurrence& ) const; bool operator!=( const Recurrence& r ) const { return !operator==(r); } /** Return the start date/time of the recurrence (Time for floating incidences will be 0:00). @return the current start/time of the recurrence. */ QDateTime startDateTime() const; /** Return the start date/time of the recurrence */ QDate startDate() const { return mStartDateTime.date(); } /** Set start of recurrence, as a date and time. Also sets the incidence to non-floating. @param start the new start date/time of the incidence. */ void setStartDateTime( const QDateTime &start ); /** Set start of recurrence, as a date. Also sets the incidence to floating. @param start The new start date of the incidence. */ void setStartDate( const QDate &start ); /** Set whether the recurrence has no time, just a date. * Floating means -- according to rfc2445 -- that the event has no time * associated. * N.B. This property is derived by default from whether setStartDateTime() or * setStartDate() is called. * @return whether the recurrence has a time (false) or it is just a date (true). */ bool doesFloat() const { return mFloating; } /** Sets whether the dtstart is a floating time (i.e. has no time attached) @param floats If the recurrence is for all-day item (true) or has a time associated (false). */ void setFloats( bool floats ); /** Set if recurrence is read-only or can be changed. */ void setRecurReadOnly(bool readOnly) { mRecurReadOnly = readOnly; } /** Returns true if the recurrence is read-only, or false if it can be changed. */ bool recurReadOnly() const { return mRecurReadOnly; } /** Returns whether the event recurs at all. */ bool doesRecur() const; /** Returns the event's recurrence status. See the enumeration at the top * of this file for possible values. */ ushort recurrenceType() const; /** Returns the recurrence status for a recurrence rule. * See the enumeration at the top of this file for possible values. */ static ushort recurrenceType( const RecurrenceRule *rrule ); /** Returns true if the date specified is one on which the event will * recur. */ bool recursOn( const QDate &qd ) const; /** Returns true if the date/time specified is one at which the event will * recur. Times are rounded down to the nearest minute to determine the result. */ bool recursAt( const QDateTime & ) const; /** Removes all recurrence rules. Recurrence dates and exceptions are not removed. */ void unsetRecurs(); /** Removes all recurrence and exception rules and dates. */ void clear(); /** Returns a list of the times on the specified date at which the * recurrence will occur. * @param date the date for which to find the recurrence times. */ QValueList recurTimesOn(const QDate &date) const; + /** Returns a list of all the times at which the recurrence will occur + * between two specified times. + * + * There is a (large) maximum limit to the number of times returned. If due to + * this limit the list is incomplete, this is indicated by the last entry being + * set to an invalid QDateTime value. If you need further values, call the + * method again with a start time set to just after the last valid time returned. + * + * @param start inclusive start of interval + * @param end inclusive end of interval + * @return list of date/time values + */ + DateTimeList timesInInterval( const QDateTime &start, const QDateTime &end ) const; + + /** Returns the date and time of the next recurrence, after the specified date/time. * If the recurrence has no time, the next date after the specified date is returned. * @param preDateTime the date/time after which to find the recurrence. * @return date/time of next recurrence (strictly later than the given QDateTiem), or invalid date if none. */ QDateTime getNextDateTime( const QDateTime& preDateTime ) const; /** Returns the date and time of the last previous recurrence, before the specified date/time. * If a time later than 00:00:00 is specified and the recurrence has no time, 00:00:00 on * the specified date is returned if that date recurs. * @param afterDateTime the date/time before which to find the recurrence. * @return date/time of previous recurrence (strictly earlier than the given QDateTime), or invalid date if none. */ QDateTime getPreviousDateTime( const QDateTime& afterDateTime ) const; /** Returns frequency of recurrence, in terms of the recurrence time period type. */ int frequency() const; /** Sets the frequency of recurrence, in terms of the recurrence time period type. */ void setFrequency(int freq); /** * Returns -1 if the event recurs infinitely, 0 if the end date is set, * otherwise the total number of recurrences, including the initial occurrence. */ int duration() const; /** Sets the total number of times the event is to occur, including both the * first and last. */ void setDuration(int duration); /** Returns the number of recurrences up to and including the date/time specified. */ int durationTo(const QDateTime &) const; /** Returns the number of recurrences up to and including the date specified. */ int durationTo( const QDate &date ) const { return durationTo( QDateTime( date, QTime( 23, 59, 59 ) ) ); } /** Returns the date/time of the last recurrence. * An invalid date is returned if the recurrence has no end. */ QDateTime endDateTime() const; /** Returns the date of the last recurrence. * An invalid date is returned if the recurrence has no end. */ QDate endDate() const; /** Sets the date of the last recurrence. The end time is set to the recurrence start time. * @param endDate the ending date after which to stop recurring. If the * incidence is not floating, the end time will be 23:59.*/ void setEndDate( const QDate &endDate ); /** Sets the date and time of the last recurrence. * @param endDateTime the ending date/time after which to stop recurring. */ void setEndDateTime( const QDateTime &endDateTime ); /** Sets an event to recur minutely. By default infinite recurrence is used. To set an end date use the method setEndDate and to set the number of occurrences use setDuration. This method clears all recurrence rules and adds one rule with a minutely recurrence. All other recurrence components (recurrence date/times, exception date/times and exception rules) are not modified. * @param freq the frequency to recur, e.g. 2 is every other minute */ void setMinutely( int freq ); /** Sets an event to recur hourly. By default infinite recurrence is used. The minute of the recurrence is taken from the start date (if you need to change it, you will have to modify the defaultRRule's byMinute list manually. To set an end date use the method setEndDate and to set the number of occurrences use setDuration. This method clears all recurrence rules and adds one rule with a hourly recurrence. All other recurrence components (recurrence date/times, exception date/times and exception rules) are not modified. * @param freq the frequency to recur, e.g. 2 is every other hour */ void setHourly( int freq ); /** Sets an event to recur daily. By default infinite recurrence is used. The minute and second of the recurrence is taken from the start date (if you need to change them, you will have to modify the defaultRRule's byMinute list manually. To set an end date use the method setEndDate and to set the number of occurrences use setDuration. This method clears all recurrence rules and adds one rule with a daily recurrence. All other recurrence components (recurrence date/times, exception date/times and exception rules) are not modified. * @param freq the frequency to recur, e.g. 2 is every other day */ void setDaily( int freq ); /** Sets an event to recur weekly. By default infinite recurrence is used. To set an end date use the method setEndDate and to set the number of occurrences use setDuration. This method clears all recurrence rules and adds one rule with a weekly recurrence. All other recurrence components (recurrence date/times, exception date/times and exception rules) are not modified. * @param freq the frequency to recur, e.g. every other week etc. * @param weekStart the first day of the week (Monday=1 .. Sunday=7, default is Monday). */ void setWeekly( int freq, int weekStart = 1 ); /** Sets an event to recur weekly. By default infinite recurrence is used. To set an end date use the method setEndDate and to set the number of occurrences use setDuration. This method clears all recurrence rules and adds one rule with a weekly recurrence. All other recurrence components (recurrence date/times, exception date/times and exception rules) are not modified. * @param freq the frequency to recur, e.g. every other week etc. * @param days a 7 bit array indicating which days on which to recur (bit 0 = Monday). * @param weekStart the first day of the week (Monday=1 .. Sunday=7, default is Monday). */ void setWeekly( int freq, const QBitArray &days, int weekStart = 1 ); /** Adds days to the weekly day recurrence list. * @param days a 7 bit array indicating which days on which to recur (bit 0 = Monday). */ void addWeeklyDays( const QBitArray &days ); /** Returns the first day of the week. Uses only the * first RRULE if present (i.e. a second RRULE as well as all EXRULES are * ignored! * @return Weekday of the first day of the week (Monday=1 .. Sunday=7) */ int weekStart() const; /** Returns week day mask (bit 0 = Monday). */ QBitArray days() const; // Emulate the old behavior /** Sets an event to recur monthly. By default infinite recurrence is used. The date of the monthly recurrence will be taken from the start date unless you explicitly add one or more recurrence dates with addMonthlyDate or a recurrence position in the month (e.g. first monday) using addMonthlyPos. To set an end date use the method setEndDate and to set the number of occurrences use setDuration. This method clears all recurrence rules and adds one rule with a monthly recurrence. All other recurrence components (recurrence date/times, exception date/times and exception rules) are not modified. * @param freq the frequency to recur, e.g. 3 for every third month. */ void setMonthly( int freq ); /** Adds a position (e.g. first monday) to the monthly recurrence rule. * @param pos the position in the month for the recurrence, with valid * values being 1-5 (5 weeks max in a month). * @param days the days for the position to recur on (bit 0 = Monday). * Example: pos = 2, and bits 0 and 2 are set in days: * the rule is to repeat every 2nd Monday and Wednesday in the month. */ void addMonthlyPos( short pos, const QBitArray &days ); void addMonthlyPos( short pos, ushort day ); /** Adds a date (e.g. the 15th of each month) to the monthly day * recurrence list. * @param day the date in the month to recur. */ void addMonthlyDate( short day ); /** Returns list of day positions in months. */ QValueList monthPositions() const; /** Returns list of day numbers of a month. */ // Emulate old behavior QValueList monthDays() const; /** Sets an event to recur yearly. By default, this will recur every year * on the same date (e.g. every year on April 15 if the start date was * April 15). * The day of the year can be specified with addYearlyDay(). * The day of the month can be specified with addYearlyByDate * If both a month and a day ar specified with addYearlyMonth and * addYearlyDay, the day is understood as day number within the month. * * A position (e.g. 3rd Sunday of year/month, or last Friday of year/month) * can be specified with addYearlyPos. Again, if a month is specified, * this position is understood as within that month, otherwise within * the year. * * By default infinite recurrence is used. To set an end date use the * method setEndDate and to set the number of occurrences use setDuration. This method clears all recurrence rules and adds one rule with a yearly recurrence. All other recurrence components (recurrence date/times, exception date/times and exception rules) are not modified. * @param freq the frequency to recur, e.g. 3 for every third year. */ void setYearly( int freq ); /** Adds day number of year within a yearly recurrence. * By default infinite recurrence is used. To set an end date use the * method setEndDate and to set the number of occurrences use setDuration. * @param day the day of the year for the event. E.g. if day is 60, this * means Feb 29 in leap years and March 1 in non-leap years. */ void addYearlyDay( int day ); /** Adds date within a yearly recurrence. The month(s) for the recurrence * can be specified with addYearlyMonth(), otherwise the month of the * start date is used. * * By default infinite recurrence is used. To set an end date use the * method setEndDate and to set the number of occurrences use setDuration. * @param date the day of the month for the event */ void addYearlyDate( int date ); /** Adds month in yearly recurrence. You can specify specific day numbers * within the months (by calling addYearlyDate()) or specific day positions * within the month (by calling addYearlyPos). * @param _rNum the month in which the event shall recur. */ void addYearlyMonth( short _rNum ); /** Adds position within month/year within a yearly recurrence. If months * are specified (via addYearlyMonth()), the parameters are understood as * position within these months, otherwise within the year. * * By default infinite recurrence is used. * To set an end date use the method setEndDate and to set the number * of occurrences use setDuration. * @param pos the position in the month/year for the recurrence, with valid * values being 1 to 53 and -1 to -53 (53 weeks max in a year). * @param days the days for the position to recur on (bit 0 = Monday). * Example: pos = 2, and bits 0 and 2 are set in days * If months are specified (via addYearlyMonth), e.g. March, the rule is * to repeat every year on the 2nd Monday and Wednesday of March. * If no months are specified, the fule is to repeat every year on the * 2nd Monday and Wednesday of the year. */ void addYearlyPos( short pos, const QBitArray &days ); /** Returns the day numbers within a yearly recurrence. * @return the days of the year for the event. E.g. if the list contains * 60, this means the recurrence happens on day 60 of the year, i.e. * on Feb 29 in leap years and March 1 in non-leap years. */ QValueList yearDays() const; /** Returns the dates within a yearly recurrence. * @return the days of the month for the event. E.g. if the list contains * 13, this means the recurrence happens on the 13th of the month. * The months for the recurrence can be obtained through * yearlyMonths(). If this list is empty, the month of the start * date is used. */ QValueList yearDates() const; /** Returns the months within a yearly recurrence. * @return the months for the event. E.g. if the list contains * 11, this means the recurrence happens in November. * The days for the recurrence can be obtained either through * yearDates() if they are given as dates within the month or * through yearlyPositions() if they are given as positions within the * month. If none is specified, the date of the start date is used. */ QValueList yearMonths() const; /** Returns the positions within a yearly recurrence. * @return the positions for the event, either within a month (if months * are set through addYearlyMonth()) or within the year. * E.g. if the list contains {Pos=3, Day=5}, this means the third * friday. If a month is set this position is understoodas third * Friday in the given months, otherwise as third Friday of the * year. */ /** Returns list of day positions in months, for a recursYearlyPos recurrence rule. */ QValueList yearPositions() const; /** Upper date limit for recurrences */ static const QDate MAX_DATE; /** Debug output. */ void dump() const; // RRULE RecurrenceRule::List rRules() const; void addRRule( RecurrenceRule *rrule ); void removeRRule( RecurrenceRule *rrule ); // EXRULE RecurrenceRule::List exRules() const; void addExRule( RecurrenceRule *exrule ); void removeExRule( RecurrenceRule *exrule ); // RDATE DateTimeList rDateTimes() const; DateList rDates() const; void setRDateTimes( const DateTimeList &rdates); void setRDates( const DateList &rdates); void addRDateTime( const QDateTime &rdate ); void addRDate( const QDate &rdate ); // ExDATE DateTimeList exDateTimes() const; DateList exDates() const; void setExDateTimes( const DateTimeList &exdates); void setExDates( const DateList &exdates); void addExDateTime( const QDateTime &exdate ); void addExDate( const QDate &exdate ); RecurrenceRule *defaultRRule( bool create = false ) const; RecurrenceRule *defaultRRuleConst() const; void updated(); /** Installs an observer. Whenever some setting of this recurrence object is changed, the recurrenceUpdated( Recurrence* ) method of each observer will be called to inform it of changes. @param observer the Recurrence::Observer-derived object, which will be installed as an observer of this object. */ void addObserver( Observer *observer ); /** Removes an observer that was added with addObserver. If the given object was not an observer, it does nothing. @param observer the Recurrence::Observer-derived object to be removed from the list of observers of this object. */ void removeObserver( Observer *observer ); void recurrenceChanged( RecurrenceRule * ); protected: RecurrenceRule *setNewRecurrenceType( RecurrenceRule::PeriodType type, int freq ); private: RecurrenceRule::List mExRules; RecurrenceRule::List mRRules; QValueListmRDateTimes; QValueList mRDates; QValueList mExDateTimes; QValueList mExDates; QDateTime mStartDateTime; // date/time of first recurrence bool mFloating; // the recurrence has no time, just a date bool mRecurReadOnly; // Cache the type of the recurrence with the old system (e.g. MonthlyPos) mutable ushort mCachedType; QValueList mObservers; class Private; Private *d; }; } #endif diff --git a/libkcal/recurrencerule.cpp b/libkcal/recurrencerule.cpp index ef6f9ccd90..57b801d617 100644 --- a/libkcal/recurrencerule.cpp +++ b/libkcal/recurrencerule.cpp @@ -1,1428 +1,1548 @@ /* This file is part of libkcal. Copyright (c) 2005 Reinhold Kainhofer This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "recurrencerule.h" #include #include #include #include #include #include using namespace KCal; +// Maximum number of intervals to process +const int LOOP_LIMIT = 10000; // FIXME: If Qt is ever changed so that QDateTime:::addSecs takes into account // DST shifts, we need to use our own addSecs method, too, since we // need to caalculate things in UTC! /** Workaround for broken QDateTime::secsTo (at least in Qt 3.3). While QDateTime::secsTo does take time zones into account, QDateTime::addSecs does not, so we have a problem: QDateTime d1(QDate(2005, 10, 30), QTime(1, 30, 0) ); QDateTime d2(QDate(2005, 10, 30), QTime(3, 30, 0) ); kdDebug(5800) << "d1=" << d1 << ", d2=" << d2 << endl; kdDebug(5800) << "d1.secsTo(d2)=" << d1.secsTo(d2) << endl; kdDebug(5800) << "d1.addSecs(d1.secsTo(d2))=" << d1.addSecs(d1.secsTo(d2)) << endl; This code generates the following output: libkcal: d1=Son Okt 30 01:30:00 2005, d2=Son Okt 30 03:30:00 2005 libkcal: d1.secsTo(d2)=10800 libkcal: d1.addSecs(d1.secsTo(d2))=Son Okt 30 04:30:00 2005 Notice that secsTo counts the hour between 2:00 and 3:00 twice, while adddSecs doesn't and so has one additional hour. This basically makes it impossible to use QDateTime for *any* calculations, in local time zone as well as in UTC. Since we don't want to use time zones anyway, but do all secondsly/minutely/hourly calculations in UTC, we simply use our own secsTo, which ignores all time zone shifts. */ long long ownSecsTo( const QDateTime &dt1, const QDateTime &dt2 ) { long long res = static_cast( dt1.date().daysTo( dt2.date() ) ) * 24*3600; res += dt1.time().secsTo( dt2.time() ); return res; } /************************************************************************** * DateHelper * **************************************************************************/ class DateHelper { public: #ifndef NDEBUG static QString dayName( short day ); #endif static QDate getNthWeek( int year, int weeknumber, short weekstart = 1 ); static int weekNumbersInYear( int year, short weekstart = 1 ); static int getWeekNumber( const QDate &date, short weekstart, int *year = 0 ); static int getWeekNumberNeg( const QDate &date, short weekstart, int *year = 0 ); }; #ifndef NDEBUG QString DateHelper::dayName( short day ) { switch ( day ) { case 1: return "MO"; break; case 2: return "TU"; break; case 3: return "WE"; break; case 4: return "TH"; break; case 5: return "FR"; break; case 6: return "SA"; break; case 7: return "SU"; break; default: return "??"; } } #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 ) { // kdDebug(5800) << "Getting week number for " << date << " with weekstart="<= 0 ) { // in first week of next year; if ( year ) *year = date.year() + 1; dt = dtn; daysto = dayston; } 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; } /************************************************************************** * RecurrenceRule::Constraint * **************************************************************************/ RecurrenceRule::Constraint::Constraint( int wkst ) { weekstart = wkst; clear(); } RecurrenceRule::Constraint::Constraint( const QDateTime &preDate, PeriodType type, int wkst ) { weekstart = wkst; readDateTime( preDate, type ); } void RecurrenceRule::Constraint::clear() { year = 0; month = 0; day = 0; hour = -1; minute = -1; second = -1; weekday = 0; weekdaynr = 0; weeknumber = 0; yearday = 0; } bool RecurrenceRule::Constraint::matches( const QDate &dt, RecurrenceRule::PeriodType type ) const { // If the event recurs in week 53 or 1, the day might not belong to the same // year as the week it is in. E.g. Jan 1, 2005 is in week 53 of year 2004. // So we can't simply check the year in that case! if ( weeknumber == 0 ) { if ( year > 0 && year != dt.year() ) return false; } else { int y; if ( weeknumber > 0 && weeknumber != DateHelper::getWeekNumber( dt, weekstart, &y ) ) return false; if ( weeknumber < 0 && weeknumber != DateHelper::getWeekNumberNeg( dt, weekstart, &y ) ) return false; if ( year > 0 && year != y ) return false; } if ( month > 0 && month != dt.month() ) return false; if ( day > 0 && day != dt.day() ) return false; if ( day < 0 && dt.day() != (dt.daysInMonth() + day + 1 ) ) return false; if ( weekday > 0 ) { if ( weekday != dt.dayOfWeek() ) return false; if ( weekdaynr != 0 ) { // If it's a yearly recurrence and a month is given, the position is // still in the month, not in the year. bool inMonth = (type == rMonthly) || ( type == rYearly && month > 0 ); // Monthly if ( weekdaynr > 0 && inMonth && weekdaynr != (dt.day() - 1)/7 + 1 ) return false; if ( weekdaynr < 0 && inMonth && weekdaynr != -((dt.daysInMonth() - dt.day() )/7 + 1 ) ) return false; // Yearly if ( weekdaynr > 0 && !inMonth && weekdaynr != (dt.dayOfYear() - 1)/7 + 1 ) return false; if ( weekdaynr < 0 && !inMonth && 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; } bool RecurrenceRule::Constraint::matches( const QDateTime &dt, RecurrenceRule::PeriodType type ) const { if ( !matches( dt.date(), type ) ) return false; if ( hour >= 0 && hour != dt.time().hour() ) return false; if ( minute >= 0 && minute != dt.time().minute() ) return false; if ( second >= 0 && second != dt.time().second() ) return false; return true; } bool RecurrenceRule::Constraint::isConsistent( PeriodType /*period*/) const { // TODO: Check for consistency, e.g. byyearday=3 and bymonth=10 return true; } QDateTime RecurrenceRule::Constraint::intervalDateTime( RecurrenceRule::PeriodType type ) const { QDateTime dt; dt.setTime( QTime( 0, 0, 0 ) ); dt.setDate( QDate( year, (month>0)?month:1, (day>0)?day:1 ) ); if ( day < 0 ) dt = dt.addDays( dt.date().daysInMonth() + day ); switch ( type ) { case rSecondly: dt.setTime( QTime( hour, minute, second ) ); break; case rMinutely: dt.setTime( QTime( hour, minute, 1 ) ); break; case rHourly: dt.setTime( QTime( hour, 1, 1 ) ); break; case rDaily: break; case rWeekly: dt = DateHelper::getNthWeek( year, weeknumber, weekstart ); break; case rMonthly: dt.setDate( QDate( year, month, 1 ) ); break; case rYearly: dt.setDate( QDate( year, 1, 1 ) ); break; default: break; } return dt; } // 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! DateTimeList RecurrenceRule::Constraint::dateTimes( RecurrenceRule::PeriodType type ) const { // kdDebug(5800) << " RecurrenceRule::Constraint::dateTimes: " << endl; DateTimeList result; bool done = false; // TODO_Recurrence: Handle floating QTime tm( hour, minute, second ); if ( !isConsistent( type ) ) return result; if ( !done && day > 0 && month > 0 ) { QDateTime dt( QDate( year, month, day ), tm ); if ( dt.isValid() ) result.append( dt ); done = true; } if ( !done && day < 0 && month > 0 ) { QDateTime dt( QDate( year, month, 1 ), tm ); dt = dt.addDays( dt.date().daysInMonth() + day ); if ( dt.isValid() ) result.append( dt ); 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(); } for ( uint d = dstart; d <= dend; ++d ) { QDateTime dt( QDate( year, m, d ), tm ); if ( dt.isValid() ) result.append( dt ); } } 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) ); result.append( QDateTime( d, tm ) ); 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 ); result.append( QDateTime( wst, tm ) ); } else { for ( int i = 0; i < 7; ++i ) { result.append( QDateTime( wst, tm ) ); 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 == rMonthly) || ( type == 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 ); result.append( QDateTime( dt, tm ) ); } else if ( weekdaynr < 0 ) { dt = dt.addDays( weekdaynr * 7 ); result.append( QDateTime( dt, tm ) ); } else { // loop through all possible weeks, non-matching will be filtered later for ( int i = 0; i < maxloop; ++i ) { result.append( QDateTime( dt, tm ) ); dt = dt.addDays( 7 ); } } } // weekday != 0 // Only use those times that really match all other constraints, too DateTimeList valid; DateTimeList::Iterator it; for ( it = result.begin(); it != result.end(); ++it ) { if ( matches( *it, type ) ) valid.append( *it ); } // 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; } bool RecurrenceRule::Constraint::increase( RecurrenceRule::PeriodType type, int freq ) { // convert the first day of the interval to QDateTime // Sub-daily types need to be converted to UTC to correctly handle DST shifts QDateTime dt( intervalDateTime( type ) ); // Now add the intervals switch ( type ) { case rSecondly: dt = dt.addSecs( freq ); break; case rMinutely: dt = dt.addSecs( 60*freq ); break; case rHourly: dt = dt.addSecs( 3600 * freq ); break; case rDaily: dt = dt.addDays( freq ); break; case rWeekly: dt = dt.addDays( 7*freq ); break; case rMonthly: dt = dt.addMonths( freq ); break; case rYearly: dt = dt.addYears( freq ); break; default: break; } // Convert back from QDateTime to the Constraint class readDateTime( dt, type ); return true; } bool RecurrenceRule::Constraint::readDateTime( const QDateTime &preDate, PeriodType type ) { clear(); switch ( type ) { // Really fall through! Only weekly needs to be treated differentely! case rSecondly: second = preDate.time().second(); case rMinutely: minute = preDate.time().minute(); case rHourly: hour = preDate.time().hour(); case rDaily: day = preDate.date().day(); case rMonthly: month = preDate.date().month(); case rYearly: year = preDate.date().year(); break; case rWeekly: // Determine start day of the current week, calculate the week number from that weeknumber = DateHelper::getWeekNumber( preDate.date(), weekstart, &year ); break; default: break; } return true; } RecurrenceRule::RecurrenceRule( ) : mPeriod( rNone ), mFrequency( 0 ), mIsReadOnly( false ), mFloating( false ), mWeekStart(1) { } RecurrenceRule::RecurrenceRule( const RecurrenceRule &r ) { mRRule = r.mRRule; mPeriod = r.mPeriod; mDateStart = r.mDateStart; mDuration = r.mDuration; mDateEnd = r.mDateEnd; mFrequency = r.mFrequency; mIsReadOnly = r.mIsReadOnly; mFloating = r.mFloating; 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; setDirty(); } RecurrenceRule::~RecurrenceRule() { } bool RecurrenceRule::operator==( const RecurrenceRule& r ) const { if ( mPeriod != r.mPeriod ) return false; if ( mDateStart != r.mDateStart ) return false; if ( mDuration != r.mDuration ) return false; if ( mDateEnd != r.mDateEnd ) return false; if ( mFrequency != r.mFrequency ) return false; if ( mIsReadOnly != r.mIsReadOnly ) return false; if ( mFloating != r.mFloating ) return false; if ( mBySeconds != r.mBySeconds ) return false; if ( mByMinutes != r.mByMinutes ) return false; if ( mByHours != r.mByHours ) return false; if ( mByDays != r.mByDays ) return false; if ( mByMonthDays != r.mByMonthDays ) return false; if ( mByYearDays != r.mByYearDays ) return false; if ( mByWeekNumbers != r.mByWeekNumbers ) return false; if ( mByMonths != r.mByMonths ) return false; if ( mBySetPos != r.mBySetPos ) return false; if ( mWeekStart != r.mWeekStart ) return false; return true; } void RecurrenceRule::addObserver( Observer *observer ) { if ( !mObservers.contains( observer ) ) mObservers.append( observer ); } void RecurrenceRule::removeObserver( Observer *observer ) { if ( mObservers.contains( observer ) ) mObservers.remove( observer ); } void RecurrenceRule::setRecurrenceType( PeriodType period ) { if ( isReadOnly() ) return; mPeriod = period; setDirty(); } QDateTime RecurrenceRule::endDt( bool *result ) const { if ( result ) *result = false; if ( mPeriod == rNone ) return QDateTime(); if ( mDuration < 0 ) return QDateTime(); if ( mDuration == 0 ) { if ( result ) *result = true; return mDateEnd; } // N occurrences. Check if we have a full cache. If so, return the cached end date. if ( ! mCached ) { // If not enough occurrences can be found (i.e. inconsistent constraints) if ( !buildCache() ) return QDateTime(); } if ( result ) *result = true; return mCachedDateEnd; } void RecurrenceRule::setEndDt( const QDateTime &dateTime ) { if ( isReadOnly() ) return; mDateEnd = dateTime; mDuration = 0; // set to 0 because there is an end date/time setDirty(); } void RecurrenceRule::setDuration(int duration) { if ( isReadOnly() ) return; mDuration = duration; setDirty(); } void RecurrenceRule::setFloats( bool floats ) { if ( isReadOnly() ) return; mFloating = floats; setDirty(); } void RecurrenceRule::clear() { if ( isReadOnly() ) return; mPeriod = rNone; mBySeconds.clear(); mByMinutes.clear(); mByHours.clear(); mByDays.clear(); mByMonthDays.clear(); mByYearDays.clear(); mByWeekNumbers.clear(); mByMonths.clear(); mBySetPos.clear(); mWeekStart = 1; setDirty(); } void RecurrenceRule::setDirty() { mConstraints.clear(); buildConstraints(); mDirty = true; mCached = false; mCachedDates.clear(); for ( QValueList::ConstIterator it = mObservers.begin(); it != mObservers.end(); ++it ) { if ( (*it) ) (*it)->recurrenceChanged( this ); } } void RecurrenceRule::setStartDt( const QDateTime &start ) { if ( isReadOnly() ) return; mDateStart = start; setDirty(); } void RecurrenceRule::setFrequency(int freq) { if ( isReadOnly() || freq <= 0 ) return; mFrequency = freq; setDirty(); } void RecurrenceRule::setBySeconds( const QValueList bySeconds ) { if ( isReadOnly() ) return; mBySeconds = bySeconds; setDirty(); } void RecurrenceRule::setByMinutes( const QValueList byMinutes ) { if ( isReadOnly() ) return; mByMinutes = byMinutes; setDirty(); } void RecurrenceRule::setByHours( const QValueList byHours ) { if ( isReadOnly() ) return; mByHours = byHours; setDirty(); } void RecurrenceRule::setByDays( const QValueList byDays ) { if ( isReadOnly() ) return; mByDays = byDays; setDirty(); } void RecurrenceRule::setByMonthDays( const QValueList byMonthDays ) { if ( isReadOnly() ) return; mByMonthDays = byMonthDays; setDirty(); } void RecurrenceRule::setByYearDays( const QValueList byYearDays ) { if ( isReadOnly() ) return; mByYearDays = byYearDays; setDirty(); } void RecurrenceRule::setByWeekNumbers( const QValueList byWeekNumbers ) { if ( isReadOnly() ) return; mByWeekNumbers = byWeekNumbers; setDirty(); } void RecurrenceRule::setByMonths( const QValueList byMonths ) { if ( isReadOnly() ) return; mByMonths = byMonths; setDirty(); } void RecurrenceRule::setBySetPos( const QValueList bySetPos ) { if ( isReadOnly() ) return; mBySetPos = bySetPos; setDirty(); } void RecurrenceRule::setWeekStart( short weekStart ) { if ( isReadOnly() ) return; mWeekStart = weekStart; 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; // } void RecurrenceRule::buildConstraints() { + mTimedRepetition = 0; + mNoByRules = mBySetPos.isEmpty(); mConstraints.clear(); Constraint con; if ( mWeekStart > 0 ) con.weekstart = mWeekStart; mConstraints.append( con ); Constraint::List tmp; Constraint::List::const_iterator it; QValueList::const_iterator intit; #define intConstraint( list, element ) \ if ( !list.isEmpty() ) { \ + mNoByRules = false; \ for ( it = mConstraints.constBegin(); it != mConstraints.constEnd(); ++it ) { \ for ( intit = list.constBegin(); intit != list.constEnd(); ++intit ) { \ con = (*it); \ con.element = (*intit); \ tmp.append( con ); \ } \ } \ mConstraints = tmp; \ tmp.clear(); \ } intConstraint( mBySeconds, second ); intConstraint( mByMinutes, minute ); intConstraint( mByHours, hour ); intConstraint( mByMonthDays, day ); intConstraint( mByMonths, month ); intConstraint( mByYearDays, yearday ); intConstraint( mByWeekNumbers, weeknumber ); #undef intConstraint if ( !mByDays.isEmpty() ) { + mNoByRules = false; for ( it = mConstraints.constBegin(); it != mConstraints.constEnd(); ++it ) { QValueList::const_iterator dayit; for ( dayit = mByDays.constBegin(); dayit != mByDays.constEnd(); ++dayit ) { con = (*it); con.weekday = (*dayit).day(); con.weekdaynr = (*dayit).pos(); tmp.append( con ); } } mConstraints = tmp; tmp.clear(); } #define fixConstraint( element, value ) \ { \ tmp.clear(); \ for ( it = mConstraints.constBegin(); it != mConstraints.constEnd(); ++it ) { \ con = (*it); con.element = value; tmp.append( con ); \ } \ mConstraints = tmp; \ } // 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( weekday, 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( month, mDateStart.date().month() ); } case rMonthly: if ( mByDays.isEmpty() && mByWeekNumbers.isEmpty() && mByYearDays.isEmpty() && mByMonthDays.isEmpty() ) { fixConstraint( day, mDateStart.date().day() ); } case rWeekly: case rDaily: if ( mByHours.isEmpty() ) { fixConstraint( hour, mDateStart.time().hour() ); } case rHourly: if ( mByMinutes.isEmpty() ) { fixConstraint( minute, mDateStart.time().minute() ); } case rMinutely: if ( mBySeconds.isEmpty() ) { fixConstraint( second, mDateStart.time().second() ); } case rSecondly: default: break; } #undef fixConstraint - Constraint::List::Iterator conit = mConstraints.begin(); - while ( conit != mConstraints.end() ) { - if ( (*conit).isConsistent( mPeriod ) ) { - ++conit; - } else { - conit = mConstraints.remove( conit ); + if ( mNoByRules ) { + switch ( mPeriod ) { + case rHourly: + mTimedRepetition = mFrequency * 3600; + break; + case rMinutely: + mTimedRepetition = mFrequency * 60; + break; + case rSecondly: + mTimedRepetition = mFrequency; + break; + default: + break; + } + } else { + Constraint::List::Iterator conit = mConstraints.begin(); + while ( conit != mConstraints.end() ) { + if ( (*conit).isConsistent( mPeriod ) ) { + ++conit; + } else { + conit = mConstraints.remove( conit ); + } } } } bool RecurrenceRule::buildCache() const { kdDebug(5800) << " RecurrenceRule::buildCache: " << endl; // Build the list of all occurrences of this event (we need that to determine // the end date!) Constraint interval( getNextValidDateInterval( startDt(), recurrenceType() ) ); QDateTime next; DateTimeList dts = datesForInterval( interval, recurrenceType() ); DateTimeList::Iterator it = dts.begin(); // Only use dates after the event has started (start date is only included // if it matches) while ( it != dts.end() ) { if ( (*it) < startDt() ) it = dts.remove( it ); else ++it; } // dts.prepend( startDt() ); // the start date is always the first occurrence int loopnr = 0; int dtnr = dts.count(); // some validity checks to avoid infinite loops (i.e. if we have // done this loop already 10000 times and found no occurrence, bail out ) while ( loopnr < 10000 && dtnr < mDuration ) { interval.increase( recurrenceType(), frequency() ); // The returned date list is already sorted! dts += datesForInterval( interval, recurrenceType() ); dtnr = dts.count(); ++loopnr; } if ( int(dts.count()) > mDuration ) { // we have picked up more occurrences than necessary, remove them it = dts.at( mDuration ); while ( it != dts.end() ) it = dts.remove( it ); } mCached = true; mCachedDates = dts; kdDebug(5800) << " Finished Building Cache, cache has " << dts.count() << " entries:" << endl; // it = dts.begin(); // while ( it != dts.end() ) { // kdDebug(5800) << " -=> " << (*it) << endl; // ++it; // } if ( int(dts.count()) == mDuration ) { mCachedDateEnd = dts.last(); return true; } else { mCachedDateEnd = QDateTime(); return false; } } bool RecurrenceRule::dateMatchesRules( const QDateTime &qdt ) const { bool match = false; for ( Constraint::List::ConstIterator it = mConstraints.begin(); it!=mConstraints.end(); ++it ) { match = match || ( (*it).matches( qdt, recurrenceType() ) ); } return match; } bool RecurrenceRule::recursOn( const QDate &qd ) const { // kdDebug(5800) << " RecurrenceRule::recursOn: " << qd << endl; if ( qd < startDt().date() ) return false; // Start date is only included if it really matches // if ( qd == startDt().date() ) return true; if ( mDuration >= 0 && qd > endDt().date() ) 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 ( Constraint::List::ConstIterator it = mConstraints.begin(); it!=mConstraints.end(); ++it ) { match = match || ( (*it).matches( qd, recurrenceType() ) ); } if ( !match ) return false; QDateTime tmp( qd, QTime( 0, 0, 0 ) ); Constraint interval( getNextValidDateInterval( tmp, 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! DateTimeList times = datesForInterval( interval, recurrenceType() ); DateTimeList::ConstIterator it = times.begin(); while ( ( it != times.end() ) && ( (*it).date() < qd ) ) ++it; if ( it != times.end() ) { // If we are beyond the end... if ( mDuration >= 0 && (*it) > endDt() ) return false; if ( (*it).date() == qd ) return true; } return false; } bool RecurrenceRule::recursAt( const QDateTime &qd ) const { // kdDebug(5800) << " RecurrenceRule::recursAt: " << qd << endl; if ( doesFloat() ) return recursOn( qd.date() ); if ( qd < startDt() ) return false; // Start date is only included if it really matches // if ( qd == startDt() ) return true; if ( mDuration >= 0 && qd > endDt() ) return false; // The date must be in an appropriate interval (getNextValidDateInterval), // Plus it must match at least one of the constraints bool match = dateMatchesRules( qd ); if ( !match ) return false; // if it recurs every interval, speed things up... // if ( mFrequency == 1 && mBySetPos.isEmpty() && mByDays.isEmpty() ) return true; Constraint interval( getNextValidDateInterval( qd, recurrenceType() ) ); // TODO_Recurrence: Does this work with BySetPos??? if ( interval.matches( qd, recurrenceType() ) ) return true; return false; } TimeList RecurrenceRule::recurTimesOn( const QDate &date ) const { // kdDebug(5800) << " RecurrenceRule::recurTimesOn: " << date << endl; TimeList lst; if ( !recursOn( date ) ) return lst; if ( doesFloat() ) return lst; QDateTime dt( date, QTime( 0, 0, 0 ) ); bool valid = dt.isValid() && ( dt.date() == date ); while ( valid ) { // TODO: Add a flag so that the date is never increased! dt = getNextDate( dt ); valid = dt.isValid() && ( dt.date() == date ); if ( valid ) lst.append( dt.time() ); } return lst; } /** Returns the number of recurrences up to and including the date/time specified. */ int RecurrenceRule::durationTo( const QDateTime &dt ) const { // kdDebug(5800) << " RecurrenceRule::durationTo: " << dt << endl; // Easy cases: either before start, or after all recurrences and we know // their number if ( dt < startDt() ) return 0; // Start date is only included if it really matches // if ( dt == startDt() ) return 1; if ( mDuration > 0 && dt >= endDt() ) return mDuration; QDateTime next( startDt() ); int found = 0; while ( next.isValid() && next <= dt ) { ++found; next = getNextDate( next ); } return found; } QDateTime RecurrenceRule::getPreviousDate( const QDateTime& afterDate ) const { // kdDebug(5800) << " RecurrenceRule::getPreviousDate: " << afterDate << endl; // Beyond end of recurrence if ( afterDate < startDt() ) return QDateTime(); // If we have a cache (duration given), use that QDateTime prev; if ( mDuration > 0 ) { if ( !mCached ) buildCache(); DateTimeList::ConstIterator it = mCachedDates.begin(); while ( it != mCachedDates.end() && (*it) < afterDate ) { prev = *it; ++it; } if ( prev.isValid() && prev < afterDate ) return prev; else return QDateTime(); } // kdDebug(5800) << " getNext date after " << preDate << endl; prev = afterDate; if ( mDuration >= 0 && endDt().isValid() && afterDate > endDt() ) prev = endDt().addSecs( 1 ); Constraint interval( getPreviousValidDateInterval( prev, recurrenceType() ) ); // kdDebug(5800) << "Previous Valid Date Interval for date " << prev << ": " << endl; // interval.dump(); DateTimeList dts = datesForInterval( interval, recurrenceType() ); DateTimeList::Iterator dtit = dts.end(); if ( dtit != dts.begin() ) { do { --dtit; } while ( dtit != dts.begin() && (*dtit) >= prev ); if ( (*dtit) < prev ) { if ( (*dtit) >= startDt() ) return (*dtit); else return QDateTime(); } } // Previous interval. As soon as we find an occurrence, we're done. while ( interval.intervalDateTime( recurrenceType() ) > startDt() ) { interval.increase( recurrenceType(), -frequency() ); // kdDebug(5800) << "Decreased interval: " << endl; // interval.dump(); // The returned date list is sorted DateTimeList dts = datesForInterval( interval, recurrenceType() ); // The list is sorted, so take the last one. if ( dts.count() > 0 ) { prev = dts.last(); if ( prev.isValid() && prev >= startDt() ) return prev; else return QDateTime(); } } return QDateTime(); } QDateTime RecurrenceRule::getNextDate( const QDateTime &preDate ) const { // kdDebug(5800) << " RecurrenceRule::getNextDate: " << preDate << endl; // Beyond end of recurrence if ( mDuration >= 0 && endDt().isValid() && preDate >= endDt() ) return QDateTime(); // Start date is only included if it really matches QDateTime adjustedPreDate; if ( preDate < startDt() ) adjustedPreDate = startDt().addSecs( -1 ); else adjustedPreDate = preDate; if ( mDuration > 0 ) { if ( !mCached ) buildCache(); DateTimeList::ConstIterator it = mCachedDates.begin(); while ( it != mCachedDates.end() && (*it) <= adjustedPreDate ) ++it; if ( it != mCachedDates.end() ) { // kdDebug(5800) << " getNext date after " << adjustedPreDate << ", cached date: " << *it << endl; return (*it); } } // kdDebug(5800) << " getNext date after " << adjustedPreDate << endl; Constraint interval( getNextValidDateInterval( adjustedPreDate, recurrenceType() ) ); DateTimeList dts = datesForInterval( interval, recurrenceType() ); DateTimeList::Iterator dtit = dts.begin(); while ( dtit != dts.end() && (*dtit) <= adjustedPreDate ) ++dtit; if ( dtit != dts.end() ) { if ( mDuration >= 0 && (*dtit) > endDt() ) return QDateTime(); else return (*dtit); } // 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 loopnr = 0; while ( loopnr < 10000 ) { interval.increase( recurrenceType(), frequency() ); DateTimeList dts = datesForInterval( interval, recurrenceType() ); if ( dts.count() > 0 ) { QDateTime ret( dts.first() ); if ( mDuration >= 0 && ret > endDt() ) return QDateTime(); else return ret; } ++loopnr; } return QDateTime(); } +DateTimeList RecurrenceRule::timesInInterval( const QDateTime &dtStart, + const QDateTime &dtEnd ) const +{ + QDateTime start = dtStart; + QDateTime end = dtEnd; + DateTimeList result; + if ( end < mDateStart ) { + return result; // before start of recurrence + } + QDateTime enddt = end; + if ( mDuration >= 0 ) { + 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 ( mTimedRepetition ) { + // It's a simple sub-daily recurrence with no constraints + int n = static_cast( ( mDateStart.secsTo( start ) - 1 ) % mTimedRepetition ); + QDateTime dt = start.addSecs( mTimedRepetition - n ); + if ( dt < enddt ) { + n = static_cast( ( dt.secsTo( enddt ) - 1 ) / mTimedRepetition ) + 1; + // limit n by a sane value else we can "explode". + n = QMIN( n, LOOP_LIMIT ); + for ( int i = 0; i < n; dt = dt.addSecs( mTimedRepetition ), ++i ) { + result += dt; + } + } + return result; + } + + QDateTime st = start; + bool done = false; + if ( mDuration > 0 ) { + if ( !mCached ) { + buildCache(); + } + if ( mCachedDateEnd.isValid() && start >= mCachedDateEnd ) { + return result; // beyond end of recurrence + } + int i = findGE( mCachedDates, start, 0 ); + if ( i >= 0 ) { + int iend = findGT( mCachedDates, enddt, i ); + if ( iend < 0 ) { + iend = mCachedDates.count(); + } else { + done = true; + } + while ( i < iend ) { + result += mCachedDates[i++]; + } + } + if ( 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 = mCachedLastDate.addSecs( 1 ); + } + + Constraint interval( getNextValidDateInterval( st, recurrenceType() ) ); + int loop = 0; + do { + DateTimeList dts = datesForInterval( interval, recurrenceType() ); + int i = 0; + int iend = dts.count(); + if ( loop == 0 ) { + i = findGE( dts, st, 0 ); + if ( i < 0 ) { + i = iend; + } + } + int j = findGT( dts, enddt, i ); + if ( j >= 0 ) { + iend = j; + loop = LOOP_LIMIT; + } + while ( i < iend ) { + result += dts[i++]; + } + // Increase the interval. + interval.increase( recurrenceType(), frequency() ); + } while ( ++loop < LOOP_LIMIT && + interval.intervalDateTime( recurrenceType() ) < end ); + return result; +} + RecurrenceRule::Constraint RecurrenceRule::getPreviousValidDateInterval( const QDateTime &preDate, PeriodType type ) const { // kdDebug(5800) << " (o) getPreviousValidDateInterval after " << preDate << ", type=" << type << endl; long periods = 0; QDateTime nextValid = startDt(); QDateTime start = startDt(); int modifier = 1; QDateTime toDate( preDate ); // 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 // FIXME: All sub-daily periods need to convert to UTC, do the calculations // in UTC, then convert back to the local time zone. Otherwise, // recurrences across DST changes will be determined wrongly switch ( type ) { // Really fall through for sub-daily, since the calculations only differ // by the factor 60 and 60*60! Same for weekly and daily (factor 7) case rHourly: modifier *= 60; case rMinutely: modifier *= 60; case rSecondly: periods = ownSecsTo( start, toDate ) / modifier; // round it down to the next lower multiple of frequency(): periods = ( periods / frequency() ) * frequency(); nextValid = start.addSecs( modifier * periods ); break; case rWeekly: toDate = toDate.addDays( -(7 + toDate.date().dayOfWeek() - mWeekStart) % 7 ); start = start.addDays( -(7 + start.date().dayOfWeek() - mWeekStart) % 7 ); modifier *= 7; case rDaily: periods = start.daysTo( toDate ) / modifier; // round it down to the next lower multiple of frequency(): periods = ( periods / frequency() ) * frequency(); 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(): periods = ( periods / frequency() ) * frequency(); // 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(): periods = ( periods / frequency() ) * frequency(); nextValid.setDate( start.date().addYears( periods ) ); break; default: break; } // kdDebug(5800) << " ~~~> date in previous interval is: : " << nextValid << endl; return Constraint( nextValid, type, mWeekStart ); } RecurrenceRule::Constraint RecurrenceRule::getNextValidDateInterval( const QDateTime &preDate, PeriodType type ) const { // TODO: Simplify this! kdDebug(5800) << " (o) getNextValidDateInterval after " << preDate << ", type=" << type << endl; long periods = 0; QDateTime start = startDt(); QDateTime nextValid( start ); int modifier = 1; QDateTime toDate( preDate ); // 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 // FIXME: All sub-daily periods need to convert to UTC, do the calculations // in UTC, then convert back to the local time zone. Otherwise, // recurrences across DST changes will be determined wrongly switch ( type ) { // Really fall through for sub-daily, since the calculations only differ // by the factor 60 and 60*60! Same for weekly and daily (factor 7) case rHourly: modifier *= 60; case rMinutely: modifier *= 60; case rSecondly: periods = ownSecsTo( start, toDate ) / modifier; periods = QMAX( 0, periods); if ( periods > 0 ) periods += ( frequency() - 1 - ( (periods - 1) % frequency() ) ); nextValid = start.addSecs( modifier * periods ); break; case rWeekly: // correct both start date and current date to start of week toDate = toDate.addDays( -(7 + toDate.date().dayOfWeek() - mWeekStart) % 7 ); start = start.addDays( -(7 + start.date().dayOfWeek() - mWeekStart) % 7 ); modifier *= 7; case rDaily: periods = start.daysTo( toDate ) / modifier; periods = QMAX( 0, periods); if ( periods > 0 ) periods += (frequency() - 1 - ( (periods - 1) % frequency() ) ); nextValid = start.addDays( modifier * periods ); break; case rMonthly: { periods = 12*( toDate.date().year() - start.date().year() ) + ( toDate.date().month() - start.date().month() ); periods = QMAX( 0, periods); if ( periods > 0 ) periods += (frequency() - 1 - ( (periods - 1) % frequency() ) ); // 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( 0, periods); if ( periods > 0 ) periods += ( frequency() - 1 - ( (periods - 1) % frequency() ) ); nextValid.setDate( start.date().addYears( periods ) ); break; default: break; } // kdDebug(5800) << " ~~~> date in next interval is: : " << nextValid << endl; return Constraint( nextValid, type, mWeekStart ); } bool RecurrenceRule::mergeIntervalConstraint( Constraint *merged, const Constraint &conit, const Constraint &interval ) const { Constraint result( interval ); #define mergeConstraint( name, cmparison ) \ if ( conit.name cmparison ) { \ if ( !(result.name cmparison) || result.name == conit.name ) { \ result.name = conit.name; \ } else return 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 if ( merged ) *merged = result; return true; } DateTimeList RecurrenceRule::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 */ // kdDebug(5800) << " RecurrenceRule::datesForInterval: " << endl; // interval.dump(); DateTimeList lst; Constraint::List::ConstIterator conit = mConstraints.begin(); for ( ; conit != mConstraints.end(); ++conit ) { Constraint merged; bool mergeok = mergeIntervalConstraint( &merged, *conit, interval ); // If the information is incomplete, we can't use this constraint if ( merged.year <= 0 || merged.hour < 0 || merged.minute < 0 || merged.second < 0 ) mergeok = false; if ( mergeok ) { // kdDebug(5800) << " -) merged constraint: " << endl; // merged.dump(); // We have a valid constraint, so get all datetimes that match it andd // append it to all date/times of this interval DateTimeList lstnew = merged.dateTimes( type ); lst += lstnew; } } // Sort it so we can apply the BySetPos. Also some logic relies on this being sorted qSortUnique( lst ); /*if ( lst.isEmpty() ) { kdDebug(5800) << " No Dates in Interval " << endl; } else { kdDebug(5800) << " Dates: " << endl; for ( DateTimeList::Iterator it = lst.begin(); it != lst.end(); ++it ) { kdDebug(5800)<< " -) " << (*it).toString() << endl; } kdDebug(5800) << " ---------------------" << endl; }*/ if ( !mBySetPos.isEmpty() ) { DateTimeList tmplst = lst; lst.clear(); QValueList::ConstIterator it; for ( it = mBySetPos.begin(); it != mBySetPos.end(); ++it ) { int pos = *it; if ( pos > 0 ) --pos; if ( pos < 0 ) pos += tmplst.count(); if ( pos >= 0 && uint(pos) < tmplst.count() ) { lst.append( tmplst[pos] ); } } qSortUnique( lst ); } return lst; } void RecurrenceRule::dump() const { #ifndef NDEBUG kdDebug(5800) << "RecurrenceRule::dump():" << endl; if ( !mRRule.isEmpty() ) kdDebug(5800) << " RRULE=" << mRRule << endl; kdDebug(5800) << " Read-Only: " << isReadOnly() << ", dirty: " << mDirty << endl; kdDebug(5800) << " Period type: " << recurrenceType() << ", frequency: " << frequency() << endl; kdDebug(5800) << " #occurrences: " << duration() << endl; kdDebug(5800) << " start date: " << startDt() <<", end date: " << endDt() << endl; #define dumpByIntList(list,label) \ if ( !list.isEmpty() ) {\ QStringList lst;\ for ( QValueList::ConstIterator it = list.begin();\ it != list.end(); ++it ) {\ lst.append( QString::number( *it ) );\ }\ kdDebug(5800) << " " << label << lst.join(", ") << endl;\ } dumpByIntList( mBySeconds, "BySeconds: " ); dumpByIntList( mByMinutes, "ByMinutes: " ); dumpByIntList( mByHours, "ByHours: " ); if ( !mByDays.isEmpty() ) { QStringList lst; for ( QValueList::ConstIterator it = mByDays.begin(); it != mByDays.end(); ++it ) { lst.append( ( ((*it).pos()!=0) ? QString::number( (*it).pos() ) : "" ) + DateHelper::dayName( (*it).day() ) ); } kdDebug(5800) << " ByDays: " << lst.join(", ") << endl; } dumpByIntList( mByMonthDays, "ByMonthDays:" ); dumpByIntList( mByYearDays, "ByYearDays: " ); dumpByIntList( mByWeekNumbers,"ByWeekNr: " ); dumpByIntList( mByMonths, "ByMonths: " ); dumpByIntList( mBySetPos, "BySetPos: " ); #undef dumpByIntList kdDebug(5800) << " Week start: " << DateHelper::dayName( mWeekStart ) << endl; kdDebug(5800) << " Constraints:" << endl; // dump constraints for ( Constraint::List::ConstIterator it = mConstraints.begin(); it!=mConstraints.end(); ++it ) { (*it).dump(); } #endif } void RecurrenceRule::Constraint::dump() const { kdDebug(5800) << " ~> Y="< Q_INLINE_TEMPLATES void qSortUnique( QValueList &lst ) { qHeapSort( lst ); if ( lst.isEmpty() ) return; // Remove all duplicates from the times list // TODO: Make this more efficient! QValueListIterator it = lst.begin(); T last = *it; ++it; T newlast; while ( it != lst.end() ) { newlast = (*it); if ( newlast == last ) it = lst.remove( it ); else { last = newlast; ++it; } } } +template +Q_INLINE_TEMPLATES int findGE( QValueList &lst, const T &value, int start ) +{ + // Do a binary search to find the first item >= value + int st = start - 1; + int end = lst.count(); + while ( end - st > 1 ) { + int i = ( st + end ) / 2; + if ( value <= lst[i] ) { + end = i; + } else { + st = i; + } + } + ++st; + return ( st == int( lst.count() ) ) ? -1 : st; +} + +template +Q_INLINE_TEMPLATES int findGT( QValueList &lst, const T &value, int start ) +{ + // Do a binary search to find the first item > value + int st = start - 1; + int end = lst.count(); + while ( end - st > 1 ) { + int i = ( st + end ) / 2; + if ( value < lst[i] ) { + end = i; + } else { + st = i; + } + } + ++st; + return ( st == int( lst.count() ) ) ? -1 : st; +} + +template +Q_INLINE_TEMPLATES int findSorted( QValueList &lst, const T &value, int start ) +{ + // Do a binary search to find the item == value + int st = start - 1; + int end = lst.count(); + while ( end - st > 1 ) { + int i = ( st + end ) / 2; + if ( value < lst[i] ) { + end = i; + } else { + st = i; + } + } + return ( end > start && value == lst[st] ) ? st : -1; +} + +template +Q_INLINE_TEMPLATES int removeSorted( QValueList &lst, const T &value, int start ) +{ + int i = findSorted( lst, value, start ); + if ( i >= 0 ) { + lst.remove( lst.at( i ) ); + } + return i; +} namespace KCal { typedef QValueList DateTimeList; typedef QValueList DateList; typedef QValueList TimeList; /** This class represents a recurrence rule for a calendar incidence. */ class LIBKCAL_EXPORT RecurrenceRule { public: class Observer { public: virtual ~Observer() {} /** This method will be called on each change of the recurrence object */ virtual void recurrenceChanged( RecurrenceRule * ) = 0; }; typedef ListBase List; /** enum for describing the frequency how an event recurs, if at all. */ enum PeriodType { rNone = 0, rSecondly, rMinutely, rHourly, rDaily, rWeekly, rMonthly, rYearly }; /** structure for describing the n-th weekday of the month/year. */ class WDayPos { public: WDayPos( int ps = 0 , short dy = 0 ) : mDay(dy), mPos(ps) {} short day() const { return mDay; } int pos() const { return mPos; } void setDay( short dy ) { mDay = dy; } void setPos( int ps ) { mPos = ps; } bool operator==( const RecurrenceRule::WDayPos &pos2 ) const { return ( mDay == pos2.mDay ) && ( mPos == pos2.mPos ); } protected: short mDay; // Weekday, 1=monday, 7=sunday int mPos; // week of the day (-1 for last, 1 for first, 0 for all weeks) // Bounded by -366 and +366, 0 means all weeks in that period }; RecurrenceRule( /*Incidence *parent, int compatVersion = 0*/ ); RecurrenceRule(const RecurrenceRule&); ~RecurrenceRule(); bool operator==( const RecurrenceRule& ) const; bool operator!=( const RecurrenceRule& r ) const { return !operator==(r); } RecurrenceRule &operator=(const RecurrenceRule&); // Incidence *parent() const { return mParent; } /** Set if recurrence is read-only or can be changed. */ void setReadOnly(bool readOnly) { mIsReadOnly = readOnly; } /** Returns true if the recurrence is read-only, or false if it can be changed. */ bool isReadOnly() const { return mIsReadOnly; } /** Returns the event's recurrence status. See the enumeration at the top * of this file for possible values. */ bool doesRecur() const { return mPeriod!=rNone; } void setRecurrenceType( PeriodType period ); PeriodType recurrenceType() const { return mPeriod; } /** Turns off recurrence for the event. */ void clear(); /** Returns frequency of recurrence, in terms of the recurrence time period type. */ uint frequency() const { return mFrequency; } /** Sets the frequency of recurrence, in terms of the recurrence time period type. */ void setFrequency( int freq ); /** Return the start of the recurrence */ QDateTime startDt() const { return mDateStart; } /** Set start of recurrence, as a date and time. */ void setStartDt(const QDateTime &start); /** Returns whether the start date has no time associated. Floating means -- according to rfc2445 -- that the event has no time associate. */ bool doesFloat() const { return mFloating; } /** Sets whether the dtstart is a floating time (i.e. has no time attached) */ void setFloats( bool floats ); /** Returns the date and time of the last recurrence. * An invalid date is returned if the recurrence has no end. * @param result if non-null, *result is updated to true if successful, * or false if there is no recurrence. */ QDateTime endDt( bool* result = 0 ) const; /** Sets the date and time of the last recurrence. * @param endDateTime the ending date/time after which to stop recurring. */ void setEndDt(const QDateTime &endDateTime); /** * Returns -1 if the event recurs infinitely, 0 if the end date is set, * otherwise the total number of recurrences, including the initial occurrence. */ int duration() const { return mDuration; } /** Sets the total number of times the event is to occur, including both the * first and last. */ void setDuration(int duration); // /** Returns the number of recurrences up to and including the date specified. */ // int durationTo(const QDate &) const; /** Returns the number of recurrences up to and including the date/time specified. */ int durationTo(const QDateTime &) const; /** Returns the number of recurrences up to and including the date specified. */ int durationTo( const QDate &date ) const { return durationTo( QDateTime( date, QTime( 23, 59, 59 ) ) ); } /** Returns true if the date specified is one on which the event will * recur. The start date returns true only if it actually matches the rule. */ bool recursOn( const QDate &qd ) const; /** Returns true if the date/time specified is one at which the event will * recur. Times are rounded down to the nearest minute to determine the result. * The start date/time returns true only if it actually matches the rule. */ bool recursAt( const QDateTime & ) const; /** Returns true if the date matches the rules. It does not necessarily mean that this is an actual occurrence. In particular, the method does not check if the date is after the end date, or if the frequency interval matches */ bool dateMatchesRules( const QDateTime &qdt ) const; /** Returns a list of the times on the specified date at which the * recurrence will occur. * @param date the date for which to find the recurrence times. */ TimeList recurTimesOn( const QDate &date ) const; + /** Returns a list of all the times at which the recurrence will occur + * between two specified times. + * + * There is a (large) maximum limit to the number of times returned. If due to + * this limit the list is incomplete, this is indicated by the last entry being + * set to an invalid KDateTime value. If you need further values, call the + * method again with a start time set to just after the last valid time returned. + * @param start inclusive start of interval + * @param end inclusive end of interval + * @return list of date/time values + */ + DateTimeList timesInInterval( const QDateTime &start, const QDateTime &end ) const; /** Returns the date and time of the next recurrence, after the specified date/time. * If the recurrence has no time, the next date after the specified date is returned. * @param preDateTime the date/time after which to find the recurrence. * @return date/time of next recurrence, or invalid date if none. */ QDateTime getNextDate( const QDateTime& preDateTime ) const; /** Returns the date and time of the last previous recurrence, before the specified date/time. * If a time later than 00:00:00 is specified and the recurrence has no time, 00:00:00 on * the specified date is returned if that date recurs. * @param afterDateTime the date/time before which to find the recurrence. * @return date/time of previous recurrence, or invalid date if none. */ QDateTime getPreviousDate( const QDateTime& afterDateTime ) const; void setBySeconds( const QValueList bySeconds ); void setByMinutes( const QValueList byMinutes ); void setByHours( const QValueList byHours ); void setByDays( const QValueList byDays ); void setByMonthDays( const QValueList byMonthDays ); void setByYearDays( const QValueList byYearDays ); void setByWeekNumbers( const QValueList byWeekNumbers ); void setByMonths( const QValueList byMonths ); void setBySetPos( const QValueList bySetPos ); void setWeekStart( short weekStart ); const QValueList &bySeconds() const { return mBySeconds; } const QValueList &byMinutes() const { return mByMinutes; } const QValueList &byHours() const { return mByHours; } const QValueList &byDays() const { return mByDays; } const QValueList &byMonthDays() const { return mByMonthDays; } const QValueList &byYearDays() const { return mByYearDays; } const QValueList &byWeekNumbers() const { return mByWeekNumbers; } const QValueList &byMonths() const { return mByMonths; } const QValueList &bySetPos() const { return mBySetPos; } short weekStart() const { return mWeekStart; } void setDirty(); /** Installs an observer. Whenever some setting of this recurrence object is changed, the recurrenceUpdated( Recurrence* ) method of each observer will be called to inform it of changes. @param observer the Recurrence::Observer-derived object, which will be installed as an observer of this object. */ void addObserver( Observer *observer ); /** Removes an observer that was added with addObserver. If the given object was not an observer, it does nothing. @param observer the Recurrence::Observer-derived object to be removed from the list of observers of this object. */ void removeObserver( Observer *observer ); /** Debug output. */ void dump() const; QString mRRule; private: class Constraint { public: typedef QValueList List; Constraint( int wkst = 1 ); /* Constraint( const Constraint &con ) : year(con.year), month(con.month), day(con.day), hour(con.hour), minute(con.minute), second(con.second), weekday(con.weekday), weeknumber(con.weeknumber), yearday(con.yearday), weekstart(con.weekstart) {}*/ Constraint( const QDateTime &preDate, PeriodType type, int wkst ); void clear(); 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.) bool readDateTime( const QDateTime &preDate, PeriodType type ); bool matches( const QDate &dt, RecurrenceRule::PeriodType type ) const; bool matches( const QDateTime &dt, RecurrenceRule::PeriodType type ) const; bool isConsistent() const; bool isConsistent( PeriodType period ) const; bool increase( PeriodType type, int freq ); QDateTime intervalDateTime( PeriodType type ) const; DateTimeList dateTimes( PeriodType type ) const; void dump() const; }; Constraint getNextValidDateInterval( const QDateTime &preDate, PeriodType type ) const; Constraint getPreviousValidDateInterval( const QDateTime &preDate, PeriodType type ) const; DateTimeList datesForInterval( const Constraint &interval, PeriodType type ) const; bool mergeIntervalConstraint( Constraint *merged, const Constraint &conit, const Constraint &interval ) const; bool buildCache() const; PeriodType mPeriod; QDateTime mDateStart; /** how often it recurs (including dtstart): -1 means infinitely, 0 means an explicit end date, positive values give the number of occurrences */ int mDuration; QDateTime mDateEnd; uint mFrequency; bool mIsReadOnly; bool mFloating; QValueList mBySeconds; // values: second 0-59 QValueList mByMinutes; // values: minute 0-59 QValueList mByHours; // values: hour 0-23 QValueList mByDays; // n-th weekday of the month or year QValueList mByMonthDays; // values: day -31 to -1 and 1-31 QValueList mByYearDays; // values: day -366 to -1 and 1-366 QValueList mByWeekNumbers; // values: week -53 to -1 and 1-53 QValueList mByMonths; // values: month 1-12 QValueList mBySetPos; // values: position -366 to -1 and 1-366 short mWeekStart; // first day of the week (1=Monday, 7=Sunday) Constraint::List mConstraints; void buildConstraints(); bool mDirty; QValueList mObservers; // Cache for duration mutable DateTimeList mCachedDates; - mutable bool mCached; mutable QDateTime mCachedDateEnd; + mutable QDateTime mCachedLastDate; // when mCachedDateEnd invalid, last date checked + mutable bool mCached; + + bool mNoByRules; // no BySeconds, ByMinutes, ... rules exist + uint mTimedRepetition; // repeats at a regular number of seconds interval, or 0 class Private; Private *d; }; } #endif