diff --git a/kcal/recurrencerule.cpp b/kcal/recurrencerule.cpp index 9870bd467..0af5eca45 100644 --- a/kcal/recurrencerule.cpp +++ b/kcal/recurrencerule.cpp @@ -1,2148 +1,2155 @@ /* This file is part of libkcal. Copyright (c) 2005 Reinhold Kainhofer Copyright (c) 2006-2008 David Jarvie This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "recurrencerule.h" #include #include #include #include #include #include #include using namespace KCal; // Maximum number of intervals to process const int LOOP_LIMIT = 10000; static QString dumpTime( const KDateTime &dt ); // for debugging /*========================================================================= = = = IMPORTANT CODING NOTE: = = = = Recurrence handling code is time critical, especially for sub-daily = = recurrences. For example, if getNextDate() is called repeatedly to = = check all consecutive occurrences over a few years, on a slow machine = = this could take many seconds to complete in the worst case. Simple = = sub-daily recurrences are optimised by use of mTimedRepetition. = = = ==========================================================================*/ /************************************************************************** * DateHelper * **************************************************************************/ //@cond PRIVATE class DateHelper { public: #ifndef NDEBUG static QString dayName( short day ); #endif static QDate getNthWeek( int year, int weeknumber, short weekstart = 1 ); static int weekNumbersInYear( int year, short weekstart = 1 ); static int getWeekNumber( const QDate &date, short weekstart, int *year = 0 ); static int getWeekNumberNeg( const QDate &date, short weekstart, int *year = 0 ); // Convert to QDate, allowing for day < 0. // month and day must be non-zero. static QDate getDate( int year, int month, int day ) { if ( day >= 0 ) { return QDate( year, month, day ); } else { if ( ++month > 12 ) { month = 1; ++year; } return QDate( year, month, 1 ).addDays( day ); } } }; #ifndef NDEBUG // TODO: Move to a general library / class, as we need the same in the iCal // generator and in the xcal format QString DateHelper::dayName( short day ) { switch ( day ) { case 1: return "MO"; case 2: return "TU"; case 3: return "WE"; case 4: return "TH"; case 5: return "FR"; case 6: return "SA"; case 7: return "SU"; 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 ) { int y = date.year(); QDate dt( y, 1, 4 ); // <= definitely in week #1 dt = dt.addDays( -( 7 + dt.dayOfWeek() - weekstart ) % 7 ); // begin of week #1 int daysto = dt.daysTo( date ); if ( daysto < 0 ) { // in first week of year --y; dt = QDate( y, 1, 4 ); dt = dt.addDays( -( 7 + dt.dayOfWeek() - weekstart ) % 7 ); // begin of week #1 daysto = dt.daysTo( date ); } else if ( daysto > 355 ) { // near the end of the year - check if it's next year QDate dtn( y+1, 1, 4 ); // <= definitely first week of next year dtn = dtn.addDays( -( 7 + dtn.dayOfWeek() - weekstart ) % 7 ); int dayston = dtn.daysTo( date ); if ( dayston >= 0 ) { // in first week of next year; ++y; daysto = dayston; } } if ( year ) { *year = y; } return daysto / 7 + 1; } int DateHelper::weekNumbersInYear( int year, short weekstart ) { QDate dt( year, 1, weekstart ); QDate dt1( year + 1, 1, weekstart ); return dt.daysTo( dt1 ) / 7; } // Week number from the end of the year int DateHelper::getWeekNumberNeg( const QDate &date, short weekstart, int *year ) { int weekpos = getWeekNumber( date, weekstart, year ); return weekNumbersInYear( *year, weekstart ) - weekpos - 1; } //@endcond /************************************************************************** * Constraint * **************************************************************************/ //@cond PRIVATE class Constraint { public: typedef QList List; explicit Constraint( KDateTime::Spec, int wkst = 1 ); Constraint( const KDateTime &dt, RecurrenceRule::PeriodType type, int wkst ); void clear(); void setYear( int n ) { year = n; useCachedDt = false; } void setMonth( int n ) { month = n; useCachedDt = false; } void setDay( int n ) { day = n; useCachedDt = false; } void setHour( int n ) { hour = n; useCachedDt = false; } void setMinute( int n ) { minute = n; useCachedDt = false; } void setSecond( int n ) { second = n; useCachedDt = false; } void setWeekday( int n ) { weekday = n; useCachedDt = false; } void setWeekdaynr( int n ) { weekdaynr = n; useCachedDt = false; } void setWeeknumber( int n ) { weeknumber = n; useCachedDt = false; } void setYearday( int n ) { yearday = n; useCachedDt = false; } void setWeekstart( int n ) { weekstart = n; useCachedDt = false; } void setSecondOccurrence( int n ) { secondOccurrence = n; useCachedDt = false; } int year; // 0 means unspecified int month; // 0 means unspecified int day; // 0 means unspecified int hour; // -1 means unspecified int minute; // -1 means unspecified int second; // -1 means unspecified int weekday; // 0 means unspecified int weekdaynr; // index of weekday in month/year (0=unspecified) int weeknumber; // 0 means unspecified int yearday; // 0 means unspecified int weekstart; // first day of week (1=monday, 7=sunday, 0=unspec.) KDateTime::Spec timespec; // time zone etc. to use bool secondOccurrence; // the time is the second occurrence during daylight savings shift bool readDateTime( const KDateTime &dt, RecurrenceRule::PeriodType type ); bool matches( const QDate &dt, RecurrenceRule::PeriodType type ) const; bool matches( const KDateTime &dt, RecurrenceRule::PeriodType type ) const; bool merge( const Constraint &interval ); bool isConsistent() const; bool isConsistent( RecurrenceRule::PeriodType period ) const; bool increase( RecurrenceRule::PeriodType type, int freq ); KDateTime intervalDateTime( RecurrenceRule::PeriodType type ) const; QList dateTimes( RecurrenceRule::PeriodType type ) const; void appendDateTime( const QDate &date, const QTime &time, QList &list ) const; void dump() const; private: mutable bool useCachedDt; mutable KDateTime cachedDt; }; Constraint::Constraint( KDateTime::Spec spec, int wkst ) : weekstart( wkst ), timespec( spec ) { clear(); } Constraint::Constraint( const KDateTime &dt, RecurrenceRule::PeriodType type, int wkst ) : weekstart( wkst ), timespec( dt.timeSpec() ) { clear(); readDateTime( dt, type ); } void Constraint::clear() { year = 0; month = 0; day = 0; hour = -1; minute = -1; second = -1; weekday = 0; weekdaynr = 0; weeknumber = 0; yearday = 0; secondOccurrence = false; useCachedDt = false; } bool Constraint::matches( const QDate &dt, RecurrenceRule::PeriodType type ) const { // If the event recurs in week 53 or 1, the day might not belong to the same // year as the week it is in. E.g. Jan 1, 2005 is in week 53 of year 2004. // So we can't simply check the year in that case! if ( weeknumber == 0 ) { if ( year > 0 && year != dt.year() ) { return false; } } else { int y; if ( weeknumber > 0 && weeknumber != DateHelper::getWeekNumber( dt, weekstart, &y ) ) { return false; } if ( weeknumber < 0 && weeknumber != DateHelper::getWeekNumberNeg( dt, weekstart, &y ) ) { return false; } if ( year > 0 && year != y ) { return false; } } if ( month > 0 && month != dt.month() ) { return false; } if ( day > 0 && day != dt.day() ) { return false; } if ( day < 0 && dt.day() != ( dt.daysInMonth() + day + 1 ) ) { return false; } if ( weekday > 0 ) { if ( weekday != dt.dayOfWeek() ) { return false; } if ( weekdaynr != 0 ) { // If it's a yearly recurrence and a month is given, the position is // still in the month, not in the year. if ( ( type == RecurrenceRule::rMonthly ) || ( type == RecurrenceRule::rYearly && month > 0 ) ) { // Monthly if ( weekdaynr > 0 && weekdaynr != ( dt.day() - 1 ) / 7 + 1 ) { return false; } if ( weekdaynr < 0 && weekdaynr != -( ( dt.daysInMonth() - dt.day() ) / 7 + 1 ) ) { return false; } } else { // Yearly if ( weekdaynr > 0 && weekdaynr != ( dt.dayOfYear() - 1 ) / 7 + 1 ) { return false; } if ( weekdaynr < 0 && weekdaynr != -( ( dt.daysInYear() - dt.dayOfYear() ) / 7 + 1 ) ) { return false; } } } } if ( yearday > 0 && yearday != dt.dayOfYear() ) { return false; } if ( yearday < 0 && yearday != dt.daysInYear() - dt.dayOfYear() + 1 ) { return false; } return true; } /* Check for a match with the specified date/time. * The date/time's time specification must correspond with that of the start date/time. */ bool Constraint::matches( const KDateTime &dt, RecurrenceRule::PeriodType type ) const { if ( ( hour >= 0 && ( hour != dt.time().hour() || secondOccurrence != dt.isSecondOccurrence() ) ) || ( minute >= 0 && minute != dt.time().minute() ) || ( second >= 0 && second != dt.time().second() ) || !matches( dt.date(), type ) ) { return false; } return true; } bool Constraint::isConsistent( RecurrenceRule::PeriodType /*period*/) const { // TODO: Check for consistency, e.g. byyearday=3 and bymonth=10 return true; } // Return a date/time set to the constraint values, but with those parts less // significant than the given period type set to 1 (for dates) or 0 (for times). KDateTime Constraint::intervalDateTime( RecurrenceRule::PeriodType type ) const { if ( useCachedDt ) { return cachedDt; } QDate d; QTime t( 0, 0, 0 ); bool subdaily = true; switch ( type ) { case RecurrenceRule::rSecondly: t.setHMS( hour, minute, second ); break; case RecurrenceRule::rMinutely: t.setHMS( hour, minute, 0 ); break; case RecurrenceRule::rHourly: t.setHMS( hour, 0, 0 ); break; case RecurrenceRule::rDaily: break; case RecurrenceRule::rWeekly: d = DateHelper::getNthWeek( year, weeknumber, weekstart ); subdaily = false; break; case RecurrenceRule::rMonthly: d.setYMD( year, month, 1 ); subdaily = false; break; case RecurrenceRule::rYearly: d.setYMD( year, 1, 1 ); subdaily = false; break; default: break; } if ( subdaily ) { d = DateHelper::getDate( year, (month>0)?month:1, day?day:1 ); } cachedDt = KDateTime( d, t, timespec ); if ( secondOccurrence ) { cachedDt.setSecondOccurrence( true ); } useCachedDt = true; return cachedDt; } bool Constraint::merge( const Constraint &interval ) { #define mergeConstraint( name, cmparison ) \ if ( interval.name cmparison ) { \ if ( !( name cmparison ) ) { \ name = interval.name; \ } else if ( name != interval.name ) { \ return false;\ } \ } useCachedDt = false; mergeConstraint( year, > 0 ); mergeConstraint( month, > 0 ); mergeConstraint( day, != 0 ); mergeConstraint( hour, >= 0 ); mergeConstraint( minute, >= 0 ); mergeConstraint( second, >= 0 ); mergeConstraint( weekday, != 0 ); mergeConstraint( weekdaynr, != 0 ); mergeConstraint( weeknumber, != 0 ); mergeConstraint( yearday, != 0 ); #undef mergeConstraint return true; } // Y M D | H Mn S | WD #WD | WN | YD // required: // x | x x x | | | // 0) Trivial: Exact date given, maybe other restrictions // x x x | x x x | | | // 1) Easy case: no weekly restrictions -> at most a loop through possible dates // x + + | x x x | - - | - | - // 2) Year day is given -> date known // x | x x x | | | + // 3) week number is given -> loop through all days of that week. Further // restrictions will be applied in the end, when we check all dates for // consistency with the constraints // x | x x x | | + | (-) // 4) week day is specified -> // x | x x x | x ? | (-)| (-) // 5) All possiblecases have already been treated, so this must be an error! QList Constraint::dateTimes( RecurrenceRule::PeriodType type ) const { QList result; bool done = false; if ( !isConsistent( type ) ) { return result; } // TODO_Recurrence: Handle all-day QTime tm( hour, minute, second ); if ( !done && day && month > 0 ) { appendDateTime( DateHelper::getDate( year, month, day ), tm, result ); done = true; } if ( !done && weekday == 0 && weeknumber == 0 && yearday == 0 ) { // Easy case: date is given, not restrictions by week or yearday uint mstart = ( month > 0 ) ? month : 1; uint mend = ( month <= 0 ) ? 12 : month; for ( uint m = mstart; m <= mend; ++m ) { uint dstart, dend; if ( day > 0 ) { dstart = dend = day; } else if ( day < 0 ) { QDate date( year, month, 1 ); dstart = dend = date.daysInMonth() + day + 1; } else { QDate date( year, month, 1 ); dstart = 1; dend = date.daysInMonth(); } uint d = dstart; for ( QDate dt( year, m, dstart ); ; dt = dt.addDays( 1 ) ) { appendDateTime( dt, tm, result ); if ( ++d > dend ) { break; } } } done = true; } // Else: At least one of the week / yearday restrictions was given... // If we have a yearday (and of course a year), we know the exact date if ( !done && yearday != 0 ) { // yearday < 0 means from end of year, so we'll need Jan 1 of the next year QDate d( year + ( ( yearday > 0 ) ? 0 : 1 ), 1, 1 ); d = d.addDays( yearday - ( ( yearday > 0 ) ? 1 : 0 ) ); appendDateTime( d, tm, result ); done = true; } // Else: If we have a weeknumber, we have at most 7 possible dates, loop through them if ( !done && weeknumber != 0 ) { QDate wst( DateHelper::getNthWeek( year, weeknumber, weekstart ) ); if ( weekday != 0 ) { wst = wst.addDays( ( 7 + weekday - weekstart ) % 7 ); appendDateTime( wst, tm, result ); } else { for ( int i = 0; i < 7; ++i ) { appendDateTime( wst, tm, result ); wst = wst.addDays( 1 ); } } done = true; } // weekday is given if ( !done && weekday != 0 ) { QDate dt( year, 1, 1 ); // If type == yearly and month is given, pos is still in month not year! // TODO_Recurrence: Correct handling of n-th BYDAY... int maxloop = 53; bool inMonth = ( type == RecurrenceRule::rMonthly ) || ( type == RecurrenceRule::rYearly && month > 0 ); if ( inMonth && month > 0 ) { dt = QDate( year, month, 1 ); maxloop = 5; } if ( weekdaynr < 0 ) { // From end of period (month, year) => relative to begin of next period if ( inMonth ) { dt = dt.addMonths( 1 ); } else { dt = dt.addYears( 1 ); } } int adj = ( 7 + weekday - dt.dayOfWeek() ) % 7; dt = dt.addDays( adj ); // correct first weekday of the period if ( weekdaynr > 0 ) { dt = dt.addDays( ( weekdaynr - 1 ) * 7 ); appendDateTime( dt, tm, result ); } else if ( weekdaynr < 0 ) { dt = dt.addDays( weekdaynr * 7 ); appendDateTime( dt, tm, result ); } else { // loop through all possible weeks, non-matching will be filtered later for ( int i = 0; i < maxloop; ++i ) { appendDateTime( dt, tm, result ); dt = dt.addDays( 7 ); } } } // weekday != 0 // Only use those times that really match all other constraints, too QList valid; for ( int i = 0, iend = result.count(); i < iend; ++i ) { if ( matches( result[i], type ) ) { valid.append( result[i] ); } } // Don't sort it here, would be unnecessary work. The results from all // constraints will be merged to one big list of the interval. Sort that one! return valid; } void Constraint::appendDateTime( const QDate &date, const QTime &time, QList &list ) const { KDateTime dt( date, time, timespec ); if ( dt.isValid() ) { if ( secondOccurrence ) { dt.setSecondOccurrence( true ); } list.append( dt ); } } bool Constraint::increase( RecurrenceRule::PeriodType type, int freq ) { // convert the first day of the interval to KDateTime intervalDateTime( type ); // Now add the intervals switch ( type ) { case RecurrenceRule::rSecondly: cachedDt = cachedDt.addSecs( freq ); break; case RecurrenceRule::rMinutely: cachedDt = cachedDt.addSecs( 60 * freq ); break; case RecurrenceRule::rHourly: cachedDt = cachedDt.addSecs( 3600 * freq ); break; case RecurrenceRule::rDaily: cachedDt = cachedDt.addDays( freq ); break; case RecurrenceRule::rWeekly: cachedDt = cachedDt.addDays( 7 * freq ); break; case RecurrenceRule::rMonthly: cachedDt = cachedDt.addMonths( freq ); break; case RecurrenceRule::rYearly: cachedDt = cachedDt.addYears( freq ); break; default: break; } // Convert back from KDateTime to the Constraint class readDateTime( cachedDt, type ); useCachedDt = true; // readDateTime() resets this return true; } // Set the constraint's value appropriate to 'type', to the value contained in a date/time. bool Constraint::readDateTime( const KDateTime &dt, RecurrenceRule::PeriodType type ) { switch ( type ) { // Really fall through! Only weekly needs to be treated differently! case RecurrenceRule::rSecondly: second = dt.time().second(); case RecurrenceRule::rMinutely: minute = dt.time().minute(); case RecurrenceRule::rHourly: hour = dt.time().hour(); secondOccurrence = dt.isSecondOccurrence(); case RecurrenceRule::rDaily: day = dt.date().day(); case RecurrenceRule::rMonthly: month = dt.date().month(); case RecurrenceRule::rYearly: year = dt.date().year(); break; case RecurrenceRule::rWeekly: // Determine start day of the current week, calculate the week number from that weeknumber = DateHelper::getWeekNumber( dt.date(), weekstart, &year ); break; default: break; } useCachedDt = false; return true; } //@endcond /************************************************************************** * RecurrenceRule::Private * **************************************************************************/ //@cond PRIVATE class KCal::RecurrenceRule::Private { public: Private( RecurrenceRule *parent ) : mParent( parent ), mPeriod( rNone ), mFrequency( 0 ), mWeekStart( 1 ), mIsReadOnly( false ), mAllDay( false ) {} Private( RecurrenceRule *parent, const Private &p ) : mParent( parent ), mRRule( p.mRRule ), mPeriod( p.mPeriod ), mDateStart( p.mDateStart ), mFrequency( p.mFrequency ), mDuration( p.mDuration ), mDateEnd( p.mDateEnd ), mBySeconds( p.mBySeconds ), mByMinutes( p.mByMinutes ), mByHours( p.mByHours ), mByDays( p.mByDays ), mByMonthDays( p.mByMonthDays ), mByYearDays( p.mByYearDays ), mByWeekNumbers( p.mByWeekNumbers ), mByMonths( p.mByMonths ), mBySetPos( p.mBySetPos ), mWeekStart( p.mWeekStart ), mIsReadOnly( p.mIsReadOnly ), mAllDay( p.mAllDay ) { setDirty(); } bool operator==( const Private &other ) const; void clear(); void setDirty(); void buildConstraints(); bool buildCache() const; Constraint getNextValidDateInterval( const KDateTime &preDate, PeriodType type ) const; Constraint getPreviousValidDateInterval( const KDateTime &afterDate, PeriodType type ) const; DateTimeList datesForInterval( const Constraint &interval, PeriodType type ) const; RecurrenceRule *mParent; QString mRRule; // RRULE string PeriodType mPeriod; KDateTime mDateStart; // start of recurrence (but mDateStart is not an occurrence // unless it matches the rule) uint mFrequency; /** how often it recurs: - -1 means infinitely, 0 means an explicit end date, positive values give the number of occurrences */ int mDuration; KDateTime mDateEnd; QList mBySeconds; // values: second 0-59 QList mByMinutes; // values: minute 0-59 QList mByHours; // values: hour 0-23 QList mByDays; // n-th weekday of the month or year QList mByMonthDays; // values: day -31 to -1 and 1-31 QList mByYearDays; // values: day -366 to -1 and 1-366 QList mByWeekNumbers; // values: week -53 to -1 and 1-53 QList mByMonths; // values: month 1-12 QList mBySetPos; // values: position -366 to -1 and 1-366 short mWeekStart; // first day of the week (1=Monday, 7=Sunday) Constraint::List mConstraints; QList mObservers; // Cache for duration mutable DateTimeList mCachedDates; mutable KDateTime mCachedDateEnd; mutable KDateTime mCachedLastDate; // when mCachedDateEnd invalid, last date checked mutable bool mCached; bool mIsReadOnly; bool mAllDay; bool mNoByRules; // no BySeconds, ByMinutes, ... rules exist uint mTimedRepetition; // repeats at a regular number of seconds interval, or 0 }; bool RecurrenceRule::Private::operator==( const Private &r ) const { return mPeriod == r.mPeriod && mDateStart == r.mDateStart && mDuration == r.mDuration && mDateEnd == r.mDateEnd && mFrequency == r.mFrequency && mIsReadOnly == r.mIsReadOnly && mAllDay == r.mAllDay && mBySeconds == r.mBySeconds && mByMinutes == r.mByMinutes && mByHours == r.mByHours && mByDays == r.mByDays && mByMonthDays == r.mByMonthDays && mByYearDays == r.mByYearDays && mByWeekNumbers == r.mByWeekNumbers && mByMonths == r.mByMonths && mBySetPos == r.mBySetPos && mWeekStart == r.mWeekStart; } void RecurrenceRule::Private::clear() { if ( mIsReadOnly ) { return; } mPeriod = rNone; mBySeconds.clear(); mByMinutes.clear(); mByHours.clear(); mByDays.clear(); mByMonthDays.clear(); mByYearDays.clear(); mByWeekNumbers.clear(); mByMonths.clear(); mBySetPos.clear(); mWeekStart = 1; setDirty(); } void RecurrenceRule::Private::setDirty() { buildConstraints(); mCached = false; mCachedDates.clear(); for ( int i = 0, iend = mObservers.count(); i < iend; ++i ) { if ( mObservers[i] ) { mObservers[i]->recurrenceChanged( mParent ); } } } //@endcond /************************************************************************** * RecurrenceRule * **************************************************************************/ RecurrenceRule::RecurrenceRule() : d( new Private( this ) ) { } RecurrenceRule::RecurrenceRule( const RecurrenceRule &r ) : d( new Private( this, *r.d ) ) { } RecurrenceRule::~RecurrenceRule() { delete d; } bool RecurrenceRule::operator==( const RecurrenceRule &r ) const { return *d == *r.d; } void RecurrenceRule::addObserver( RuleObserver *observer ) { if ( !d->mObservers.contains( observer ) ) { d->mObservers.append( observer ); } } void RecurrenceRule::removeObserver( RuleObserver *observer ) { if ( d->mObservers.contains( observer ) ) { d->mObservers.removeAll( observer ); } } void RecurrenceRule::setRecurrenceType( PeriodType period ) { if ( isReadOnly() ) { return; } d->mPeriod = period; d->setDirty(); } KDateTime RecurrenceRule::endDt( bool *result ) const { if ( result ) { *result = false; } if ( d->mPeriod == rNone ) { return KDateTime(); } if ( d->mDuration < 0 ) { return KDateTime(); } if ( d->mDuration == 0 ) { if ( result ) { *result = true; } return d->mDateEnd; } // N occurrences. Check if we have a full cache. If so, return the cached end date. if ( !d->mCached ) { // If not enough occurrences can be found (i.e. inconsistent constraints) if ( !d->buildCache() ) { return KDateTime(); } } if ( result ) { *result = true; } return d->mCachedDateEnd; } void RecurrenceRule::setEndDt( const KDateTime &dateTime ) { if ( isReadOnly() ) { return; } d->mDateEnd = dateTime; d->mDuration = 0; // set to 0 because there is an end date/time d->setDirty(); } void RecurrenceRule::setDuration( int duration ) { if ( isReadOnly() ) { return; } d->mDuration = duration; d->setDirty(); } void RecurrenceRule::setAllDay( bool allDay ) { if ( isReadOnly() ) { return; } d->mAllDay = allDay; d->setDirty(); } void RecurrenceRule::clear() { d->clear(); } void RecurrenceRule::setDirty() { d->setDirty(); } void RecurrenceRule::setStartDt( const KDateTime &start ) { if ( isReadOnly() ) { return; } d->mDateStart = start; d->setDirty(); } void RecurrenceRule::setFrequency( int freq ) { if ( isReadOnly() || freq <= 0 ) { return; } d->mFrequency = freq; d->setDirty(); } void RecurrenceRule::setBySeconds( const QList bySeconds ) { if ( isReadOnly() ) { return; } d->mBySeconds = bySeconds; d->setDirty(); } void RecurrenceRule::setByMinutes( const QList byMinutes ) { if ( isReadOnly() ) { return; } d->mByMinutes = byMinutes; d->setDirty(); } void RecurrenceRule::setByHours( const QList byHours ) { if ( isReadOnly() ) { return; } d->mByHours = byHours; d->setDirty(); } void RecurrenceRule::setByDays( const QList byDays ) { if ( isReadOnly() ) { return; } d->mByDays = byDays; d->setDirty(); } void RecurrenceRule::setByMonthDays( const QList byMonthDays ) { if ( isReadOnly() ) { return; } d->mByMonthDays = byMonthDays; d->setDirty(); } void RecurrenceRule::setByYearDays( const QList byYearDays ) { if ( isReadOnly() ) { return; } d->mByYearDays = byYearDays; d->setDirty(); } void RecurrenceRule::setByWeekNumbers( const QList byWeekNumbers ) { if ( isReadOnly() ) { return; } d->mByWeekNumbers = byWeekNumbers; d->setDirty(); } void RecurrenceRule::setByMonths( const QList byMonths ) { if ( isReadOnly() ) { return; } d->mByMonths = byMonths; d->setDirty(); } void RecurrenceRule::setBySetPos( const QList bySetPos ) { if ( isReadOnly() ) { return; } d->mBySetPos = bySetPos; d->setDirty(); } void RecurrenceRule::setWeekStart( short weekStart ) { if ( isReadOnly() ) { return; } d->mWeekStart = weekStart; d->setDirty(); } void RecurrenceRule::shiftTimes( const KDateTime::Spec &oldSpec, const KDateTime::Spec &newSpec ) { d->mDateStart = d->mDateStart.toTimeSpec( oldSpec ); d->mDateStart.setTimeSpec( newSpec ); if ( d->mDuration == 0 ) { d->mDateEnd = d->mDateEnd.toTimeSpec( oldSpec ); d->mDateEnd.setTimeSpec( newSpec ); } d->setDirty(); } // Taken from recurrence.cpp // int RecurrenceRule::maxIterations() const // { // /* Find the maximum number of iterations which may be needed to reach the // * next actual occurrence of a monthly or yearly recurrence. // * More than one iteration may be needed if, for example, it's the 29th February, // * the 31st day of the month or the 5th Monday, and the month being checked is // * February or a 30-day month. // * The following recurrences may never occur: // * - For rMonthlyDay: if the frequency is a whole number of years. // * - For rMonthlyPos: if the frequency is an even whole number of years. // * - For rYearlyDay, rYearlyMonth: if the frequeny is a multiple of 4 years. // * - For rYearlyPos: if the frequency is an even number of years. // * The maximum number of iterations needed, assuming that it does actually occur, // * was found empirically. // */ // switch (recurs) { // case rMonthlyDay: // return (rFreq % 12) ? 6 : 8; // // case rMonthlyPos: // if (rFreq % 12 == 0) { // // Some of these frequencies may never occur // return (rFreq % 84 == 0) ? 364 // frequency = multiple of 7 years // : (rFreq % 48 == 0) ? 7 // frequency = multiple of 4 years // : (rFreq % 24 == 0) ? 14 : 28; // frequency = multiple of 2 or 1 year // } // // All other frequencies will occur sometime // if (rFreq > 120) // return 364; // frequencies of > 10 years will hit the date limit first // switch (rFreq) { // case 23: return 50; // case 46: return 38; // case 56: return 138; // case 66: return 36; // case 89: return 54; // case 112: return 253; // default: return 25; // most frequencies will need < 25 iterations // } // // case rYearlyMonth: // case rYearlyDay: // return 8; // only 29th Feb or day 366 will need more than one iteration // // case rYearlyPos: // if (rFreq % 7 == 0) // return 364; // frequencies of a multiple of 7 years will hit the date limit first // if (rFreq % 2 == 0) { // // Some of these frequencies may never occur // return (rFreq % 4 == 0) ? 7 : 14; // frequency = even number of years // } // return 28; // } // return 1; // } //@cond PRIVATE void RecurrenceRule::Private::buildConstraints() { mTimedRepetition = 0; mNoByRules = mBySetPos.isEmpty(); mConstraints.clear(); Constraint con( mDateStart.timeSpec() ); if ( mWeekStart > 0 ) { con.setWeekstart( mWeekStart ); } mConstraints.append( con ); int c, cend; int i, iend; Constraint::List tmp; #define intConstraint( list, setElement ) \ if ( !list.isEmpty() ) { \ mNoByRules = false; \ iend = list.count(); \ if ( iend == 1 ) { \ for ( c = 0, cend = mConstraints.count(); c < cend; ++c ) { \ mConstraints[c].setElement( list[0] ); \ } \ } else { \ for ( c = 0, cend = mConstraints.count(); c < cend; ++c ) { \ for ( i = 0; i < iend; ++i ) { \ con = mConstraints[c]; \ con.setElement( list[i] ); \ tmp.append( con ); \ } \ } \ mConstraints = tmp; \ tmp.clear(); \ } \ } intConstraint( mBySeconds, setSecond ); intConstraint( mByMinutes, setMinute ); intConstraint( mByHours, setHour ); intConstraint( mByMonthDays, setDay ); intConstraint( mByMonths, setMonth ); intConstraint( mByYearDays, setYearday ); intConstraint( mByWeekNumbers, setWeeknumber ); #undef intConstraint if ( !mByDays.isEmpty() ) { mNoByRules = false; for ( c = 0, cend = mConstraints.count(); c < cend; ++c ) { for ( i = 0, iend = mByDays.count(); i < iend; ++i ) { con = mConstraints[c]; con.setWeekday( mByDays[i].day() ); con.setWeekdaynr( mByDays[i].pos() ); tmp.append( con ); } } mConstraints = tmp; tmp.clear(); } #define fixConstraint( setElement, value ) \ { \ for ( c = 0, cend = mConstraints.count(); c < cend; ++c ) { \ mConstraints[c].setElement( value ); \ } \ } // Now determine missing values from DTSTART. This can speed up things, // because we have more restrictions and save some loops. // TODO: Does RFC 2445 intend to restrict the weekday in all cases of weekly? if ( mPeriod == rWeekly && mByDays.isEmpty() ) { fixConstraint( setWeekday, mDateStart.date().dayOfWeek() ); } // Really fall through in the cases, because all smaller time intervals are // constrained from dtstart switch ( mPeriod ) { case rYearly: if ( mByDays.isEmpty() && mByWeekNumbers.isEmpty() && mByYearDays.isEmpty() && mByMonths.isEmpty() ) { fixConstraint( setMonth, mDateStart.date().month() ); } case rMonthly: if ( mByDays.isEmpty() && mByWeekNumbers.isEmpty() && mByYearDays.isEmpty() && mByMonthDays.isEmpty() ) { fixConstraint( setDay, mDateStart.date().day() ); } case rWeekly: case rDaily: if ( mByHours.isEmpty() ) { fixConstraint( setHour, mDateStart.time().hour() ); } case rHourly: if ( mByMinutes.isEmpty() ) { fixConstraint( setMinute, mDateStart.time().minute() ); } case rMinutely: if ( mBySeconds.isEmpty() ) { fixConstraint( setSecond, mDateStart.time().second() ); } case rSecondly: default: break; } #undef fixConstraint if ( mNoByRules ) { switch ( mPeriod ) { case rHourly: mTimedRepetition = mFrequency * 3600; break; case rMinutely: mTimedRepetition = mFrequency * 60; break; case rSecondly: mTimedRepetition = mFrequency; break; default: break; } } else { for ( c = 0, cend = mConstraints.count(); c < cend; ) { if ( mConstraints[c].isConsistent( mPeriod ) ) { ++c; } else { mConstraints.removeAt( c ); --cend; } } } } // Build and cache a list of all occurrences. // Only call buildCache() if mDuration > 0. bool RecurrenceRule::Private::buildCache() const { // Build the list of all occurrences of this event (we need that to determine // the end date!) Constraint interval( getNextValidDateInterval( mDateStart, mPeriod ) ); QDateTime next; DateTimeList dts = datesForInterval( interval, mPeriod ); // Only use dates after the event has started (start date is only included // if it matches) int i = dts.findLT( mDateStart ); if ( i >= 0 ) { dts.erase( dts.begin(), dts.begin() + i + 1 ); } 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, bail out ) while ( loopnr < LOOP_LIMIT && dtnr < mDuration ) { interval.increase( mPeriod, mFrequency ); // The returned date list is already sorted! dts += datesForInterval( interval, mPeriod ); dtnr = dts.count(); ++loopnr; } if ( dts.count() > mDuration ) { // we have picked up more occurrences than necessary, remove them dts.erase( dts.begin() + mDuration, dts.end() ); } mCached = true; mCachedDates = dts; // it = dts.begin(); // while ( it != dts.end() ) { // kDebug() << " -=>" << dumpTime(*it); // ++it; // } if ( int( dts.count() ) == mDuration ) { mCachedDateEnd = dts.last(); return true; } else { // The cached date list is incomplete mCachedDateEnd = KDateTime(); mCachedLastDate = interval.intervalDateTime( mPeriod ); return false; } } //@endcond bool RecurrenceRule::dateMatchesRules( const KDateTime &kdt ) const { KDateTime dt = kdt.toTimeSpec( d->mDateStart.timeSpec() ); for ( int i = 0, iend = d->mConstraints.count(); i < iend; ++i ) { if ( d->mConstraints[i].matches( dt, recurrenceType() ) ) { return true; } } return false; } bool RecurrenceRule::recursOn( const QDate &qd, const KDateTime::Spec &timeSpec ) const { int i, iend; if ( allDay() ) { // It's a date-only rule, so it has no time specification. // Therefore ignore 'timeSpec'. if ( qd < d->mDateStart.date() ) { return false; } // Start date is only included if it really matches QDate endDate; if ( d->mDuration >= 0 ) { endDate = endDt().date(); if ( qd > endDate ) { return false; } } // The date must be in an appropriate interval (getNextValidDateInterval), // Plus it must match at least one of the constraints bool match = false; for ( i = 0, iend = d->mConstraints.count(); i < iend && !match; ++i ) { match = d->mConstraints[i].matches( qd, recurrenceType() ); } if ( !match ) { return false; } KDateTime start( qd, QTime( 0, 0, 0 ), d->mDateStart.timeSpec() ); Constraint interval( d->getNextValidDateInterval( start, recurrenceType() ) ); // Constraint::matches is quite efficient, so first check if it can occur at // all before we calculate all actual dates. if ( !interval.matches( qd, recurrenceType() ) ) { return false; } // We really need to obtain the list of dates in this interval, since // otherwise BYSETPOS will not work (i.e. the date will match the interval, // but BYSETPOS selects only one of these matching dates! KDateTime end = start.addDays(1); do { DateTimeList dts = d->datesForInterval( interval, recurrenceType() ); for ( i = 0, iend = dts.count(); i < iend; ++i ) { if ( dts[i].date() >= qd ) { return dts[i].date() == qd; } } interval.increase( recurrenceType(), frequency() ); } while ( interval.intervalDateTime( recurrenceType() ) < end ); return false; } // It's a date-time rule, so we need to take the time specification into account. KDateTime start( qd, QTime( 0, 0, 0 ), timeSpec ); KDateTime end = start.addDays( 1 ).toTimeSpec( d->mDateStart.timeSpec() ); start = start.toTimeSpec( d->mDateStart.timeSpec() ); if ( end < d->mDateStart ) { return false; } if ( start < d->mDateStart ) { start = d->mDateStart; } // Start date is only included if it really matches if ( d->mDuration >= 0 ) { KDateTime endRecur = endDt(); if ( endRecur.isValid() ) { if ( start > endRecur ) { return false; } if ( end > endRecur ) { end = endRecur; // limit end-of-day time to end of recurrence rule } } } if ( d->mTimedRepetition ) { // It's a simple sub-daily recurrence with no constraints int n = static_cast( ( d->mDateStart.secsTo_long( start ) - 1 ) % d->mTimedRepetition ); return start.addSecs( d->mTimedRepetition - n ) < end; } // Find the start and end dates in the time spec for the rule QDate startDay = start.date(); QDate endDay = end.addSecs( -1 ).date(); int dayCount = startDay.daysTo( endDay ) + 1; // The date must be in an appropriate interval (getNextValidDateInterval), // Plus it must match at least one of the constraints bool match = false; for ( i = 0, iend = d->mConstraints.count(); i < iend && !match; ++i ) { match = d->mConstraints[i].matches( startDay, recurrenceType() ); for ( int day = 1; day < dayCount && !match; ++day ) { match = d->mConstraints[i].matches( startDay.addDays( day ), recurrenceType() ); } } if ( !match ) { return false; } Constraint interval( d->getNextValidDateInterval( start, recurrenceType() ) ); // Constraint::matches is quite efficient, so first check if it can occur at // all before we calculate all actual dates. match = false; Constraint intervalm = interval; do { match = intervalm.matches( startDay, recurrenceType() ); for ( int day = 1; day < dayCount && !match; ++day ) { match = intervalm.matches( startDay.addDays( day ), recurrenceType() ); } if ( match ) { break; } intervalm.increase( recurrenceType(), frequency() ); } while ( intervalm.intervalDateTime( recurrenceType() ) < end ); if ( !match ) { return false; } // We really need to obtain the list of dates in this interval, since // otherwise BYSETPOS will not work (i.e. the date will match the interval, // but BYSETPOS selects only one of these matching dates! do { DateTimeList dts = d->datesForInterval( interval, recurrenceType() ); int i = dts.findGE( start ); if ( i >= 0 ) { return dts[i] <= end; } interval.increase( recurrenceType(), frequency() ); } while ( interval.intervalDateTime( recurrenceType() ) < end ); return false; } bool RecurrenceRule::recursAt( const KDateTime &kdt ) const { // Convert to the time spec used by this recurrence rule KDateTime dt( kdt.toTimeSpec( d->mDateStart.timeSpec() ) ); if ( allDay() ) { return recursOn( dt.date(), dt.timeSpec() ); } if ( dt < d->mDateStart ) { return false; } // Start date is only included if it really matches if ( d->mDuration >= 0 && dt > endDt() ) { return false; } if ( d->mTimedRepetition ) { // It's a simple sub-daily recurrence with no constraints return !( d->mDateStart.secsTo_long( dt ) % d->mTimedRepetition ); } // The date must be in an appropriate interval (getNextValidDateInterval), // Plus it must match at least one of the constraints if ( !dateMatchesRules( dt ) ) { return false; } // if it recurs every interval, speed things up... // if ( d->mFrequency == 1 && d->mBySetPos.isEmpty() && d->mByDays.isEmpty() ) return true; Constraint interval( d->getNextValidDateInterval( dt, recurrenceType() ) ); // TODO_Recurrence: Does this work with BySetPos??? if ( interval.matches( dt, recurrenceType() ) ) { return true; } return false; } TimeList RecurrenceRule::recurTimesOn( const QDate &date, const KDateTime::Spec &timeSpec ) const { TimeList lst; if ( allDay() ) { return lst; } KDateTime start( date, QTime( 0, 0, 0 ), timeSpec ); KDateTime end = start.addDays( 1 ).addSecs( -1 ); DateTimeList dts = timesInInterval( start, end ); // returns between start and end inclusive for ( int i = 0, iend = dts.count(); i < iend; ++i ) { lst += dts[i].toTimeSpec( timeSpec ).time(); } return lst; } /** Returns the number of recurrences up to and including the date/time specified. */ int RecurrenceRule::durationTo( const KDateTime &dt ) const { // Convert to the time spec used by this recurrence rule KDateTime toDate( dt.toTimeSpec( d->mDateStart.timeSpec() ) ); // Easy cases: // either before start, or after all recurrences and we know their number if ( toDate < d->mDateStart ) { return 0; } // Start date is only included if it really matches if ( d->mDuration > 0 && toDate >= endDt() ) { return d->mDuration; } if ( d->mTimedRepetition ) { // It's a simple sub-daily recurrence with no constraints return static_cast( d->mDateStart.secsTo_long( toDate ) / d->mTimedRepetition ); } return timesInInterval( d->mDateStart, toDate ).count(); } int RecurrenceRule::durationTo( const QDate &date ) const { return durationTo( KDateTime( date, QTime( 23, 59, 59 ), d->mDateStart.timeSpec() ) ); } KDateTime RecurrenceRule::getPreviousDate( const KDateTime &afterDate ) const { // Convert to the time spec used by this recurrence rule KDateTime toDate( afterDate.toTimeSpec( d->mDateStart.timeSpec() ) ); // Invalid starting point, or beyond end of recurrence if ( !toDate.isValid() || toDate < d->mDateStart ) { return KDateTime(); } if ( d->mTimedRepetition ) { // It's a simple sub-daily recurrence with no constraints KDateTime prev = toDate; if ( d->mDuration >= 0 && endDt().isValid() && toDate > endDt() ) { prev = endDt().addSecs( 1 ).toTimeSpec( d->mDateStart.timeSpec() ); } int n = static_cast( ( d->mDateStart.secsTo_long( prev ) - 1 ) % d->mTimedRepetition ); if ( n < 0 ) { return KDateTime(); // before recurrence start } prev = prev.addSecs( -n - 1 ); return prev >= d->mDateStart ? prev : KDateTime(); } // If we have a cache (duration given), use that if ( d->mDuration > 0 ) { if ( !d->mCached ) { d->buildCache(); } int i = d->mCachedDates.findLT( toDate ); if ( i >= 0 ) { return d->mCachedDates[i]; } return KDateTime(); } KDateTime prev = toDate; if ( d->mDuration >= 0 && endDt().isValid() && toDate > endDt() ) { prev = endDt().addSecs( 1 ).toTimeSpec( d->mDateStart.timeSpec() ); } Constraint interval( d->getPreviousValidDateInterval( prev, recurrenceType() ) ); DateTimeList dts = d->datesForInterval( interval, recurrenceType() ); int i = dts.findLT( prev ); if ( i >= 0 ) { return ( dts[i] >= d->mDateStart ) ? dts[i] : KDateTime(); } // Previous interval. As soon as we find an occurrence, we're done. while ( interval.intervalDateTime( recurrenceType() ) > d->mDateStart ) { interval.increase( recurrenceType(), -int( frequency() ) ); // The returned date list is sorted DateTimeList dts = d->datesForInterval( interval, recurrenceType() ); // The list is sorted, so take the last one. if ( !dts.isEmpty() ) { prev = dts.last(); if ( prev.isValid() && prev >= d->mDateStart ) { return prev; } else { return KDateTime(); } } } return KDateTime(); } KDateTime RecurrenceRule::getNextDate( const KDateTime &preDate ) const { // Convert to the time spec used by this recurrence rule KDateTime fromDate( preDate.toTimeSpec( d->mDateStart.timeSpec() ) ); // Beyond end of recurrence if ( d->mDuration >= 0 && endDt().isValid() && fromDate >= endDt() ) { return KDateTime(); } // Start date is only included if it really matches if ( fromDate < d->mDateStart ) { fromDate = d->mDateStart.addSecs( -1 ); } if ( d->mTimedRepetition ) { // It's a simple sub-daily recurrence with no constraints int n = static_cast( ( d->mDateStart.secsTo_long( fromDate ) + 1 ) % d->mTimedRepetition ); KDateTime next = fromDate.addSecs( d->mTimedRepetition - n + 1 ); return d->mDuration < 0 || !endDt().isValid() || next <= endDt() ? next : KDateTime(); } if ( d->mDuration > 0 ) { if ( !d->mCached ) { d->buildCache(); } int i = d->mCachedDates.findGT( fromDate ); if ( i >= 0 ) { return d->mCachedDates[i]; } } KDateTime end = endDt(); Constraint interval( d->getNextValidDateInterval( fromDate, recurrenceType() ) ); DateTimeList dts = d->datesForInterval( interval, recurrenceType() ); int i = dts.findGT( fromDate ); if ( i >= 0 ) { return ( d->mDuration < 0 || dts[i] <= end ) ? dts[i] : KDateTime(); } interval.increase( recurrenceType(), frequency() ); if ( d->mDuration >= 0 && interval.intervalDateTime( recurrenceType() ) > end ) { return KDateTime(); } // Increase the interval. The first occurrence that we find is the result (if // if's before the end date). // TODO: some validity checks to avoid infinite loops for contradictory constraints int loop = 0; do { DateTimeList dts = d->datesForInterval( interval, recurrenceType() ); if ( dts.count() > 0 ) { KDateTime ret( dts[0] ); if ( d->mDuration >= 0 && ret > end ) { return KDateTime(); } else { return ret; } } interval.increase( recurrenceType(), frequency() ); } while ( ++loop < LOOP_LIMIT && ( d->mDuration < 0 || interval.intervalDateTime( recurrenceType() ) < end ) ); return KDateTime(); } DateTimeList RecurrenceRule::timesInInterval( const KDateTime &dtStart, const KDateTime &dtEnd ) const { KDateTime start = dtStart.toTimeSpec( d->mDateStart.timeSpec() ); KDateTime end = dtEnd.toTimeSpec( d->mDateStart.timeSpec() ); DateTimeList result; if ( end < d->mDateStart ) { return result; // before start of recurrence } KDateTime enddt = end; if ( d->mDuration >= 0 ) { KDateTime endRecur = endDt(); if ( endRecur.isValid() ) { if ( start >= endRecur ) { return result; // beyond end of recurrence } if ( end > endRecur ) { enddt = endRecur; // limit end time to end of recurrence rule } } } if ( d->mTimedRepetition ) { // It's a simple sub-daily recurrence with no constraints int n = static_cast( ( d->mDateStart.secsTo_long( start ) - 1 ) % d->mTimedRepetition ); KDateTime dt = start.addSecs( d->mTimedRepetition - n ); if ( dt < enddt ) { n = static_cast( ( dt.secsTo_long( enddt ) - 1 ) / d->mTimedRepetition ) + 1; for ( int i = 0; i < n; dt = dt.addSecs( d->mTimedRepetition ), ++i ) { result += dt; } } return result; } KDateTime st = start; bool done = false; if ( d->mDuration > 0 ) { if ( !d->mCached ) { d->buildCache(); } if ( d->mCachedDateEnd.isValid() && start >= d->mCachedDateEnd ) { return result; // beyond end of recurrence } int i = d->mCachedDates.findGE( start ); if ( i >= 0 ) { int iend = d->mCachedDates.findGT( enddt, i ); if ( iend < 0 ) { iend = d->mCachedDates.count(); } else { done = true; } while ( i < iend ) { result += d->mCachedDates[i++]; } } if ( d->mCachedDateEnd.isValid() ) { done = true; } else if ( !result.isEmpty() ) { result += KDateTime(); // indicate that the returned list is incomplete done = true; } if ( done ) { return result; } // We don't have any result yet, but we reached the end of the incomplete cache st = d->mCachedLastDate.addSecs( 1 ); } Constraint interval( d->getNextValidDateInterval( st, recurrenceType() ) ); int loop = 0; do { DateTimeList dts = d->datesForInterval( interval, recurrenceType() ); int i = 0; int iend = dts.count(); if ( loop == 0 ) { i = dts.findGE( st ); if ( i < 0 ) { i = iend; } } int j = dts.findGT( enddt, i ); if ( j >= 0 ) { iend = j; loop = LOOP_LIMIT; } while ( i < iend ) { result += dts[i++]; } // Increase the interval. interval.increase( recurrenceType(), frequency() ); } while ( ++loop < LOOP_LIMIT && interval.intervalDateTime( recurrenceType() ) < end ); return result; } //@cond PRIVATE // Find the date/time of the occurrence at or before a date/time, // for a given period type. // Return a constraint whose value appropriate to 'type', is set to // the value contained in the date/time. Constraint RecurrenceRule::Private::getPreviousValidDateInterval( const KDateTime &dt, PeriodType type ) const { long periods = 0; KDateTime start = mDateStart; KDateTime nextValid( start ); int modifier = 1; KDateTime toDate( dt.toTimeSpec( start.timeSpec() ) ); // for super-daily recurrences, don't care about the time part // Find the #intervals since the dtstart and round to the next multiple of // the frequency switch ( type ) { // Really fall through for sub-daily, since the calculations only differ // by the factor 60 and 60*60! Same for weekly and daily (factor 7) case rHourly: modifier *= 60; case rMinutely: modifier *= 60; case rSecondly: periods = static_cast( start.secsTo_long( toDate ) / modifier ); // round it down to the next lower multiple of frequency: - periods = ( periods / mFrequency ) * mFrequency; + if ( mFrequency > 0 ) { + periods = ( periods / mFrequency ) * mFrequency; + } nextValid = start.addSecs( modifier * periods ); break; case rWeekly: toDate = toDate.addDays( -( 7 + toDate.date().dayOfWeek() - mWeekStart ) % 7 ); start = start.addDays( -( 7 + start.date().dayOfWeek() - mWeekStart ) % 7 ); modifier *= 7; case rDaily: periods = start.daysTo( toDate ) / modifier; // round it down to the next lower multiple of frequency: - periods = ( periods / mFrequency ) * mFrequency; + if ( mFrequency > 0 ) { + periods = ( periods / mFrequency ) * mFrequency; + } nextValid = start.addDays( modifier * periods ); break; case rMonthly: { periods = 12 * ( toDate.date().year() - start.date().year() ) + ( toDate.date().month() - start.date().month() ); // round it down to the next lower multiple of frequency: - periods = ( periods / mFrequency ) * mFrequency; + if ( mFrequency > 0 ) { + periods = ( periods / mFrequency ) * mFrequency; + } // set the day to the first day of the month, so we don't have problems // with non-existent days like Feb 30 or April 31 start.setDate( QDate( start.date().year(), start.date().month(), 1 ) ); nextValid.setDate( start.date().addMonths( periods ) ); break; } case rYearly: periods = ( toDate.date().year() - start.date().year() ); // round it down to the next lower multiple of frequency: - periods = ( periods / mFrequency ) * mFrequency; + if ( mFrequency > 0 ) { + periods = ( periods / mFrequency ) * mFrequency; + } nextValid.setDate( start.date().addYears( periods ) ); break; default: break; } return Constraint( nextValid, type, mWeekStart ); } // Find the date/time of the next occurrence at or after a date/time, // for a given period type. // Return a constraint whose value appropriate to 'type', is set to the // value contained in the date/time. Constraint RecurrenceRule::Private::getNextValidDateInterval( const KDateTime &dt, PeriodType type ) const { // TODO: Simplify this! long periods = 0; KDateTime start = mDateStart; KDateTime nextValid( start ); int modifier = 1; KDateTime toDate( dt.toTimeSpec( start.timeSpec() ) ); // for super-daily recurrences, don't care about the time part // Find the #intervals since the dtstart and round to the next multiple of // the frequency switch ( type ) { // Really fall through for sub-daily, since the calculations only differ // by the factor 60 and 60*60! Same for weekly and daily (factor 7) case rHourly: modifier *= 60; case rMinutely: modifier *= 60; case rSecondly: periods = static_cast( start.secsTo_long( toDate ) / modifier ); periods = qMax( 0L, periods ); - if ( periods > 0 ) { + if ( periods > 0 && mFrequency > 0 ) { periods += ( mFrequency - 1 - ( ( periods - 1 ) % mFrequency ) ); } nextValid = start.addSecs( modifier * periods ); break; case rWeekly: // correct both start date and current date to start of week toDate = toDate.addDays( -( 7 + toDate.date().dayOfWeek() - mWeekStart ) % 7 ); start = start.addDays( -( 7 + start.date().dayOfWeek() - mWeekStart ) % 7 ); modifier *= 7; case rDaily: periods = start.daysTo( toDate ) / modifier; periods = qMax( 0L, periods ); - if ( periods > 0 ) { + if ( periods > 0 && mFrequency > 0 ) { periods += ( mFrequency - 1 - ( ( periods - 1 ) % mFrequency ) ); } nextValid = start.addDays( modifier * periods ); break; case rMonthly: { periods = 12 * ( toDate.date().year() - start.date().year() ) + ( toDate.date().month() - start.date().month() ); periods = qMax( 0L, periods ); - if ( periods > 0 ) { + if ( periods > 0 && mFrequency > 0 ) { periods += ( mFrequency - 1 - ( ( periods - 1 ) % mFrequency ) ); } // set the day to the first day of the month, so we don't have problems // with non-existent days like Feb 30 or April 31 start.setDate( QDate( start.date().year(), start.date().month(), 1 ) ); nextValid.setDate( start.date().addMonths( periods ) ); break; } case rYearly: periods = ( toDate.date().year() - start.date().year() ); periods = qMax( 0L, periods ); - if ( periods > 0 ) { + if ( periods > 0 && mFrequency > 0 ) { periods += ( mFrequency - 1 - ( ( periods - 1 ) % mFrequency ) ); } nextValid.setDate( start.date().addYears( periods ) ); break; default: break; } return Constraint( nextValid, type, mWeekStart ); } DateTimeList RecurrenceRule::Private::datesForInterval( const Constraint &interval, PeriodType type ) const { /* -) Loop through constraints, -) merge interval with each constraint -) if merged constraint is not consistent => ignore that constraint -) if complete => add that one date to the date list -) Loop through all missing fields => For each add the resulting */ DateTimeList lst; for ( int i = 0, iend = mConstraints.count(); i < iend; ++i ) { Constraint merged( interval ); if ( merged.merge( mConstraints[i] ) ) { // If the information is incomplete, we can't use this constraint if ( merged.year > 0 && merged.hour >= 0 && merged.minute >= 0 && merged.second >= 0 ) { // We have a valid constraint, so get all datetimes that match it andd // append it to all date/times of this interval QList lstnew = merged.dateTimes( type ); lst += lstnew; } } } // Sort it so we can apply the BySetPos. Also some logic relies on this being sorted lst.sortUnique(); /*if ( lst.isEmpty() ) { kDebug() << " No Dates in Interval"; } else { kDebug() << " Dates:"; for ( int i = 0, iend = lst.count(); i < iend; ++i ) { kDebug()<< " -)" << dumpTime(lst[i]); } kDebug() << " ---------------------"; }*/ if ( !mBySetPos.isEmpty() ) { DateTimeList tmplst = lst; lst.clear(); for ( int i = 0, iend = mBySetPos.count(); i < iend; ++i ) { int pos = mBySetPos[i]; if ( pos > 0 ) { --pos; } if ( pos < 0 ) { pos += tmplst.count(); } if ( pos >= 0 && pos < tmplst.count() ) { lst.append( tmplst[pos] ); } } lst.sortUnique(); } return lst; } //@endcond void RecurrenceRule::dump() const { #ifndef NDEBUG kDebug(); if ( !d->mRRule.isEmpty() ) { kDebug() << " RRULE=" << d->mRRule; } kDebug() << " Read-Only:" << isReadOnly(); kDebug() << " Period type:" << recurrenceType() << ", frequency:" << frequency(); kDebug() << " #occurrences:" << duration(); kDebug() << " start date:" << dumpTime( startDt() ) << ", end date:" << dumpTime( endDt() ); #define dumpByIntList(list,label) \ if ( !list.isEmpty() ) {\ QStringList lst;\ for ( int i = 0, iend = list.count(); i < iend; ++i ) {\ lst.append( QString::number( list[i] ) );\ }\ kDebug() << " " << label << lst.join( ", " );\ } dumpByIntList( d->mBySeconds, "BySeconds: " ); dumpByIntList( d->mByMinutes, "ByMinutes: " ); dumpByIntList( d->mByHours, "ByHours: " ); if ( !d->mByDays.isEmpty() ) { QStringList lst; for ( int i = 0, iend = d->mByDays.count(); i < iend; ++i ) {\ lst.append( ( d->mByDays[i].pos() ? QString::number( d->mByDays[i].pos() ) : "" ) + DateHelper::dayName( d->mByDays[i].day() ) ); } kDebug() << " ByDays: " << lst.join( ", " ); } dumpByIntList( d->mByMonthDays, "ByMonthDays:" ); dumpByIntList( d->mByYearDays, "ByYearDays: " ); dumpByIntList( d->mByWeekNumbers, "ByWeekNr: " ); dumpByIntList( d->mByMonths, "ByMonths: " ); dumpByIntList( d->mBySetPos, "BySetPos: " ); #undef dumpByIntList kDebug() << " Week start:" << DateHelper::dayName( d->mWeekStart ); kDebug() << " Constraints:"; // dump constraints for ( int i = 0, iend = d->mConstraints.count(); i < iend; ++i ) { d->mConstraints[i].dump(); } #endif } //@cond PRIVATE void Constraint::dump() const { kDebug() << " ~> Y=" << year << ", M=" << month << ", D=" << day << ", H=" << hour << ", m=" << minute << ", S=" << second << ", wd=" << weekday << ",#wd=" << weekdaynr << ", #w=" << weeknumber << ", yd=" << yearday; } //@endcond QString dumpTime( const KDateTime &dt ) { #ifndef NDEBUG if ( !dt.isValid() ) { return QString(); } QString result; if ( dt.isDateOnly() ) { result = dt.toString( "%a %Y-%m-%d %:Z" ); } else { result = dt.toString( "%a %Y-%m-%d %H:%M:%S %:Z" ); if ( dt.isSecondOccurrence() ) { result += QLatin1String( " (2nd)" ); } } if ( dt.timeSpec() == KDateTime::Spec::ClockTime() ) { result += QLatin1String( "Clock" ); } return result; #else return QString(); #endif } KDateTime RecurrenceRule::startDt() const { return d->mDateStart; } RecurrenceRule::PeriodType RecurrenceRule::recurrenceType() const { return d->mPeriod; } uint RecurrenceRule::frequency() const { return d->mFrequency; } int RecurrenceRule::duration() const { return d->mDuration; } QString RecurrenceRule::rrule() const { return d->mRRule; } void RecurrenceRule::setRRule( const QString &rrule ) { d->mRRule = rrule; } bool RecurrenceRule::isReadOnly() const { return d->mIsReadOnly; } void RecurrenceRule::setReadOnly( bool readOnly ) { d->mIsReadOnly = readOnly; } bool RecurrenceRule::recurs() const { return d->mPeriod != rNone; } bool RecurrenceRule::allDay() const { return d->mAllDay; } const QList &RecurrenceRule::bySeconds() const { return d->mBySeconds; } const QList &RecurrenceRule::byMinutes() const { return d->mByMinutes; } const QList &RecurrenceRule::byHours() const { return d->mByHours; } const QList &RecurrenceRule::byDays() const { return d->mByDays; } const QList &RecurrenceRule::byMonthDays() const { return d->mByMonthDays; } const QList &RecurrenceRule::byYearDays() const { return d->mByYearDays; } const QList &RecurrenceRule::byWeekNumbers() const { return d->mByWeekNumbers; } const QList &RecurrenceRule::byMonths() const { return d->mByMonths; } const QList &RecurrenceRule::bySetPos() const { return d->mBySetPos; } short RecurrenceRule::weekStart() const { return d->mWeekStart; }