diff --git a/kcal/recurrencerule.cpp b/kcal/recurrencerule.cpp index 9870bd467..a7b7dba74 100644 --- a/kcal/recurrencerule.cpp +++ b/kcal/recurrencerule.cpp @@ -1,2148 +1,2196 @@ /* 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(); - } + Private( RecurrenceRule *parent, const Private &p ); + Private &operator=( const Private &other ); bool operator==( const Private &other ) const; void clear(); void setDirty(); void buildConstraints(); bool buildCache() const; Constraint getNextValidDateInterval( const KDateTime &preDate, PeriodType type ) const; Constraint getPreviousValidDateInterval( const KDateTime &afterDate, PeriodType type ) const; DateTimeList datesForInterval( const Constraint &interval, PeriodType type ) const; RecurrenceRule *mParent; QString mRRule; // RRULE string PeriodType mPeriod; KDateTime mDateStart; // start of recurrence (but mDateStart is not an occurrence // unless it matches the rule) uint mFrequency; /** how often it recurs: -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 }; +RecurrenceRule::Private::Private( RecurrenceRule *parent, const Private &p ) + : mParent( parent ), + mRRule( p.mRRule ), + mPeriod( p.mPeriod ), + mDateStart( p.mDateStart ), + mFrequency( p.mFrequency ), + mDuration( p.mDuration ), + mDateEnd( p.mDateEnd ), + + mBySeconds( p.mBySeconds ), + mByMinutes( p.mByMinutes ), + mByHours( p.mByHours ), + mByDays( p.mByDays ), + mByMonthDays( p.mByMonthDays ), + mByYearDays( p.mByYearDays ), + mByWeekNumbers( p.mByWeekNumbers ), + mByMonths( p.mByMonths ), + mBySetPos( p.mBySetPos ), + mWeekStart( p.mWeekStart ), + + mIsReadOnly( p.mIsReadOnly ), + mAllDay( p.mAllDay ) +{ + setDirty(); +} + +RecurrenceRule::Private &RecurrenceRule::Private::operator=( const Private &p ) +{ + // check for self assignment + if ( &p == this ) { + return *this; + } + + mRRule = p.mRRule; + mPeriod = p.mPeriod; + mDateStart = p.mDateStart; + mFrequency = p.mFrequency; + mDuration = p.mDuration; + mDateEnd = p.mDateEnd; + + mBySeconds = p.mBySeconds; + mByMinutes = p.mByMinutes; + mByHours = p.mByHours; + mByDays = p.mByDays; + mByMonthDays = p.mByMonthDays; + mByYearDays = p.mByYearDays; + mByWeekNumbers = p.mByWeekNumbers; + mByMonths = p.mByMonths; + mBySetPos = p.mBySetPos; + mWeekStart = p.mWeekStart; + + mIsReadOnly = p.mIsReadOnly; + mAllDay = p.mAllDay; + + setDirty(); + + return *this; +} + 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; } +RecurrenceRule &RecurrenceRule::operator=( const RecurrenceRule &r ) +{ + // check for self assignment + if ( &r == this ) { + return *this; + } + + *d = *r.d; + + return *this; +} + void RecurrenceRule::addObserver( RuleObserver *observer ) { if ( !d->mObservers.contains( observer ) ) { d->mObservers.append( observer ); } } void RecurrenceRule::removeObserver( RuleObserver *observer ) { if ( d->mObservers.contains( observer ) ) { d->mObservers.removeAll( observer ); } } void RecurrenceRule::setRecurrenceType( PeriodType period ) { if ( isReadOnly() ) { return; } d->mPeriod = period; d->setDirty(); } KDateTime RecurrenceRule::endDt( bool *result ) const { if ( result ) { *result = false; } if ( d->mPeriod == rNone ) { return KDateTime(); } if ( d->mDuration < 0 ) { return KDateTime(); } if ( d->mDuration == 0 ) { if ( result ) { *result = true; } return d->mDateEnd; } // N occurrences. Check if we have a full cache. If so, return the cached end date. if ( !d->mCached ) { // If not enough occurrences can be found (i.e. inconsistent constraints) if ( !d->buildCache() ) { return KDateTime(); } } if ( result ) { *result = true; } return d->mCachedDateEnd; } void RecurrenceRule::setEndDt( const KDateTime &dateTime ) { if ( isReadOnly() ) { return; } d->mDateEnd = dateTime; d->mDuration = 0; // set to 0 because there is an end date/time 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; 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; 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; // 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; 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 ) { 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 ) { 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 ) { 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 ) { 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; }