You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1647 lines
52 KiB
1647 lines
52 KiB
/*
|
|
This file is part of libkcal.
|
|
|
|
Copyright (c) 2005 Reinhold Kainhofer <reinhold@kainhofe.com>
|
|
|
|
|
|
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 <kdebug.h>
|
|
#include <kglobal.h>
|
|
#include <tqdatetime.h>
|
|
#include <tqstringlist.h>
|
|
|
|
#include <limits.h>
|
|
#include <math.h>
|
|
|
|
using namespace KCal;
|
|
|
|
// Maximum number of intervals to process
|
|
const int LOOP_LIMIT = 10000;
|
|
|
|
// FIXME: If Qt is ever changed so that TQDateTime:::addSecs takes into account
|
|
// DST shifts, we need to use our own addSecs method, too, since we
|
|
// need to caalculate things in UTC!
|
|
/** Workaround for broken TQDateTime::secsTo (at least in Qt 3.3). While
|
|
TQDateTime::secsTo does take time zones into account, TQDateTime::addSecs
|
|
does not, so we have a problem:
|
|
TQDateTime d1(TQDate(2005, 10, 30), TQTime(1, 30, 0) );
|
|
TQDateTime d2(TQDate(2005, 10, 30), TQTime(3, 30, 0) );
|
|
|
|
kdDebug(5800) << "d1=" << d1 << ", d2=" << d2 << endl;
|
|
kdDebug(5800) << "d1.secsTo(d2)=" << d1.secsTo(d2) << endl;
|
|
kdDebug(5800) << "d1.addSecs(d1.secsTo(d2))=" << d1.addSecs(d1.secsTo(d2)) << endl;
|
|
This code generates the following output:
|
|
libkcal: d1=Son Okt 30 01:30:00 2005, d2=Son Okt 30 03:30:00 2005
|
|
libkcal: d1.secsTo(d2)=10800
|
|
libkcal: d1.addSecs(d1.secsTo(d2))=Son Okt 30 04:30:00 2005
|
|
Notice that secsTo counts the hour between 2:00 and 3:00 twice, while
|
|
adddSecs doesn't and so has one additional hour. This basically makes it
|
|
impossible to use TQDateTime for *any* calculations, in local time zone as
|
|
well as in UTC. Since we don't want to use
|
|
time zones anyway, but do all secondsly/minutely/hourly calculations in UTC,
|
|
we simply use our own secsTo, which ignores all time zone shifts. */
|
|
long long ownSecsTo( const TQDateTime &dt1, const TQDateTime &dt2 )
|
|
{
|
|
long long res = static_cast<long long>( dt1.date().daysTo( dt2.date() ) ) * 24*3600;
|
|
res += dt1.time().secsTo( dt2.time() );
|
|
return res;
|
|
}
|
|
|
|
|
|
|
|
/**************************************************************************
|
|
* DateHelper *
|
|
**************************************************************************/
|
|
|
|
|
|
class DateHelper {
|
|
public:
|
|
#ifndef NDEBUG
|
|
static TQString dayName( short day );
|
|
#endif
|
|
static TQDate getNthWeek( int year, int weeknumber, short weekstart = 1 );
|
|
static int weekNumbersInYear( int year, short weekstart = 1 );
|
|
static int getWeekNumber( const TQDate &date, short weekstart, int *year = 0 );
|
|
static int getWeekNumberNeg( const TQDate &date, short weekstart, int *year = 0 );
|
|
};
|
|
|
|
|
|
#ifndef NDEBUG
|
|
TQString DateHelper::dayName( short day )
|
|
{
|
|
switch ( day ) {
|
|
case 1: return "MO"; break;
|
|
case 2: return "TU"; break;
|
|
case 3: return "WE"; break;
|
|
case 4: return "TH"; break;
|
|
case 5: return "FR"; break;
|
|
case 6: return "SA"; break;
|
|
case 7: return "SU"; break;
|
|
default: return "??";
|
|
}
|
|
}
|
|
#endif
|
|
|
|
|
|
TQDate DateHelper::getNthWeek( int year, int weeknumber, short weekstart )
|
|
{
|
|
if ( weeknumber == 0 ) return TQDate();
|
|
// Adjust this to the first day of week #1 of the year and add 7*weekno days.
|
|
TQDate 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 TQDate &date, short weekstart, int *year )
|
|
{
|
|
// kdDebug(5800) << "Getting week number for " << date << " with weekstart="<<weekstart<<endl;
|
|
if ( year ) *year = date.year();
|
|
TQDate dt( date.year(), 1, 4 ); // <= definitely in week #1
|
|
dt = dt.addDays( -(7 + dt.dayOfWeek() - weekstart) % 7 ); // begin of week #1
|
|
TQDate dtn( date.year()+1, 1, 4 ); // <= definitely first week of next year
|
|
dtn = dtn.addDays( -(7 + dtn.dayOfWeek() - weekstart) % 7 );
|
|
|
|
int daysto = dt.daysTo( date );
|
|
int dayston = dtn.daysTo( date );
|
|
if ( daysto < 0 ) {
|
|
if ( year ) *year = date.year()-1;
|
|
dt = TQDate( date.year()-1, 1, 4 );
|
|
dt = dt.addDays( -(7 + dt.dayOfWeek() - weekstart) % 7 ); // begin of week #1
|
|
daysto = dt.daysTo( date );
|
|
} else if ( dayston >= 0 ) {
|
|
// in first week of next year;
|
|
if ( year ) *year = date.year() + 1;
|
|
dt = dtn;
|
|
daysto = dayston;
|
|
}
|
|
return daysto / 7 + 1;
|
|
}
|
|
|
|
int DateHelper::weekNumbersInYear( int year, short weekstart )
|
|
{
|
|
TQDate dt( year, 1, weekstart );
|
|
TQDate dt1( year + 1, 1, weekstart );
|
|
return dt.daysTo( dt1 ) / 7;
|
|
}
|
|
|
|
// Week number from the end of the year
|
|
int DateHelper::getWeekNumberNeg( const TQDate &date, short weekstart, int *year )
|
|
{
|
|
int weekpos = getWeekNumber( date, weekstart, year );
|
|
return weekNumbersInYear( *year, weekstart ) - weekpos - 1;
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**************************************************************************
|
|
* RecurrenceRule::Constraint *
|
|
**************************************************************************/
|
|
|
|
|
|
RecurrenceRule::Constraint::Constraint( int wkst )
|
|
{
|
|
weekstart = wkst;
|
|
clear();
|
|
}
|
|
|
|
RecurrenceRule::Constraint::Constraint( const TQDateTime &preDate, PeriodType type, int wkst )
|
|
{
|
|
weekstart = wkst;
|
|
readDateTime( preDate, type );
|
|
}
|
|
|
|
void RecurrenceRule::Constraint::clear()
|
|
{
|
|
year = 0;
|
|
month = 0;
|
|
day = 0;
|
|
hour = -1;
|
|
minute = -1;
|
|
second = -1;
|
|
weekday = 0;
|
|
weekdaynr = 0;
|
|
weeknumber = 0;
|
|
yearday = 0;
|
|
}
|
|
|
|
bool RecurrenceRule::Constraint::matches( const TQDate &dt, RecurrenceRule::PeriodType type ) const
|
|
{
|
|
// If the event recurs in week 53 or 1, the day might not belong to the same
|
|
// year as the week it is in. E.g. Jan 1, 2005 is in week 53 of year 2004.
|
|
// So we can't simply check the year in that case!
|
|
if ( weeknumber == 0 ) {
|
|
if ( year > 0 && year != dt.year() ) return false;
|
|
} else {
|
|
int y;
|
|
if ( weeknumber > 0 &&
|
|
weeknumber != DateHelper::getWeekNumber( dt, weekstart, &y ) ) return false;
|
|
if ( weeknumber < 0 &&
|
|
weeknumber != DateHelper::getWeekNumberNeg( dt, weekstart, &y ) ) return false;
|
|
if ( year > 0 && year != y ) return false;
|
|
}
|
|
|
|
if ( month > 0 && month != dt.month() ) return false;
|
|
if ( day > 0 && day != dt.day() ) return false;
|
|
if ( day < 0 && dt.day() != (dt.daysInMonth() + day + 1 ) ) return false;
|
|
if ( weekday > 0 ) {
|
|
if ( weekday != dt.dayOfWeek() ) return false;
|
|
if ( weekdaynr != 0 ) {
|
|
// If it's a yearly recurrence and a month is given, the position is
|
|
// still in the month, not in the year.
|
|
bool inMonth = (type == rMonthly) || ( type == rYearly && month > 0 );
|
|
// Monthly
|
|
if ( weekdaynr > 0 && inMonth &&
|
|
weekdaynr != (dt.day() - 1)/7 + 1 ) return false;
|
|
if ( weekdaynr < 0 && inMonth &&
|
|
weekdaynr != -((dt.daysInMonth() - dt.day() )/7 + 1 ) )
|
|
return false;
|
|
// Yearly
|
|
if ( weekdaynr > 0 && !inMonth &&
|
|
weekdaynr != (dt.dayOfYear() - 1)/7 + 1 ) return false;
|
|
if ( weekdaynr < 0 && !inMonth &&
|
|
weekdaynr != -((dt.daysInYear() - dt.dayOfYear() )/7 + 1 ) )
|
|
return false;
|
|
}
|
|
}
|
|
if ( yearday > 0 && yearday != dt.dayOfYear() ) return false;
|
|
if ( yearday < 0 && yearday != dt.daysInYear() - dt.dayOfYear() + 1 )
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
bool RecurrenceRule::Constraint::matches( const TQDateTime &dt, RecurrenceRule::PeriodType type ) const
|
|
{
|
|
if ( !matches( dt.date(), type ) ) return false;
|
|
if ( hour >= 0 && hour != dt.time().hour() ) return false;
|
|
if ( minute >= 0 && minute != dt.time().minute() ) return false;
|
|
if ( second >= 0 && second != dt.time().second() ) return false;
|
|
return true;
|
|
}
|
|
|
|
bool RecurrenceRule::Constraint::isConsistent( PeriodType /*period*/) const
|
|
{
|
|
// TODO: Check for consistency, e.g. byyearday=3 and bymonth=10
|
|
return true;
|
|
}
|
|
|
|
TQDateTime RecurrenceRule::Constraint::intervalDateTime( RecurrenceRule::PeriodType type ) const
|
|
{
|
|
TQDateTime dt;
|
|
dt.setTime( TQTime( 0, 0, 0 ) );
|
|
dt.setDate( TQDate( year, (month>0)?month:1, (day>0)?day:1 ) );
|
|
if ( day < 0 )
|
|
dt = dt.addDays( dt.date().daysInMonth() + day );
|
|
switch ( type ) {
|
|
case rSecondly:
|
|
dt.setTime( TQTime( hour, minute, second ) ); break;
|
|
case rMinutely:
|
|
dt.setTime( TQTime( hour, minute, 1 ) ); break;
|
|
case rHourly:
|
|
dt.setTime( TQTime( hour, 1, 1 ) ); break;
|
|
case rDaily:
|
|
break;
|
|
case rWeekly:
|
|
dt = DateHelper::getNthWeek( year, weeknumber, weekstart ); break;
|
|
case rMonthly:
|
|
dt.setDate( TQDate( year, month, 1 ) ); break;
|
|
case rYearly:
|
|
dt.setDate( TQDate( year, 1, 1 ) ); break;
|
|
default:
|
|
break;
|
|
}
|
|
return dt;
|
|
}
|
|
|
|
|
|
// Y M D | H Mn S | WD #WD | WN | YD
|
|
// required:
|
|
// x | x x x | | |
|
|
// 0) Trivial: Exact date given, maybe other restrictions
|
|
// x x x | x x x | | |
|
|
// 1) Easy case: no weekly restrictions -> at most a loop through possible dates
|
|
// x + + | x x x | - - | - | -
|
|
// 2) Year day is given -> date known
|
|
// x | x x x | | | +
|
|
// 3) week number is given -> loop through all days of that week. Further
|
|
// restrictions will be applied in the end, when we check all dates for
|
|
// consistency with the constraints
|
|
// x | x x x | | + | (-)
|
|
// 4) week day is specified ->
|
|
// x | x x x | x ? | (-)| (-)
|
|
// 5) All possiblecases have already been treated, so this must be an error!
|
|
|
|
DateTimeList RecurrenceRule::Constraint::dateTimes( RecurrenceRule::PeriodType type ) const
|
|
{
|
|
// kdDebug(5800) << " RecurrenceRule::Constraint::dateTimes: " << endl;
|
|
DateTimeList result;
|
|
bool done = false;
|
|
// TODO_Recurrence: Handle floating
|
|
TQTime tm( hour, minute, second );
|
|
if ( !isConsistent( type ) ) return result;
|
|
|
|
if ( !done && day > 0 && month > 0 ) {
|
|
TQDateTime dt( TQDate( year, month, day ), tm );
|
|
if ( dt.isValid() ) result.append( dt );
|
|
done = true;
|
|
}
|
|
if ( !done && day < 0 && month > 0 ) {
|
|
TQDateTime dt( TQDate( year, month, 1 ), tm );
|
|
dt = dt.addDays( dt.date().daysInMonth() + day );
|
|
if ( dt.isValid() ) result.append( dt );
|
|
done = true;
|
|
}
|
|
|
|
|
|
if ( !done && weekday == 0 && weeknumber == 0 && yearday == 0 ) {
|
|
// Easy case: date is given, not restrictions by week or yearday
|
|
uint mstart = (month>0) ? month : 1;
|
|
uint mend = (month <= 0) ? 12 : month;
|
|
for ( uint m = mstart; m <= mend; ++m ) {
|
|
uint dstart, dend;
|
|
if ( day > 0 ) {
|
|
dstart = dend = day;
|
|
} else if ( day < 0 ) {
|
|
TQDate date( year, month, 1 );
|
|
dstart = dend = date.daysInMonth() + day + 1;
|
|
} else {
|
|
TQDate date( year, month, 1 );
|
|
dstart = 1;
|
|
dend = date.daysInMonth();
|
|
}
|
|
for ( uint d = dstart; d <= dend; ++d ) {
|
|
TQDateTime dt( TQDate( year, m, d ), tm );
|
|
if ( dt.isValid() ) result.append( dt );
|
|
}
|
|
}
|
|
done = true;
|
|
}
|
|
|
|
// Else: At least one of the week / yearday restrictions was given...
|
|
// If we have a yearday (and of course a year), we know the exact date
|
|
if ( !done && yearday != 0 ) {
|
|
// yearday < 0 means from end of year, so we'll need Jan 1 of the next year
|
|
TQDate d( year + ((yearday>0)?0:1), 1, 1 );
|
|
d = d.addDays( yearday - ((yearday>0)?1:0) );
|
|
result.append( TQDateTime( d, tm ) );
|
|
done = true;
|
|
}
|
|
|
|
// Else: If we have a weeknumber, we have at most 7 possible dates, loop through them
|
|
if ( !done && weeknumber != 0 ) {
|
|
TQDate wst( DateHelper::getNthWeek( year, weeknumber, weekstart ) );
|
|
if ( weekday != 0 ) {
|
|
wst = wst.addDays( (7 + weekday - weekstart ) % 7 );
|
|
result.append( TQDateTime( wst, tm ) );
|
|
} else {
|
|
for ( int i = 0; i < 7; ++i ) {
|
|
result.append( TQDateTime( wst, tm ) );
|
|
wst = wst.addDays( 1 );
|
|
}
|
|
}
|
|
done = true;
|
|
}
|
|
|
|
// weekday is given
|
|
if ( !done && weekday != 0 ) {
|
|
TQDate dt( year, 1, 1 );
|
|
// If type == yearly and month is given, pos is still in month not year!
|
|
// TODO_Recurrence: Correct handling of n-th BYDAY...
|
|
int maxloop = 53;
|
|
bool inMonth = ( type == rMonthly) || ( type == rYearly && month > 0 );
|
|
if ( inMonth && month > 0 ) {
|
|
dt = TQDate( year, month, 1 );
|
|
maxloop = 5;
|
|
}
|
|
if ( weekdaynr < 0 ) {
|
|
// From end of period (month, year) => relative to begin of next period
|
|
if ( inMonth )
|
|
dt = dt.addMonths( 1 );
|
|
else
|
|
dt = dt.addYears( 1 );
|
|
}
|
|
int adj = ( 7 + weekday - dt.dayOfWeek() ) % 7;
|
|
dt = dt.addDays( adj ); // correct first weekday of the period
|
|
|
|
if ( weekdaynr > 0 ) {
|
|
dt = dt.addDays( ( weekdaynr - 1 ) * 7 );
|
|
result.append( TQDateTime( dt, tm ) );
|
|
} else if ( weekdaynr < 0 ) {
|
|
dt = dt.addDays( weekdaynr * 7 );
|
|
result.append( TQDateTime( dt, tm ) );
|
|
} else {
|
|
// loop through all possible weeks, non-matching will be filtered later
|
|
for ( int i = 0; i < maxloop; ++i ) {
|
|
result.append( TQDateTime( dt, tm ) );
|
|
dt = dt.addDays( 7 );
|
|
}
|
|
}
|
|
} // weekday != 0
|
|
|
|
|
|
// Only use those times that really match all other constraints, too
|
|
DateTimeList valid;
|
|
DateTimeList::Iterator it;
|
|
for ( it = result.begin(); it != result.end(); ++it ) {
|
|
if ( matches( *it, type ) ) valid.append( *it );
|
|
}
|
|
// Don't sort it here, would be unnecessary work. The results from all
|
|
// constraints will be merged to one big list of the interval. Sort that one!
|
|
return valid;
|
|
}
|
|
|
|
|
|
bool RecurrenceRule::Constraint::increase( RecurrenceRule::PeriodType type, int freq )
|
|
{
|
|
// convert the first day of the interval to QDateTime
|
|
// Sub-daily types need to be converted to UTC to correctly handle DST shifts
|
|
TQDateTime dt( intervalDateTime( type ) );
|
|
|
|
// Now add the intervals
|
|
switch ( type ) {
|
|
case rSecondly:
|
|
dt = dt.addSecs( freq ); break;
|
|
case rMinutely:
|
|
dt = dt.addSecs( 60*freq ); break;
|
|
case rHourly:
|
|
dt = dt.addSecs( 3600 * freq ); break;
|
|
case rDaily:
|
|
dt = dt.addDays( freq ); break;
|
|
case rWeekly:
|
|
dt = dt.addDays( 7*freq ); break;
|
|
case rMonthly:
|
|
dt = dt.addMonths( freq ); break;
|
|
case rYearly:
|
|
dt = dt.addYears( freq ); break;
|
|
default:
|
|
break;
|
|
}
|
|
// Convert back from TQDateTime to the Constraint class
|
|
readDateTime( dt, type );
|
|
|
|
return true;
|
|
}
|
|
|
|
bool RecurrenceRule::Constraint::readDateTime( const TQDateTime &preDate, PeriodType type )
|
|
{
|
|
clear();
|
|
switch ( type ) {
|
|
// Really fall through! Only weekly needs to be treated differentely!
|
|
case rSecondly:
|
|
second = preDate.time().second();
|
|
case rMinutely:
|
|
minute = preDate.time().minute();
|
|
case rHourly:
|
|
hour = preDate.time().hour();
|
|
case rDaily:
|
|
day = preDate.date().day();
|
|
case rMonthly:
|
|
month = preDate.date().month();
|
|
case rYearly:
|
|
year = preDate.date().year();
|
|
break;
|
|
|
|
case rWeekly:
|
|
// Determine start day of the current week, calculate the week number from that
|
|
weeknumber = DateHelper::getWeekNumber( preDate.date(), weekstart, &year );
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
RecurrenceRule::RecurrenceRule( )
|
|
: mPeriod( rNone ), mFrequency( 0 ), mIsReadOnly( false ),
|
|
mFloating( false ),
|
|
mWeekStart(1)
|
|
{
|
|
}
|
|
|
|
RecurrenceRule::RecurrenceRule( const RecurrenceRule &r )
|
|
{
|
|
mRRule = r.mRRule;
|
|
mPeriod = r.mPeriod;
|
|
mDateStart = r.mDateStart;
|
|
mDuration = r.mDuration;
|
|
mDateEnd = r.mDateEnd;
|
|
mFrequency = r.mFrequency;
|
|
|
|
mIsReadOnly = r.mIsReadOnly;
|
|
mFloating = r.mFloating;
|
|
|
|
mBySeconds = r.mBySeconds;
|
|
mByMinutes = r.mByMinutes;
|
|
mByHours = r.mByHours;
|
|
mByDays = r.mByDays;
|
|
mByMonthDays = r.mByMonthDays;
|
|
mByYearDays = r.mByYearDays;
|
|
mByWeekNumbers = r.mByWeekNumbers;
|
|
mByMonths = r.mByMonths;
|
|
mBySetPos = r.mBySetPos;
|
|
mWeekStart = r.mWeekStart;
|
|
|
|
setDirty();
|
|
}
|
|
|
|
RecurrenceRule::~RecurrenceRule()
|
|
{
|
|
}
|
|
|
|
bool RecurrenceRule::operator==( const RecurrenceRule& r ) const
|
|
{
|
|
if ( mPeriod != r.mPeriod ) return false;
|
|
if ( mDateStart != r.mDateStart ) return false;
|
|
if ( mDuration != r.mDuration ) return false;
|
|
if ( mDateEnd != r.mDateEnd ) return false;
|
|
if ( mFrequency != r.mFrequency ) return false;
|
|
|
|
if ( mIsReadOnly != r.mIsReadOnly ) return false;
|
|
if ( mFloating != r.mFloating ) return false;
|
|
|
|
if ( mBySeconds != r.mBySeconds ) return false;
|
|
if ( mByMinutes != r.mByMinutes ) return false;
|
|
if ( mByHours != r.mByHours ) return false;
|
|
if ( mByDays != r.mByDays ) return false;
|
|
if ( mByMonthDays != r.mByMonthDays ) return false;
|
|
if ( mByYearDays != r.mByYearDays ) return false;
|
|
if ( mByWeekNumbers != r.mByWeekNumbers ) return false;
|
|
if ( mByMonths != r.mByMonths ) return false;
|
|
if ( mBySetPos != r.mBySetPos ) return false;
|
|
if ( mWeekStart != r.mWeekStart ) return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
void RecurrenceRule::addObserver( Observer *observer )
|
|
{
|
|
if ( !mObservers.contains( observer ) )
|
|
mObservers.append( observer );
|
|
}
|
|
|
|
void RecurrenceRule::removeObserver( Observer *observer )
|
|
{
|
|
if ( mObservers.contains( observer ) )
|
|
mObservers.remove( observer );
|
|
}
|
|
|
|
|
|
|
|
void RecurrenceRule::setRecurrenceType( PeriodType period )
|
|
{
|
|
if ( isReadOnly() ) return;
|
|
mPeriod = period;
|
|
setDirty();
|
|
}
|
|
|
|
TQDateTime RecurrenceRule::endDt( bool *result ) const
|
|
{
|
|
if ( result ) *result = false;
|
|
if ( mPeriod == rNone ) return TQDateTime();
|
|
if ( mDuration < 0 ) return TQDateTime();
|
|
if ( mDuration == 0 ) {
|
|
if ( result ) *result = true;
|
|
return mDateEnd;
|
|
}
|
|
// N occurrences. Check if we have a full cache. If so, return the cached end date.
|
|
if ( ! mCached ) {
|
|
// If not enough occurrences can be found (i.e. inconsistent constraints)
|
|
if ( !buildCache() ) return TQDateTime();
|
|
}
|
|
if ( result ) *result = true;
|
|
return mCachedDateEnd;
|
|
}
|
|
|
|
void RecurrenceRule::setEndDt( const TQDateTime &dateTime )
|
|
{
|
|
if ( isReadOnly() ) return;
|
|
mDateEnd = dateTime;
|
|
mDuration = 0; // set to 0 because there is an end date/time
|
|
setDirty();
|
|
}
|
|
|
|
void RecurrenceRule::setDuration(int duration)
|
|
{
|
|
if ( isReadOnly() ) return;
|
|
mDuration = duration;
|
|
setDirty();
|
|
}
|
|
|
|
void RecurrenceRule::setFloats( bool floats )
|
|
{
|
|
if ( isReadOnly() ) return;
|
|
mFloating = floats;
|
|
setDirty();
|
|
}
|
|
|
|
void RecurrenceRule::clear()
|
|
{
|
|
if ( isReadOnly() ) return;
|
|
mPeriod = rNone;
|
|
mBySeconds.clear();
|
|
mByMinutes.clear();
|
|
mByHours.clear();
|
|
mByDays.clear();
|
|
mByMonthDays.clear();
|
|
mByYearDays.clear();
|
|
mByWeekNumbers.clear();
|
|
mByMonths.clear();
|
|
mBySetPos.clear();
|
|
mWeekStart = 1;
|
|
|
|
setDirty();
|
|
}
|
|
|
|
void RecurrenceRule::setDirty()
|
|
{
|
|
mConstraints.clear();
|
|
buildConstraints();
|
|
mDirty = true;
|
|
mCached = false;
|
|
mCachedDates.clear();
|
|
for ( TQValueList<Observer*>::ConstIterator it = mObservers.begin();
|
|
it != mObservers.end(); ++it ) {
|
|
if ( (*it) ) (*it)->recurrenceChanged( this );
|
|
}
|
|
}
|
|
|
|
void RecurrenceRule::setStartDt( const TQDateTime &start )
|
|
{
|
|
if ( isReadOnly() ) return;
|
|
mDateStart = start;
|
|
setDirty();
|
|
}
|
|
|
|
void RecurrenceRule::setFrequency(int freq)
|
|
{
|
|
if ( isReadOnly() || freq <= 0 ) return;
|
|
mFrequency = freq;
|
|
setDirty();
|
|
}
|
|
|
|
void RecurrenceRule::setBySeconds( const TQValueList<int> bySeconds )
|
|
{
|
|
if ( isReadOnly() ) return;
|
|
mBySeconds = bySeconds;
|
|
setDirty();
|
|
}
|
|
|
|
void RecurrenceRule::setByMinutes( const TQValueList<int> byMinutes )
|
|
{
|
|
if ( isReadOnly() ) return;
|
|
mByMinutes = byMinutes;
|
|
setDirty();
|
|
}
|
|
|
|
void RecurrenceRule::setByHours( const TQValueList<int> byHours )
|
|
{
|
|
if ( isReadOnly() ) return;
|
|
mByHours = byHours;
|
|
setDirty();
|
|
}
|
|
|
|
|
|
void RecurrenceRule::setByDays( const TQValueList<WDayPos> byDays )
|
|
{
|
|
if ( isReadOnly() ) return;
|
|
mByDays = byDays;
|
|
setDirty();
|
|
}
|
|
|
|
void RecurrenceRule::setByMonthDays( const TQValueList<int> byMonthDays )
|
|
{
|
|
if ( isReadOnly() ) return;
|
|
mByMonthDays = byMonthDays;
|
|
setDirty();
|
|
}
|
|
|
|
void RecurrenceRule::setByYearDays( const TQValueList<int> byYearDays )
|
|
{
|
|
if ( isReadOnly() ) return;
|
|
mByYearDays = byYearDays;
|
|
setDirty();
|
|
}
|
|
|
|
void RecurrenceRule::setByWeekNumbers( const TQValueList<int> byWeekNumbers )
|
|
{
|
|
if ( isReadOnly() ) return;
|
|
mByWeekNumbers = byWeekNumbers;
|
|
setDirty();
|
|
}
|
|
|
|
void RecurrenceRule::setByMonths( const TQValueList<int> byMonths )
|
|
{
|
|
if ( isReadOnly() ) return;
|
|
mByMonths = byMonths;
|
|
setDirty();
|
|
}
|
|
|
|
void RecurrenceRule::setBySetPos( const TQValueList<int> bySetPos )
|
|
{
|
|
if ( isReadOnly() ) return;
|
|
mBySetPos = bySetPos;
|
|
setDirty();
|
|
}
|
|
|
|
void RecurrenceRule::setWeekStart( short weekStart )
|
|
{
|
|
if ( isReadOnly() ) return;
|
|
mWeekStart = weekStart;
|
|
setDirty();
|
|
}
|
|
|
|
|
|
|
|
// Taken from recurrence.cpp
|
|
// int RecurrenceRule::maxIterations() const
|
|
// {
|
|
// /* Find the maximum number of iterations which may be needed to reach the
|
|
// * next actual occurrence of a monthly or yearly recurrence.
|
|
// * More than one iteration may be needed if, for example, it's the 29th February,
|
|
// * the 31st day of the month or the 5th Monday, and the month being checked is
|
|
// * February or a 30-day month.
|
|
// * The following recurrences may never occur:
|
|
// * - For rMonthlyDay: if the frequency is a whole number of years.
|
|
// * - For rMonthlyPos: if the frequency is an even whole number of years.
|
|
// * - For rYearlyDay, rYearlyMonth: if the frequeny is a multiple of 4 years.
|
|
// * - For rYearlyPos: if the frequency is an even number of years.
|
|
// * The maximum number of iterations needed, assuming that it does actually occur,
|
|
// * was found empirically.
|
|
// */
|
|
// switch (recurs) {
|
|
// case rMonthlyDay:
|
|
// return (rFreq % 12) ? 6 : 8;
|
|
//
|
|
// case rMonthlyPos:
|
|
// if (rFreq % 12 == 0) {
|
|
// // Some of these frequencies may never occur
|
|
// return (rFreq % 84 == 0) ? 364 // frequency = multiple of 7 years
|
|
// : (rFreq % 48 == 0) ? 7 // frequency = multiple of 4 years
|
|
// : (rFreq % 24 == 0) ? 14 : 28; // frequency = multiple of 2 or 1 year
|
|
// }
|
|
// // All other frequencies will occur sometime
|
|
// if (rFreq > 120)
|
|
// return 364; // frequencies of > 10 years will hit the date limit first
|
|
// switch (rFreq) {
|
|
// case 23: return 50;
|
|
// case 46: return 38;
|
|
// case 56: return 138;
|
|
// case 66: return 36;
|
|
// case 89: return 54;
|
|
// case 112: return 253;
|
|
// default: return 25; // most frequencies will need < 25 iterations
|
|
// }
|
|
//
|
|
// case rYearlyMonth:
|
|
// case rYearlyDay:
|
|
// return 8; // only 29th Feb or day 366 will need more than one iteration
|
|
//
|
|
// case rYearlyPos:
|
|
// if (rFreq % 7 == 0)
|
|
// return 364; // frequencies of a multiple of 7 years will hit the date limit first
|
|
// if (rFreq % 2 == 0) {
|
|
// // Some of these frequencies may never occur
|
|
// return (rFreq % 4 == 0) ? 7 : 14; // frequency = even number of years
|
|
// }
|
|
// return 28;
|
|
// }
|
|
// return 1;
|
|
// }
|
|
|
|
void RecurrenceRule::buildConstraints()
|
|
{
|
|
mTimedRepetition = 0;
|
|
mNoByRules = mBySetPos.isEmpty();
|
|
mConstraints.clear();
|
|
Constraint con;
|
|
if ( mWeekStart > 0 ) con.weekstart = mWeekStart;
|
|
mConstraints.append( con );
|
|
|
|
Constraint::List tmp;
|
|
Constraint::List::const_iterator it;
|
|
TQValueList<int>::const_iterator intit;
|
|
|
|
#define intConstraint( list, element ) \
|
|
if ( !list.isEmpty() ) { \
|
|
mNoByRules = false; \
|
|
for ( it = mConstraints.constBegin(); it != mConstraints.constEnd(); ++it ) { \
|
|
for ( intit = list.constBegin(); intit != list.constEnd(); ++intit ) { \
|
|
con = (*it); \
|
|
con.element = (*intit); \
|
|
tmp.append( con ); \
|
|
} \
|
|
} \
|
|
mConstraints = tmp; \
|
|
tmp.clear(); \
|
|
}
|
|
|
|
intConstraint( mBySeconds, second );
|
|
intConstraint( mByMinutes, minute );
|
|
intConstraint( mByHours, hour );
|
|
intConstraint( mByMonthDays, day );
|
|
intConstraint( mByMonths, month );
|
|
intConstraint( mByYearDays, yearday );
|
|
intConstraint( mByWeekNumbers, weeknumber );
|
|
#undef intConstraint
|
|
|
|
if ( !mByDays.isEmpty() ) {
|
|
mNoByRules = false;
|
|
for ( it = mConstraints.constBegin(); it != mConstraints.constEnd(); ++it ) {
|
|
TQValueList<WDayPos>::const_iterator dayit;
|
|
for ( dayit = mByDays.constBegin(); dayit != mByDays.constEnd(); ++dayit ) {
|
|
con = (*it);
|
|
con.weekday = (*dayit).day();
|
|
con.weekdaynr = (*dayit).pos();
|
|
tmp.append( con );
|
|
}
|
|
}
|
|
mConstraints = tmp;
|
|
tmp.clear();
|
|
}
|
|
|
|
#define fixConstraint( element, value ) \
|
|
{ \
|
|
tmp.clear(); \
|
|
for ( it = mConstraints.constBegin(); it != mConstraints.constEnd(); ++it ) { \
|
|
con = (*it); con.element = value; tmp.append( con ); \
|
|
} \
|
|
mConstraints = tmp; \
|
|
}
|
|
// Now determine missing values from DTSTART. This can speed up things,
|
|
// because we have more restrictions and save some loops.
|
|
|
|
// TODO: Does RFC 2445 intend to restrict the weekday in all cases of weekly?
|
|
if ( mPeriod == rWeekly && mByDays.isEmpty() ) {
|
|
fixConstraint( weekday, mDateStart.date().dayOfWeek() );
|
|
}
|
|
|
|
// Really fall through in the cases, because all smaller time intervals are
|
|
// constrained from dtstart
|
|
switch ( mPeriod ) {
|
|
case rYearly:
|
|
if ( mByDays.isEmpty() && mByWeekNumbers.isEmpty() && mByYearDays.isEmpty() && mByMonths.isEmpty() ) {
|
|
fixConstraint( month, mDateStart.date().month() );
|
|
}
|
|
case rMonthly:
|
|
if ( mByDays.isEmpty() && mByWeekNumbers.isEmpty() && mByYearDays.isEmpty() && mByMonthDays.isEmpty() ) {
|
|
fixConstraint( day, mDateStart.date().day() );
|
|
}
|
|
|
|
case rWeekly:
|
|
case rDaily:
|
|
if ( mByHours.isEmpty() ) {
|
|
fixConstraint( hour, mDateStart.time().hour() );
|
|
}
|
|
case rHourly:
|
|
if ( mByMinutes.isEmpty() ) {
|
|
fixConstraint( minute, mDateStart.time().minute() );
|
|
}
|
|
case rMinutely:
|
|
if ( mBySeconds.isEmpty() ) {
|
|
fixConstraint( second, mDateStart.time().second() );
|
|
}
|
|
case rSecondly:
|
|
default:
|
|
break;
|
|
}
|
|
#undef fixConstraint
|
|
|
|
if ( mNoByRules ) {
|
|
switch ( mPeriod ) {
|
|
case rHourly:
|
|
mTimedRepetition = mFrequency * 3600;
|
|
break;
|
|
case rMinutely:
|
|
mTimedRepetition = mFrequency * 60;
|
|
break;
|
|
case rSecondly:
|
|
mTimedRepetition = mFrequency;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
} else {
|
|
Constraint::List::Iterator conit = mConstraints.begin();
|
|
while ( conit != mConstraints.end() ) {
|
|
if ( (*conit).isConsistent( mPeriod ) ) {
|
|
++conit;
|
|
} else {
|
|
conit = mConstraints.remove( conit );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
bool RecurrenceRule::buildCache() const
|
|
{
|
|
kdDebug(5800) << " RecurrenceRule::buildCache: " << endl;
|
|
// Build the list of all occurrences of this event (we need that to determine
|
|
// the end date!)
|
|
Constraint interval( getNextValidDateInterval( startDt(), recurrenceType() ) );
|
|
TQDateTime next;
|
|
|
|
DateTimeList dts = datesForInterval( interval, recurrenceType() );
|
|
DateTimeList::Iterator it = dts.begin();
|
|
// Only use dates after the event has started (start date is only included
|
|
// if it matches)
|
|
while ( it != dts.end() ) {
|
|
if ( (*it) < startDt() ) it = dts.remove( it );
|
|
else ++it;
|
|
}
|
|
// dts.prepend( startDt() ); // the start date is always the first occurrence
|
|
|
|
|
|
int loopnr = 0;
|
|
int dtnr = dts.count();
|
|
// some validity checks to avoid infinite loops (i.e. if we have
|
|
// done this loop already 10000 times and found no occurrence, bail out )
|
|
while ( loopnr < 10000 && dtnr < mDuration ) {
|
|
interval.increase( recurrenceType(), frequency() );
|
|
// The returned date list is already sorted!
|
|
dts += datesForInterval( interval, recurrenceType() );
|
|
dtnr = dts.count();
|
|
++loopnr;
|
|
}
|
|
if ( int(dts.count()) > mDuration ) {
|
|
// we have picked up more occurrences than necessary, remove them
|
|
it = dts.at( mDuration );
|
|
while ( it != dts.end() ) it = dts.remove( it );
|
|
}
|
|
mCached = true;
|
|
mCachedDates = dts;
|
|
|
|
kdDebug(5800) << " Finished Building Cache, cache has " << dts.count() << " entries:" << endl;
|
|
// it = dts.begin();
|
|
// while ( it != dts.end() ) {
|
|
// kdDebug(5800) << " -=> " << (*it) << endl;
|
|
// ++it;
|
|
// }
|
|
if ( int(dts.count()) == mDuration ) {
|
|
mCachedDateEnd = dts.last();
|
|
return true;
|
|
} else {
|
|
mCachedDateEnd = TQDateTime();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
bool RecurrenceRule::dateMatchesRules( const TQDateTime &qdt ) const
|
|
{
|
|
bool match = false;
|
|
for ( Constraint::List::ConstIterator it = mConstraints.begin();
|
|
it!=mConstraints.end(); ++it ) {
|
|
match = match || ( (*it).matches( qdt, recurrenceType() ) );
|
|
}
|
|
return match;
|
|
}
|
|
|
|
bool RecurrenceRule::recursOn( const TQDate &qd ) const
|
|
{
|
|
int i, iend;
|
|
if ( doesFloat() ) {
|
|
// It's a date-only rule, so it has no time specification.
|
|
if ( qd < mDateStart.date() ) {
|
|
return false;
|
|
}
|
|
// Start date is only included if it really matches
|
|
TQDate endDate;
|
|
if ( 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 = mConstraints.count(); i < iend && !match; ++i ) {
|
|
match = mConstraints[i].matches( qd, recurrenceType() );
|
|
}
|
|
if ( !match ) {
|
|
return false;
|
|
}
|
|
|
|
TQDateTime start( qd, TQTime( 0, 0, 0 ) );
|
|
Constraint interval( 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!
|
|
TQDateTime end = start.addDays(1);
|
|
do {
|
|
DateTimeList dts = 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.
|
|
TQDateTime start( qd, TQTime( 0, 0, 0 ) );
|
|
TQDateTime end = start.addDays( 1 );
|
|
if ( end < mDateStart ) {
|
|
return false;
|
|
}
|
|
if ( start < mDateStart ) {
|
|
start = mDateStart;
|
|
}
|
|
|
|
// Start date is only included if it really matches
|
|
if ( mDuration >= 0 ) {
|
|
TQDateTime 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 ( mTimedRepetition ) {
|
|
// It's a simple sub-daily recurrence with no constraints
|
|
int n = static_cast<int>( ( mDateStart.secsTo( start ) - 1 ) % mTimedRepetition );
|
|
return start.addSecs( mTimedRepetition - n ) < end;
|
|
}
|
|
|
|
// Find the start and end dates in the time spec for the rule
|
|
TQDate startDay = start.date();
|
|
TQDate 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 = mConstraints.count(); i < iend && !match; ++i ) {
|
|
match = mConstraints[i].matches( startDay, recurrenceType() );
|
|
for ( int day = 1; day < dayCount && !match; ++day ) {
|
|
match = mConstraints[i].matches( startDay.addDays( day ), recurrenceType() );
|
|
}
|
|
}
|
|
if ( !match ) {
|
|
return false;
|
|
}
|
|
|
|
Constraint interval( 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 = datesForInterval( interval, recurrenceType() );
|
|
int i = findGE( dts, start, 0 );
|
|
if ( i >= 0 ) {
|
|
return dts[i] < end;
|
|
}
|
|
interval.increase( recurrenceType(), frequency() );
|
|
} while ( interval.intervalDateTime( recurrenceType() ) < end );
|
|
|
|
return false;
|
|
}
|
|
|
|
bool RecurrenceRule::recursAt( const TQDateTime &dt ) const
|
|
{
|
|
if ( doesFloat() ) {
|
|
return recursOn( dt.date() );
|
|
}
|
|
if ( dt < mDateStart ) {
|
|
return false;
|
|
}
|
|
// Start date is only included if it really matches
|
|
if ( mDuration >= 0 && dt > endDt() ) {
|
|
return false;
|
|
}
|
|
|
|
if ( mTimedRepetition ) {
|
|
// It's a simple sub-daily recurrence with no constraints
|
|
return !( mDateStart.secsTo( dt ) % 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( getNextValidDateInterval( dt, recurrenceType() ) );
|
|
// TODO_Recurrence: Does this work with BySetPos???
|
|
if ( interval.matches( dt, recurrenceType() ) ) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
TimeList RecurrenceRule::recurTimesOn( const TQDate &date ) const
|
|
{
|
|
TimeList lst;
|
|
if ( doesFloat() ) {
|
|
return lst;
|
|
}
|
|
TQDateTime start( date, TQTime( 0, 0, 0 ) );
|
|
TQDateTime 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].time();
|
|
}
|
|
return lst;
|
|
}
|
|
|
|
/** Returns the number of recurrences up to and including the date/time specified. */
|
|
int RecurrenceRule::durationTo( const TQDateTime &dt ) const
|
|
{
|
|
// kdDebug(5800) << " RecurrenceRule::durationTo: " << dt << endl;
|
|
// Easy cases: either before start, or after all recurrences and we know
|
|
// their number
|
|
if ( dt < startDt() ) return 0;
|
|
// Start date is only included if it really matches
|
|
// if ( dt == startDt() ) return 1;
|
|
if ( mDuration > 0 && dt >= endDt() ) return mDuration;
|
|
|
|
TQDateTime next( startDt() );
|
|
int found = 0;
|
|
while ( next.isValid() && next <= dt ) {
|
|
++found;
|
|
next = getNextDate( next );
|
|
}
|
|
return found;
|
|
}
|
|
|
|
|
|
TQDateTime RecurrenceRule::getPreviousDate( const TQDateTime& afterDate ) const
|
|
{
|
|
// kdDebug(5800) << " RecurrenceRule::getPreviousDate: " << afterDate << endl;
|
|
// Beyond end of recurrence
|
|
if ( afterDate < startDt() )
|
|
return TQDateTime();
|
|
|
|
// If we have a cache (duration given), use that
|
|
TQDateTime prev;
|
|
if ( mDuration > 0 ) {
|
|
if ( !mCached ) buildCache();
|
|
DateTimeList::ConstIterator it = mCachedDates.begin();
|
|
while ( it != mCachedDates.end() && (*it) < afterDate ) {
|
|
prev = *it;
|
|
++it;
|
|
}
|
|
if ( prev.isValid() && prev < afterDate ) return prev;
|
|
else return TQDateTime();
|
|
}
|
|
|
|
// kdDebug(5800) << " getNext date after " << preDate << endl;
|
|
prev = afterDate;
|
|
if ( mDuration >= 0 && endDt().isValid() && afterDate > endDt() )
|
|
prev = endDt().addSecs( 1 );
|
|
|
|
Constraint interval( getPreviousValidDateInterval( prev, recurrenceType() ) );
|
|
// kdDebug(5800) << "Previous Valid Date Interval for date " << prev << ": " << endl;
|
|
// interval.dump();
|
|
DateTimeList dts = datesForInterval( interval, recurrenceType() );
|
|
DateTimeList::Iterator dtit = dts.end();
|
|
if ( dtit != dts.begin() ) {
|
|
do {
|
|
--dtit;
|
|
} while ( dtit != dts.begin() && (*dtit) >= prev );
|
|
if ( (*dtit) < prev ) {
|
|
if ( (*dtit) >= startDt() ) return (*dtit);
|
|
else return TQDateTime();
|
|
}
|
|
}
|
|
|
|
// Previous interval. As soon as we find an occurrence, we're done.
|
|
while ( interval.intervalDateTime( recurrenceType() ) > startDt() ) {
|
|
interval.increase( recurrenceType(), -frequency() );
|
|
// kdDebug(5800) << "Decreased interval: " << endl;
|
|
// interval.dump();
|
|
// The returned date list is sorted
|
|
DateTimeList dts = datesForInterval( interval, recurrenceType() );
|
|
// The list is sorted, so take the last one.
|
|
if ( dts.count() > 0 ) {
|
|
prev = dts.last();
|
|
if ( prev.isValid() && prev >= startDt() ) return prev;
|
|
else return TQDateTime();
|
|
}
|
|
}
|
|
return TQDateTime();
|
|
}
|
|
|
|
|
|
TQDateTime RecurrenceRule::getNextDate( const TQDateTime &preDate ) const
|
|
{
|
|
// kdDebug(5800) << " RecurrenceRule::getNextDate: " << preDate << endl;
|
|
// Beyond end of recurrence
|
|
if ( mDuration >= 0 && endDt().isValid() && preDate >= endDt() )
|
|
return TQDateTime();
|
|
|
|
// Start date is only included if it really matches
|
|
TQDateTime adjustedPreDate;
|
|
if ( preDate < startDt() )
|
|
adjustedPreDate = startDt().addSecs( -1 );
|
|
else
|
|
adjustedPreDate = preDate;
|
|
|
|
if ( mDuration > 0 ) {
|
|
if ( !mCached ) buildCache();
|
|
DateTimeList::ConstIterator it = mCachedDates.begin();
|
|
while ( it != mCachedDates.end() && (*it) <= adjustedPreDate ) ++it;
|
|
if ( it != mCachedDates.end() ) {
|
|
// kdDebug(5800) << " getNext date after " << adjustedPreDate << ", cached date: " << *it << endl;
|
|
return (*it);
|
|
}
|
|
}
|
|
|
|
// kdDebug(5800) << " getNext date after " << adjustedPreDate << endl;
|
|
Constraint interval( getNextValidDateInterval( adjustedPreDate, recurrenceType() ) );
|
|
DateTimeList dts = datesForInterval( interval, recurrenceType() );
|
|
DateTimeList::Iterator dtit = dts.begin();
|
|
while ( dtit != dts.end() && (*dtit) <= adjustedPreDate ) ++dtit;
|
|
if ( dtit != dts.end() ) {
|
|
if ( mDuration >= 0 && (*dtit) > endDt() ) return TQDateTime();
|
|
else return (*dtit);
|
|
}
|
|
|
|
// Increase the interval. The first occurrence that we find is the result (if
|
|
// if's before the end date).
|
|
// TODO: some validity checks to avoid infinite loops for contradictory constraints
|
|
int loopnr = 0;
|
|
while ( loopnr < 10000 ) {
|
|
interval.increase( recurrenceType(), frequency() );
|
|
DateTimeList dts = datesForInterval( interval, recurrenceType() );
|
|
if ( dts.count() > 0 ) {
|
|
TQDateTime ret( dts.first() );
|
|
if ( mDuration >= 0 && ret > endDt() ) return TQDateTime();
|
|
else return ret;
|
|
}
|
|
++loopnr;
|
|
}
|
|
return TQDateTime();
|
|
}
|
|
|
|
DateTimeList RecurrenceRule::timesInInterval( const TQDateTime &dtStart,
|
|
const TQDateTime &dtEnd ) const
|
|
{
|
|
TQDateTime start = dtStart;
|
|
TQDateTime end = dtEnd;
|
|
DateTimeList result;
|
|
if ( end < mDateStart ) {
|
|
return result; // before start of recurrence
|
|
}
|
|
TQDateTime enddt = end;
|
|
if ( mDuration >= 0 ) {
|
|
TQDateTime endRecur = endDt();
|
|
if ( endRecur.isValid() ) {
|
|
if ( start > endRecur ) {
|
|
return result; // beyond end of recurrence
|
|
}
|
|
if ( end > endRecur ) {
|
|
enddt = endRecur; // limit end time to end of recurrence rule
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( mTimedRepetition ) {
|
|
// It's a simple sub-daily recurrence with no constraints
|
|
int n = static_cast<int>( ( mDateStart.secsTo( start ) - 1 ) % mTimedRepetition );
|
|
TQDateTime dt = start.addSecs( mTimedRepetition - n );
|
|
if ( dt < enddt ) {
|
|
n = static_cast<int>( ( dt.secsTo( enddt ) - 1 ) / mTimedRepetition ) + 1;
|
|
// limit n by a sane value else we can "explode".
|
|
n = QMIN( n, LOOP_LIMIT );
|
|
for ( int i = 0; i < n; dt = dt.addSecs( mTimedRepetition ), ++i ) {
|
|
result += dt;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
TQDateTime st = start;
|
|
bool done = false;
|
|
if ( mDuration > 0 ) {
|
|
if ( !mCached ) {
|
|
buildCache();
|
|
}
|
|
if ( mCachedDateEnd.isValid() && start > mCachedDateEnd ) {
|
|
return result; // beyond end of recurrence
|
|
}
|
|
int i = findGE( mCachedDates, start, 0 );
|
|
if ( i >= 0 ) {
|
|
int iend = findGT( mCachedDates, enddt, i );
|
|
if ( iend < 0 ) {
|
|
iend = mCachedDates.count();
|
|
} else {
|
|
done = true;
|
|
}
|
|
while ( i < iend ) {
|
|
result += mCachedDates[i++];
|
|
}
|
|
}
|
|
if ( mCachedDateEnd.isValid() ) {
|
|
done = true;
|
|
} else if ( !result.isEmpty() ) {
|
|
result += TQDateTime(); // indicate that the returned list is incomplete
|
|
done = true;
|
|
}
|
|
if ( done ) {
|
|
return result;
|
|
}
|
|
// We don't have any result yet, but we reached the end of the incomplete cache
|
|
st = mCachedLastDate.addSecs( 1 );
|
|
}
|
|
|
|
Constraint interval( getNextValidDateInterval( st, recurrenceType() ) );
|
|
int loop = 0;
|
|
do {
|
|
DateTimeList dts = datesForInterval( interval, recurrenceType() );
|
|
int i = 0;
|
|
int iend = dts.count();
|
|
if ( loop == 0 ) {
|
|
i = findGE( dts, st, 0 );
|
|
if ( i < 0 ) {
|
|
i = iend;
|
|
}
|
|
}
|
|
int j = findGT( dts, enddt, i );
|
|
if ( j >= 0 ) {
|
|
iend = j;
|
|
loop = LOOP_LIMIT;
|
|
}
|
|
while ( i < iend ) {
|
|
result += dts[i++];
|
|
}
|
|
// Increase the interval.
|
|
interval.increase( recurrenceType(), frequency() );
|
|
} while ( ++loop < LOOP_LIMIT &&
|
|
interval.intervalDateTime( recurrenceType() ) < end );
|
|
return result;
|
|
}
|
|
|
|
RecurrenceRule::Constraint RecurrenceRule::getPreviousValidDateInterval( const TQDateTime &preDate, PeriodType type ) const
|
|
{
|
|
// kdDebug(5800) << " (o) getPreviousValidDateInterval after " << preDate << ", type=" << type << endl;
|
|
long periods = 0;
|
|
TQDateTime nextValid = startDt();
|
|
TQDateTime start = startDt();
|
|
int modifier = 1;
|
|
TQDateTime toDate( preDate );
|
|
// for super-daily recurrences, don't care about the time part
|
|
|
|
// Find the #intervals since the dtstart and round to the next multiple of
|
|
// the frequency
|
|
// FIXME: All sub-daily periods need to convert to UTC, do the calculations
|
|
// in UTC, then convert back to the local time zone. Otherwise,
|
|
// recurrences across DST changes will be determined wrongly
|
|
switch ( type ) {
|
|
// Really fall through for sub-daily, since the calculations only differ
|
|
// by the factor 60 and 60*60! Same for weekly and daily (factor 7)
|
|
case rHourly: modifier *= 60;
|
|
case rMinutely: modifier *= 60;
|
|
case rSecondly:
|
|
periods = ownSecsTo( start, toDate ) / modifier;
|
|
// round it down to the next lower multiple of frequency():
|
|
periods = ( periods / frequency() ) * frequency();
|
|
nextValid = start.addSecs( modifier * periods );
|
|
break;
|
|
|
|
case rWeekly:
|
|
toDate = toDate.addDays( -(7 + toDate.date().dayOfWeek() - mWeekStart) % 7 );
|
|
start = start.addDays( -(7 + start.date().dayOfWeek() - mWeekStart) % 7 );
|
|
modifier *= 7;
|
|
case rDaily:
|
|
periods = start.daysTo( toDate ) / modifier;
|
|
// round it down to the next lower multiple of frequency():
|
|
periods = ( periods / frequency() ) * frequency();
|
|
nextValid = start.addDays( modifier * periods );
|
|
break;
|
|
|
|
case rMonthly: {
|
|
periods = 12*( toDate.date().year() - start.date().year() ) +
|
|
( toDate.date().month() - start.date().month() );
|
|
// round it down to the next lower multiple of frequency():
|
|
periods = ( periods / frequency() ) * frequency();
|
|
// set the day to the first day of the month, so we don't have problems
|
|
// with non-existent days like Feb 30 or April 31
|
|
start.setDate( TQDate( start.date().year(), start.date().month(), 1 ) );
|
|
nextValid.setDate( start.date().addMonths( periods ) );
|
|
break; }
|
|
case rYearly:
|
|
periods = ( toDate.date().year() - start.date().year() );
|
|
// round it down to the next lower multiple of frequency():
|
|
periods = ( periods / frequency() ) * frequency();
|
|
nextValid.setDate( start.date().addYears( periods ) );
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
// kdDebug(5800) << " ~~~> date in previous interval is: : " << nextValid << endl;
|
|
|
|
return Constraint( nextValid, type, mWeekStart );
|
|
}
|
|
|
|
RecurrenceRule::Constraint RecurrenceRule::getNextValidDateInterval( const TQDateTime &preDate, PeriodType type ) const
|
|
{
|
|
// TODO: Simplify this!
|
|
kdDebug(5800) << " (o) getNextValidDateInterval after " << preDate << ", type=" << type << endl;
|
|
long periods = 0;
|
|
TQDateTime start = startDt();
|
|
TQDateTime nextValid( start );
|
|
int modifier = 1;
|
|
TQDateTime toDate( preDate );
|
|
// for super-daily recurrences, don't care about the time part
|
|
|
|
// Find the #intervals since the dtstart and round to the next multiple of
|
|
// the frequency
|
|
// FIXME: All sub-daily periods need to convert to UTC, do the calculations
|
|
// in UTC, then convert back to the local time zone. Otherwise,
|
|
// recurrences across DST changes will be determined wrongly
|
|
switch ( type ) {
|
|
// Really fall through for sub-daily, since the calculations only differ
|
|
// by the factor 60 and 60*60! Same for weekly and daily (factor 7)
|
|
case rHourly: modifier *= 60;
|
|
case rMinutely: modifier *= 60;
|
|
case rSecondly:
|
|
periods = ownSecsTo( start, toDate ) / modifier;
|
|
periods = QMAX( 0, periods);
|
|
if ( periods > 0 )
|
|
periods += ( frequency() - 1 - ( (periods - 1) % frequency() ) );
|
|
nextValid = start.addSecs( modifier * periods );
|
|
break;
|
|
|
|
case rWeekly:
|
|
// correct both start date and current date to start of week
|
|
toDate = toDate.addDays( -(7 + toDate.date().dayOfWeek() - mWeekStart) % 7 );
|
|
start = start.addDays( -(7 + start.date().dayOfWeek() - mWeekStart) % 7 );
|
|
modifier *= 7;
|
|
case rDaily:
|
|
periods = start.daysTo( toDate ) / modifier;
|
|
periods = QMAX( 0, periods);
|
|
if ( periods > 0 )
|
|
periods += (frequency() - 1 - ( (periods - 1) % frequency() ) );
|
|
nextValid = start.addDays( modifier * periods );
|
|
break;
|
|
|
|
case rMonthly: {
|
|
periods = 12*( toDate.date().year() - start.date().year() ) +
|
|
( toDate.date().month() - start.date().month() );
|
|
periods = QMAX( 0, periods);
|
|
if ( periods > 0 )
|
|
periods += (frequency() - 1 - ( (periods - 1) % frequency() ) );
|
|
// set the day to the first day of the month, so we don't have problems
|
|
// with non-existent days like Feb 30 or April 31
|
|
start.setDate( TQDate( start.date().year(), start.date().month(), 1 ) );
|
|
nextValid.setDate( start.date().addMonths( periods ) );
|
|
break; }
|
|
case rYearly:
|
|
periods = ( toDate.date().year() - start.date().year() );
|
|
periods = QMAX( 0, periods);
|
|
if ( periods > 0 )
|
|
periods += ( frequency() - 1 - ( (periods - 1) % frequency() ) );
|
|
nextValid.setDate( start.date().addYears( periods ) );
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
// kdDebug(5800) << " ~~~> date in next interval is: : " << nextValid << endl;
|
|
|
|
return Constraint( nextValid, type, mWeekStart );
|
|
}
|
|
|
|
bool RecurrenceRule::mergeIntervalConstraint( Constraint *merged,
|
|
const Constraint &conit, const Constraint &interval ) const
|
|
{
|
|
Constraint result( interval );
|
|
|
|
#define mergeConstraint( name, cmparison ) \
|
|
if ( conit.name cmparison ) { \
|
|
if ( !(result.name cmparison) || result.name == conit.name ) { \
|
|
result.name = conit.name; \
|
|
} else return false;\
|
|
}
|
|
|
|
mergeConstraint( year, > 0 );
|
|
mergeConstraint( month, > 0 );
|
|
mergeConstraint( day, != 0 );
|
|
mergeConstraint( hour, >= 0 );
|
|
mergeConstraint( minute, >= 0 );
|
|
mergeConstraint( second, >= 0 );
|
|
|
|
mergeConstraint( weekday, != 0 );
|
|
mergeConstraint( weekdaynr, != 0 );
|
|
mergeConstraint( weeknumber, != 0 );
|
|
mergeConstraint( yearday, != 0 );
|
|
|
|
#undef mergeConstraint
|
|
if ( merged ) *merged = result;
|
|
return true;
|
|
}
|
|
|
|
|
|
DateTimeList RecurrenceRule::datesForInterval( const Constraint &interval, PeriodType type ) const
|
|
{
|
|
/* -) Loop through constraints,
|
|
-) merge interval with each constraint
|
|
-) if merged constraint is not consistent => ignore that constraint
|
|
-) if complete => add that one date to the date list
|
|
-) Loop through all missing fields => For each add the resulting
|
|
*/
|
|
// kdDebug(5800) << " RecurrenceRule::datesForInterval: " << endl;
|
|
// interval.dump();
|
|
DateTimeList lst;
|
|
Constraint::List::ConstIterator conit = mConstraints.begin();
|
|
for ( ; conit != mConstraints.end(); ++conit ) {
|
|
Constraint merged;
|
|
bool mergeok = mergeIntervalConstraint( &merged, *conit, interval );
|
|
// If the information is incomplete, we can't use this constraint
|
|
if ( merged.year <= 0 || merged.hour < 0 || merged.minute < 0 || merged.second < 0 )
|
|
mergeok = false;
|
|
if ( mergeok ) {
|
|
// kdDebug(5800) << " -) merged constraint: " << endl;
|
|
// merged.dump();
|
|
// We have a valid constraint, so get all datetimes that match it andd
|
|
// append it to all date/times of this interval
|
|
DateTimeList lstnew = merged.dateTimes( type );
|
|
lst += lstnew;
|
|
}
|
|
}
|
|
// Sort it so we can apply the BySetPos. Also some logic relies on this being sorted
|
|
qSortUnique( lst );
|
|
|
|
|
|
/*if ( lst.isEmpty() ) {
|
|
kdDebug(5800) << " No Dates in Interval " << endl;
|
|
} else {
|
|
kdDebug(5800) << " Dates: " << endl;
|
|
for ( DateTimeList::Iterator it = lst.begin(); it != lst.end(); ++it ) {
|
|
kdDebug(5800)<< " -) " << (*it).toString() << endl;
|
|
}
|
|
kdDebug(5800) << " ---------------------" << endl;
|
|
}*/
|
|
if ( !mBySetPos.isEmpty() ) {
|
|
DateTimeList tmplst = lst;
|
|
lst.clear();
|
|
TQValueList<int>::ConstIterator it;
|
|
for ( it = mBySetPos.begin(); it != mBySetPos.end(); ++it ) {
|
|
int pos = *it;
|
|
if ( pos > 0 ) --pos;
|
|
if ( pos < 0 ) pos += tmplst.count();
|
|
if ( pos >= 0 && uint(pos) < tmplst.count() ) {
|
|
lst.append( tmplst[pos] );
|
|
}
|
|
}
|
|
qSortUnique( lst );
|
|
}
|
|
|
|
return lst;
|
|
}
|
|
|
|
|
|
void RecurrenceRule::dump() const
|
|
{
|
|
#ifndef NDEBUG
|
|
kdDebug(5800) << "RecurrenceRule::dump():" << endl;
|
|
if ( !mRRule.isEmpty() )
|
|
kdDebug(5800) << " RRULE=" << mRRule << endl;
|
|
kdDebug(5800) << " Read-Only: " << isReadOnly() <<
|
|
", dirty: " << mDirty << endl;
|
|
|
|
kdDebug(5800) << " Period type: " << recurrenceType() << ", frequency: " << frequency() << endl;
|
|
kdDebug(5800) << " #occurrences: " << duration() << endl;
|
|
kdDebug(5800) << " start date: " << startDt() <<", end date: " << endDt() << endl;
|
|
|
|
|
|
#define dumpByIntList(list,label) \
|
|
if ( !list.isEmpty() ) {\
|
|
TQStringList lst;\
|
|
for ( TQValueList<int>::ConstIterator it = list.begin();\
|
|
it != list.end(); ++it ) {\
|
|
lst.append( TQString::number( *it ) );\
|
|
}\
|
|
kdDebug(5800) << " " << label << lst.join(", ") << endl;\
|
|
}
|
|
dumpByIntList( mBySeconds, "BySeconds: " );
|
|
dumpByIntList( mByMinutes, "ByMinutes: " );
|
|
dumpByIntList( mByHours, "ByHours: " );
|
|
if ( !mByDays.isEmpty() ) {
|
|
TQStringList lst;
|
|
for ( TQValueList<WDayPos>::ConstIterator it = mByDays.begin();
|
|
it != mByDays.end(); ++it ) {
|
|
lst.append( ( ((*it).pos()!=0) ? TQString::number( (*it).pos() ) : "" ) +
|
|
DateHelper::dayName( (*it).day() ) );
|
|
}
|
|
kdDebug(5800) << " ByDays: " << lst.join(", ") << endl;
|
|
}
|
|
dumpByIntList( mByMonthDays, "ByMonthDays:" );
|
|
dumpByIntList( mByYearDays, "ByYearDays: " );
|
|
dumpByIntList( mByWeekNumbers,"ByWeekNr: " );
|
|
dumpByIntList( mByMonths, "ByMonths: " );
|
|
dumpByIntList( mBySetPos, "BySetPos: " );
|
|
#undef dumpByIntList
|
|
|
|
kdDebug(5800) << " Week start: " << DateHelper::dayName( mWeekStart ) << endl;
|
|
|
|
kdDebug(5800) << " Constraints:" << endl;
|
|
// dump constraints
|
|
for ( Constraint::List::ConstIterator it = mConstraints.begin();
|
|
it!=mConstraints.end(); ++it ) {
|
|
(*it).dump();
|
|
}
|
|
#endif
|
|
}
|
|
|
|
void RecurrenceRule::Constraint::dump() const
|
|
{
|
|
kdDebug(5800) << " ~> Y="<<year<<", M="<<month<<", D="<<day<<", H="<<hour<<", m="<<minute<<", S="<<second<<", wd="<<weekday<<",#wd="<<weekdaynr<<", #w="<<weeknumber<<", yd="<<yearday<<endl;
|
|
}
|