1 | /* |
2 | This file is part of libkcal. |
3 | |
4 | Copyright (c) 2005 Reinhold Kainhofer <reinhold@kainhofe.com> |
5 | Copyright (c) 2006-2008 David Jarvie <djarvie@kde.org> |
6 | |
7 | This library is free software; you can redistribute it and/or |
8 | modify it under the terms of the GNU Library General Public |
9 | License as published by the Free Software Foundation; either |
10 | version 2 of the License, or (at your option) any later version. |
11 | |
12 | This library is distributed in the hope that it will be useful, |
13 | but WITHOUT ANY WARRANTY; without even the implied warranty of |
14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
15 | Library General Public License for more details. |
16 | |
17 | You should have received a copy of the GNU Library General Public License |
18 | along with this library; see the file COPYING.LIB. If not, write to |
19 | the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, |
20 | Boston, MA 02110-1301, USA. |
21 | */ |
22 | |
23 | #include "recurrencerule.h" |
24 | |
25 | #include <kdebug.h> |
26 | #include <kglobal.h> |
27 | |
28 | #include <QtCore/QDateTime> |
29 | #include <QtCore/QList> |
30 | #include <QtCore/QStringList> |
31 | |
32 | #include <limits.h> |
33 | #include <math.h> |
34 | |
35 | using namespace KCal; |
36 | |
37 | // Maximum number of intervals to process |
38 | const int LOOP_LIMIT = 10000; |
39 | |
40 | static QString dumpTime( const KDateTime &dt ); // for debugging |
41 | |
42 | /*========================================================================= |
43 | = = |
44 | = IMPORTANT CODING NOTE: = |
45 | = = |
46 | = Recurrence handling code is time critical, especially for sub-daily = |
47 | = recurrences. For example, if getNextDate() is called repeatedly to = |
48 | = check all consecutive occurrences over a few years, on a slow machine = |
49 | = this could take many seconds to complete in the worst case. Simple = |
50 | = sub-daily recurrences are optimised by use of mTimedRepetition. = |
51 | = = |
52 | ==========================================================================*/ |
53 | |
54 | /************************************************************************** |
55 | * DateHelper * |
56 | **************************************************************************/ |
57 | //@cond PRIVATE |
58 | class DateHelper |
59 | { |
60 | public: |
61 | #ifndef NDEBUG |
62 | static QString dayName( short day ); |
63 | #endif |
64 | static QDate getNthWeek( int year, int weeknumber, short weekstart = 1 ); |
65 | static int weekNumbersInYear( int year, short weekstart = 1 ); |
66 | static int getWeekNumber( const QDate &date, short weekstart, int *year = 0 ); |
67 | static int getWeekNumberNeg( const QDate &date, short weekstart, int *year = 0 ); |
68 | // Convert to QDate, allowing for day < 0. |
69 | // month and day must be non-zero. |
70 | static QDate getDate( int year, int month, int day ) |
71 | { |
72 | if ( day >= 0 ) { |
73 | return QDate( year, month, day ); |
74 | } else { |
75 | if ( ++month > 12 ) { |
76 | month = 1; |
77 | ++year; |
78 | } |
79 | return QDate( year, month, 1 ).addDays( day ); |
80 | } |
81 | } |
82 | }; |
83 | |
84 | #ifndef NDEBUG |
85 | // TODO: Move to a general library / class, as we need the same in the iCal |
86 | // generator and in the xcal format |
87 | QString DateHelper::dayName( short day ) |
88 | { |
89 | switch ( day ) { |
90 | case 1: |
91 | return "MO" ; |
92 | case 2: |
93 | return "TU" ; |
94 | case 3: |
95 | return "WE" ; |
96 | case 4: |
97 | return "TH" ; |
98 | case 5: |
99 | return "FR" ; |
100 | case 6: |
101 | return "SA" ; |
102 | case 7: |
103 | return "SU" ; |
104 | default: |
105 | return "??" ; |
106 | } |
107 | } |
108 | #endif |
109 | |
110 | QDate DateHelper::getNthWeek( int year, int weeknumber, short weekstart ) |
111 | { |
112 | if ( weeknumber == 0 ) { |
113 | return QDate(); |
114 | } |
115 | |
116 | // Adjust this to the first day of week #1 of the year and add 7*weekno days. |
117 | QDate dt( year, 1, 4 ); // Week #1 is the week that contains Jan 4 |
118 | int adjust = -( 7 + dt.dayOfWeek() - weekstart ) % 7; |
119 | if ( weeknumber > 0 ) { |
120 | dt = dt.addDays( 7 * (weeknumber-1) + adjust ); |
121 | } else if ( weeknumber < 0 ) { |
122 | dt = dt.addYears( 1 ); |
123 | dt = dt.addDays( 7 * weeknumber + adjust ); |
124 | } |
125 | return dt; |
126 | } |
127 | |
128 | int DateHelper::getWeekNumber( const QDate &date, short weekstart, int *year ) |
129 | { |
130 | int y = date.year(); |
131 | QDate dt( y, 1, 4 ); // <= definitely in week #1 |
132 | dt = dt.addDays( -( 7 + dt.dayOfWeek() - weekstart ) % 7 ); // begin of week #1 |
133 | |
134 | int daysto = dt.daysTo( date ); |
135 | if ( daysto < 0 ) { |
136 | // in first week of year |
137 | --y; |
138 | dt = QDate( y, 1, 4 ); |
139 | dt = dt.addDays( -( 7 + dt.dayOfWeek() - weekstart ) % 7 ); // begin of week #1 |
140 | daysto = dt.daysTo( date ); |
141 | } else if ( daysto > 355 ) { |
142 | // near the end of the year - check if it's next year |
143 | QDate dtn( y+1, 1, 4 ); // <= definitely first week of next year |
144 | dtn = dtn.addDays( -( 7 + dtn.dayOfWeek() - weekstart ) % 7 ); |
145 | int dayston = dtn.daysTo( date ); |
146 | if ( dayston >= 0 ) { |
147 | // in first week of next year; |
148 | ++y; |
149 | daysto = dayston; |
150 | } |
151 | } |
152 | if ( year ) { |
153 | *year = y; |
154 | } |
155 | return daysto / 7 + 1; |
156 | } |
157 | |
158 | int DateHelper::weekNumbersInYear( int year, short weekstart ) |
159 | { |
160 | QDate dt( year, 1, weekstart ); |
161 | QDate dt1( year + 1, 1, weekstart ); |
162 | return dt.daysTo( dt1 ) / 7; |
163 | } |
164 | |
165 | // Week number from the end of the year |
166 | int DateHelper::getWeekNumberNeg( const QDate &date, short weekstart, int *year ) |
167 | { |
168 | int weekpos = getWeekNumber( date, weekstart, year ); |
169 | return weekNumbersInYear( *year, weekstart ) - weekpos - 1; |
170 | } |
171 | //@endcond |
172 | |
173 | /************************************************************************** |
174 | * Constraint * |
175 | **************************************************************************/ |
176 | //@cond PRIVATE |
177 | class Constraint |
178 | { |
179 | public: |
180 | typedef QList<Constraint> List; |
181 | |
182 | explicit Constraint( KDateTime::Spec, int wkst = 1 ); |
183 | Constraint( const KDateTime &dt, RecurrenceRule::PeriodType type, int wkst ); |
184 | void clear(); |
185 | void setYear( int n ) |
186 | { |
187 | year = n; |
188 | useCachedDt = false; |
189 | } |
190 | void setMonth( int n ) |
191 | { |
192 | month = n; |
193 | useCachedDt = false; |
194 | } |
195 | void setDay( int n ) |
196 | { |
197 | day = n; |
198 | useCachedDt = false; |
199 | } |
200 | void setHour( int n ) |
201 | { |
202 | hour = n; |
203 | useCachedDt = false; |
204 | } |
205 | void setMinute( int n ) |
206 | { |
207 | minute = n; |
208 | useCachedDt = false; |
209 | } |
210 | void setSecond( int n ) |
211 | { |
212 | second = n; |
213 | useCachedDt = false; |
214 | } |
215 | void setWeekday( int n ) |
216 | { |
217 | weekday = n; |
218 | useCachedDt = false; |
219 | } |
220 | void setWeekdaynr( int n ) |
221 | { |
222 | weekdaynr = n; |
223 | useCachedDt = false; |
224 | } |
225 | void setWeeknumber( int n ) |
226 | { |
227 | weeknumber = n; |
228 | useCachedDt = false; |
229 | } |
230 | void setYearday( int n ) |
231 | { |
232 | yearday = n; |
233 | useCachedDt = false; |
234 | } |
235 | void setWeekstart( int n ) |
236 | { |
237 | weekstart = n; |
238 | useCachedDt = false; |
239 | } |
240 | void setSecondOccurrence( int n ) |
241 | { |
242 | secondOccurrence = n; |
243 | useCachedDt = false; |
244 | } |
245 | |
246 | int year; // 0 means unspecified |
247 | int month; // 0 means unspecified |
248 | int day; // 0 means unspecified |
249 | int hour; // -1 means unspecified |
250 | int minute; // -1 means unspecified |
251 | int second; // -1 means unspecified |
252 | int weekday; // 0 means unspecified |
253 | int weekdaynr; // index of weekday in month/year (0=unspecified) |
254 | int weeknumber; // 0 means unspecified |
255 | int yearday; // 0 means unspecified |
256 | int weekstart; // first day of week (1=monday, 7=sunday, 0=unspec.) |
257 | KDateTime::Spec timespec; // time zone etc. to use |
258 | bool secondOccurrence; // the time is the second occurrence during daylight savings shift |
259 | |
260 | bool readDateTime( const KDateTime &dt, RecurrenceRule::PeriodType type ); |
261 | bool matches( const QDate &dt, RecurrenceRule::PeriodType type ) const; |
262 | bool matches( const KDateTime &dt, RecurrenceRule::PeriodType type ) const; |
263 | bool merge( const Constraint &interval ); |
264 | bool isConsistent() const; |
265 | bool isConsistent( RecurrenceRule::PeriodType period ) const; |
266 | bool increase( RecurrenceRule::PeriodType type, int freq ); |
267 | KDateTime intervalDateTime( RecurrenceRule::PeriodType type ) const; |
268 | QList<KDateTime> dateTimes( RecurrenceRule::PeriodType type ) const; |
269 | void appendDateTime( const QDate &date, const QTime &time, QList<KDateTime> &list ) const; |
270 | void dump() const; |
271 | |
272 | private: |
273 | mutable bool useCachedDt; |
274 | mutable KDateTime cachedDt; |
275 | }; |
276 | |
277 | Constraint::Constraint( KDateTime::Spec spec, int wkst ) |
278 | : weekstart( wkst ), |
279 | timespec( spec ) |
280 | { |
281 | clear(); |
282 | } |
283 | |
284 | Constraint::Constraint( const KDateTime &dt, RecurrenceRule::PeriodType type, int wkst ) |
285 | : weekstart( wkst ), |
286 | timespec( dt.timeSpec() ) |
287 | { |
288 | clear(); |
289 | readDateTime( dt, type ); |
290 | } |
291 | |
292 | void Constraint::clear() |
293 | { |
294 | year = 0; |
295 | month = 0; |
296 | day = 0; |
297 | hour = -1; |
298 | minute = -1; |
299 | second = -1; |
300 | weekday = 0; |
301 | weekdaynr = 0; |
302 | weeknumber = 0; |
303 | yearday = 0; |
304 | secondOccurrence = false; |
305 | useCachedDt = false; |
306 | } |
307 | |
308 | bool Constraint::matches( const QDate &dt, RecurrenceRule::PeriodType type ) const |
309 | { |
310 | // If the event recurs in week 53 or 1, the day might not belong to the same |
311 | // year as the week it is in. E.g. Jan 1, 2005 is in week 53 of year 2004. |
312 | // So we can't simply check the year in that case! |
313 | if ( weeknumber == 0 ) { |
314 | if ( year > 0 && year != dt.year() ) { |
315 | return false; |
316 | } |
317 | } else { |
318 | int y; |
319 | if ( weeknumber > 0 && |
320 | weeknumber != DateHelper::getWeekNumber( dt, weekstart, &y ) ) { |
321 | return false; |
322 | } |
323 | if ( weeknumber < 0 && |
324 | weeknumber != DateHelper::getWeekNumberNeg( dt, weekstart, &y ) ) { |
325 | return false; |
326 | } |
327 | if ( year > 0 && year != y ) { |
328 | return false; |
329 | } |
330 | } |
331 | |
332 | if ( month > 0 && month != dt.month() ) { |
333 | return false; |
334 | } |
335 | if ( day > 0 && day != dt.day() ) { |
336 | return false; |
337 | } |
338 | if ( day < 0 && dt.day() != ( dt.daysInMonth() + day + 1 ) ) { |
339 | return false; |
340 | } |
341 | if ( weekday > 0 ) { |
342 | if ( weekday != dt.dayOfWeek() ) { |
343 | return false; |
344 | } |
345 | if ( weekdaynr != 0 ) { |
346 | // If it's a yearly recurrence and a month is given, the position is |
347 | // still in the month, not in the year. |
348 | if ( ( type == RecurrenceRule::rMonthly ) || |
349 | ( type == RecurrenceRule::rYearly && month > 0 ) ) { |
350 | // Monthly |
351 | if ( weekdaynr > 0 && |
352 | weekdaynr != ( dt.day() - 1 ) / 7 + 1 ) { |
353 | return false; |
354 | } |
355 | if ( weekdaynr < 0 && |
356 | weekdaynr != -( ( dt.daysInMonth() - dt.day() ) / 7 + 1 ) ) { |
357 | return false; |
358 | } |
359 | } else { |
360 | // Yearly |
361 | if ( weekdaynr > 0 && |
362 | weekdaynr != ( dt.dayOfYear() - 1 ) / 7 + 1 ) { |
363 | return false; |
364 | } |
365 | if ( weekdaynr < 0 && |
366 | weekdaynr != -( ( dt.daysInYear() - dt.dayOfYear() ) / 7 + 1 ) ) { |
367 | return false; |
368 | } |
369 | } |
370 | } |
371 | } |
372 | if ( yearday > 0 && yearday != dt.dayOfYear() ) { |
373 | return false; |
374 | } |
375 | if ( yearday < 0 && yearday != dt.daysInYear() - dt.dayOfYear() + 1 ) { |
376 | return false; |
377 | } |
378 | return true; |
379 | } |
380 | |
381 | /* Check for a match with the specified date/time. |
382 | * The date/time's time specification must correspond with that of the start date/time. |
383 | */ |
384 | bool Constraint::matches( const KDateTime &dt, RecurrenceRule::PeriodType type ) const |
385 | { |
386 | if ( ( hour >= 0 && ( hour != dt.time().hour() || |
387 | secondOccurrence != dt.isSecondOccurrence() ) ) || |
388 | ( minute >= 0 && minute != dt.time().minute() ) || |
389 | ( second >= 0 && second != dt.time().second() ) || |
390 | !matches( dt.date(), type ) ) { |
391 | return false; |
392 | } |
393 | return true; |
394 | } |
395 | |
396 | bool Constraint::isConsistent( RecurrenceRule::PeriodType /*period*/) const |
397 | { |
398 | // TODO: Check for consistency, e.g. byyearday=3 and bymonth=10 |
399 | return true; |
400 | } |
401 | |
402 | // Return a date/time set to the constraint values, but with those parts less |
403 | // significant than the given period type set to 1 (for dates) or 0 (for times). |
404 | KDateTime Constraint::intervalDateTime( RecurrenceRule::PeriodType type ) const |
405 | { |
406 | if ( useCachedDt ) { |
407 | return cachedDt; |
408 | } |
409 | QDate d; |
410 | QTime t( 0, 0, 0 ); |
411 | bool subdaily = true; |
412 | switch ( type ) { |
413 | case RecurrenceRule::rSecondly: |
414 | t.setHMS( hour, minute, second ); |
415 | break; |
416 | case RecurrenceRule::rMinutely: |
417 | t.setHMS( hour, minute, 0 ); |
418 | break; |
419 | case RecurrenceRule::rHourly: |
420 | t.setHMS( hour, 0, 0 ); |
421 | break; |
422 | case RecurrenceRule::rDaily: |
423 | break; |
424 | case RecurrenceRule::rWeekly: |
425 | d = DateHelper::getNthWeek( year, weeknumber, weekstart ); |
426 | subdaily = false; |
427 | break; |
428 | case RecurrenceRule::rMonthly: |
429 | d.setYMD( year, month, 1 ); |
430 | subdaily = false; |
431 | break; |
432 | case RecurrenceRule::rYearly: |
433 | d.setYMD( year, 1, 1 ); |
434 | subdaily = false; |
435 | break; |
436 | default: |
437 | break; |
438 | } |
439 | if ( subdaily ) { |
440 | d = DateHelper::getDate( year, (month>0)?month:1, day?day:1 ); |
441 | } |
442 | cachedDt = KDateTime( d, t, timespec ); |
443 | if ( secondOccurrence ) { |
444 | cachedDt.setSecondOccurrence( true ); |
445 | } |
446 | useCachedDt = true; |
447 | return cachedDt; |
448 | } |
449 | |
450 | bool Constraint::merge( const Constraint &interval ) |
451 | { |
452 | #define mergeConstraint( name, cmparison ) \ |
453 | if ( interval.name cmparison ) { \ |
454 | if ( !( name cmparison ) ) { \ |
455 | name = interval.name; \ |
456 | } else if ( name != interval.name ) { \ |
457 | return false;\ |
458 | } \ |
459 | } |
460 | |
461 | useCachedDt = false; |
462 | |
463 | mergeConstraint( year, > 0 ); |
464 | mergeConstraint( month, > 0 ); |
465 | mergeConstraint( day, != 0 ); |
466 | mergeConstraint( hour, >= 0 ); |
467 | mergeConstraint( minute, >= 0 ); |
468 | mergeConstraint( second, >= 0 ); |
469 | |
470 | mergeConstraint( weekday, != 0 ); |
471 | mergeConstraint( weekdaynr, != 0 ); |
472 | mergeConstraint( weeknumber, != 0 ); |
473 | mergeConstraint( yearday, != 0 ); |
474 | |
475 | #undef mergeConstraint |
476 | return true; |
477 | } |
478 | |
479 | // Y M D | H Mn S | WD #WD | WN | YD |
480 | // required: |
481 | // x | x x x | | | |
482 | // 0) Trivial: Exact date given, maybe other restrictions |
483 | // x x x | x x x | | | |
484 | // 1) Easy case: no weekly restrictions -> at most a loop through possible dates |
485 | // x + + | x x x | - - | - | - |
486 | // 2) Year day is given -> date known |
487 | // x | x x x | | | + |
488 | // 3) week number is given -> loop through all days of that week. Further |
489 | // restrictions will be applied in the end, when we check all dates for |
490 | // consistency with the constraints |
491 | // x | x x x | | + | (-) |
492 | // 4) week day is specified -> |
493 | // x | x x x | x ? | (-)| (-) |
494 | // 5) All possiblecases have already been treated, so this must be an error! |
495 | |
496 | QList<KDateTime> Constraint::dateTimes( RecurrenceRule::PeriodType type ) const |
497 | { |
498 | QList<KDateTime> result; |
499 | bool done = false; |
500 | if ( !isConsistent( type ) ) { |
501 | return result; |
502 | } |
503 | |
504 | // TODO_Recurrence: Handle all-day |
505 | QTime tm( hour, minute, second ); |
506 | |
507 | if ( !done && day && month > 0 ) { |
508 | appendDateTime( DateHelper::getDate( year, month, day ), tm, result ); |
509 | done = true; |
510 | } |
511 | |
512 | if ( !done && weekday == 0 && weeknumber == 0 && yearday == 0 ) { |
513 | // Easy case: date is given, not restrictions by week or yearday |
514 | uint mstart = ( month > 0 ) ? month : 1; |
515 | uint mend = ( month <= 0 ) ? 12 : month; |
516 | for ( uint m = mstart; m <= mend; ++m ) { |
517 | uint dstart, dend; |
518 | if ( day > 0 ) { |
519 | dstart = dend = day; |
520 | } else if ( day < 0 ) { |
521 | QDate date( year, month, 1 ); |
522 | dstart = dend = date.daysInMonth() + day + 1; |
523 | } else { |
524 | QDate date( year, month, 1 ); |
525 | dstart = 1; |
526 | dend = date.daysInMonth(); |
527 | } |
528 | uint d = dstart; |
529 | for ( QDate dt( year, m, dstart ); ; dt = dt.addDays( 1 ) ) { |
530 | appendDateTime( dt, tm, result ); |
531 | if ( ++d > dend ) { |
532 | break; |
533 | } |
534 | } |
535 | } |
536 | done = true; |
537 | } |
538 | |
539 | // Else: At least one of the week / yearday restrictions was given... |
540 | // If we have a yearday (and of course a year), we know the exact date |
541 | if ( !done && yearday != 0 ) { |
542 | // yearday < 0 means from end of year, so we'll need Jan 1 of the next year |
543 | QDate d( year + ( ( yearday > 0 ) ? 0 : 1 ), 1, 1 ); |
544 | d = d.addDays( yearday - ( ( yearday > 0 ) ? 1 : 0 ) ); |
545 | appendDateTime( d, tm, result ); |
546 | done = true; |
547 | } |
548 | |
549 | // Else: If we have a weeknumber, we have at most 7 possible dates, loop through them |
550 | if ( !done && weeknumber != 0 ) { |
551 | QDate wst( DateHelper::getNthWeek( year, weeknumber, weekstart ) ); |
552 | if ( weekday != 0 ) { |
553 | wst = wst.addDays( ( 7 + weekday - weekstart ) % 7 ); |
554 | appendDateTime( wst, tm, result ); |
555 | } else { |
556 | for ( int i = 0; i < 7; ++i ) { |
557 | appendDateTime( wst, tm, result ); |
558 | wst = wst.addDays( 1 ); |
559 | } |
560 | } |
561 | done = true; |
562 | } |
563 | |
564 | // weekday is given |
565 | if ( !done && weekday != 0 ) { |
566 | QDate dt( year, 1, 1 ); |
567 | // If type == yearly and month is given, pos is still in month not year! |
568 | // TODO_Recurrence: Correct handling of n-th BYDAY... |
569 | int maxloop = 53; |
570 | bool inMonth = ( type == RecurrenceRule::rMonthly ) || |
571 | ( type == RecurrenceRule::rYearly && month > 0 ); |
572 | if ( inMonth && month > 0 ) { |
573 | dt = QDate( year, month, 1 ); |
574 | maxloop = 5; |
575 | } |
576 | if ( weekdaynr < 0 ) { |
577 | // From end of period (month, year) => relative to begin of next period |
578 | if ( inMonth ) { |
579 | dt = dt.addMonths( 1 ); |
580 | } else { |
581 | dt = dt.addYears( 1 ); |
582 | } |
583 | } |
584 | int adj = ( 7 + weekday - dt.dayOfWeek() ) % 7; |
585 | dt = dt.addDays( adj ); // correct first weekday of the period |
586 | |
587 | if ( weekdaynr > 0 ) { |
588 | dt = dt.addDays( ( weekdaynr - 1 ) * 7 ); |
589 | appendDateTime( dt, tm, result ); |
590 | } else if ( weekdaynr < 0 ) { |
591 | dt = dt.addDays( weekdaynr * 7 ); |
592 | appendDateTime( dt, tm, result ); |
593 | } else { |
594 | // loop through all possible weeks, non-matching will be filtered later |
595 | for ( int i = 0; i < maxloop; ++i ) { |
596 | appendDateTime( dt, tm, result ); |
597 | dt = dt.addDays( 7 ); |
598 | } |
599 | } |
600 | } // weekday != 0 |
601 | |
602 | // Only use those times that really match all other constraints, too |
603 | QList<KDateTime> valid; |
604 | for ( int i = 0, iend = result.count(); i < iend; ++i ) { |
605 | if ( matches( result[i], type ) ) { |
606 | valid.append( result[i] ); |
607 | } |
608 | } |
609 | // Don't sort it here, would be unnecessary work. The results from all |
610 | // constraints will be merged to one big list of the interval. Sort that one! |
611 | return valid; |
612 | } |
613 | |
614 | void Constraint::appendDateTime( const QDate &date, const QTime &time, |
615 | QList<KDateTime> &list ) const |
616 | { |
617 | KDateTime dt( date, time, timespec ); |
618 | if ( dt.isValid() ) { |
619 | if ( secondOccurrence ) { |
620 | dt.setSecondOccurrence( true ); |
621 | } |
622 | list.append( dt ); |
623 | } |
624 | } |
625 | |
626 | bool Constraint::increase( RecurrenceRule::PeriodType type, int freq ) |
627 | { |
628 | // convert the first day of the interval to KDateTime |
629 | intervalDateTime( type ); |
630 | |
631 | // Now add the intervals |
632 | switch ( type ) { |
633 | case RecurrenceRule::rSecondly: |
634 | cachedDt = cachedDt.addSecs( freq ); |
635 | break; |
636 | case RecurrenceRule::rMinutely: |
637 | cachedDt = cachedDt.addSecs( 60 * freq ); |
638 | break; |
639 | case RecurrenceRule::rHourly: |
640 | cachedDt = cachedDt.addSecs( 3600 * freq ); |
641 | break; |
642 | case RecurrenceRule::rDaily: |
643 | cachedDt = cachedDt.addDays( freq ); |
644 | break; |
645 | case RecurrenceRule::rWeekly: |
646 | cachedDt = cachedDt.addDays( 7 * freq ); |
647 | break; |
648 | case RecurrenceRule::rMonthly: |
649 | cachedDt = cachedDt.addMonths( freq ); |
650 | break; |
651 | case RecurrenceRule::rYearly: |
652 | cachedDt = cachedDt.addYears( freq ); |
653 | break; |
654 | default: |
655 | break; |
656 | } |
657 | // Convert back from KDateTime to the Constraint class |
658 | readDateTime( cachedDt, type ); |
659 | useCachedDt = true; // readDateTime() resets this |
660 | |
661 | return true; |
662 | } |
663 | |
664 | // Set the constraint's value appropriate to 'type', to the value contained in a date/time. |
665 | bool Constraint::readDateTime( const KDateTime &dt, RecurrenceRule::PeriodType type ) |
666 | { |
667 | switch ( type ) { |
668 | // Really fall through! Only weekly needs to be treated differently! |
669 | case RecurrenceRule::rSecondly: |
670 | second = dt.time().second(); |
671 | case RecurrenceRule::rMinutely: |
672 | minute = dt.time().minute(); |
673 | case RecurrenceRule::rHourly: |
674 | hour = dt.time().hour(); |
675 | secondOccurrence = dt.isSecondOccurrence(); |
676 | case RecurrenceRule::rDaily: |
677 | day = dt.date().day(); |
678 | case RecurrenceRule::rMonthly: |
679 | month = dt.date().month(); |
680 | case RecurrenceRule::rYearly: |
681 | year = dt.date().year(); |
682 | break; |
683 | case RecurrenceRule::rWeekly: |
684 | // Determine start day of the current week, calculate the week number from that |
685 | weeknumber = DateHelper::getWeekNumber( dt.date(), weekstart, &year ); |
686 | break; |
687 | default: |
688 | break; |
689 | } |
690 | useCachedDt = false; |
691 | return true; |
692 | } |
693 | //@endcond |
694 | |
695 | /************************************************************************** |
696 | * RecurrenceRule::Private * |
697 | **************************************************************************/ |
698 | |
699 | //@cond PRIVATE |
700 | class KCal::RecurrenceRule::Private |
701 | { |
702 | public: |
703 | Private( RecurrenceRule *parent ) |
704 | : mParent( parent ), |
705 | mPeriod( rNone ), |
706 | mFrequency( 0 ), |
707 | mWeekStart( 1 ), |
708 | mIsReadOnly( false ), |
709 | mAllDay( false ) |
710 | {} |
711 | |
712 | Private( RecurrenceRule *parent, const Private &p ); |
713 | |
714 | Private &operator=( const Private &other ); |
715 | bool operator==( const Private &other ) const; |
716 | void clear(); |
717 | void setDirty(); |
718 | void buildConstraints(); |
719 | bool buildCache() const; |
720 | Constraint getNextValidDateInterval( const KDateTime &preDate, PeriodType type ) const; |
721 | Constraint getPreviousValidDateInterval( const KDateTime &afterDate, PeriodType type ) const; |
722 | DateTimeList datesForInterval( const Constraint &interval, PeriodType type ) const; |
723 | |
724 | RecurrenceRule *mParent; |
725 | QString mRRule; // RRULE string |
726 | PeriodType mPeriod; |
727 | KDateTime mDateStart; // start of recurrence (but mDateStart is not an occurrence |
728 | // unless it matches the rule) |
729 | uint mFrequency; |
730 | /** how often it recurs: |
731 | < 0 means no end date, |
732 | 0 means an explicit end date, |
733 | positive values give the number of occurrences */ |
734 | int mDuration; |
735 | KDateTime mDateEnd; |
736 | |
737 | QList<int> mBySeconds; // values: second 0-59 |
738 | QList<int> mByMinutes; // values: minute 0-59 |
739 | QList<int> mByHours; // values: hour 0-23 |
740 | |
741 | QList<WDayPos> mByDays; // n-th weekday of the month or year |
742 | QList<int> mByMonthDays; // values: day -31 to -1 and 1-31 |
743 | QList<int> mByYearDays; // values: day -366 to -1 and 1-366 |
744 | QList<int> mByWeekNumbers; // values: week -53 to -1 and 1-53 |
745 | QList<int> mByMonths; // values: month 1-12 |
746 | QList<int> mBySetPos; // values: position -366 to -1 and 1-366 |
747 | short mWeekStart; // first day of the week (1=Monday, 7=Sunday) |
748 | |
749 | Constraint::List mConstraints; |
750 | QList<RuleObserver*> mObservers; |
751 | |
752 | // Cache for duration |
753 | mutable DateTimeList mCachedDates; |
754 | mutable KDateTime mCachedDateEnd; |
755 | mutable KDateTime mCachedLastDate; // when mCachedDateEnd invalid, last date checked |
756 | mutable bool mCached; |
757 | |
758 | bool mIsReadOnly; |
759 | bool mAllDay; |
760 | bool mNoByRules; // no BySeconds, ByMinutes, ... rules exist |
761 | uint mTimedRepetition; // repeats at a regular number of seconds interval, or 0 |
762 | }; |
763 | |
764 | RecurrenceRule::Private::Private( RecurrenceRule *parent, const Private &p ) |
765 | : mParent( parent ), |
766 | mRRule( p.mRRule ), |
767 | mPeriod( p.mPeriod ), |
768 | mDateStart( p.mDateStart ), |
769 | mFrequency( p.mFrequency ), |
770 | mDuration( p.mDuration ), |
771 | mDateEnd( p.mDateEnd ), |
772 | |
773 | mBySeconds( p.mBySeconds ), |
774 | mByMinutes( p.mByMinutes ), |
775 | mByHours( p.mByHours ), |
776 | mByDays( p.mByDays ), |
777 | mByMonthDays( p.mByMonthDays ), |
778 | mByYearDays( p.mByYearDays ), |
779 | mByWeekNumbers( p.mByWeekNumbers ), |
780 | mByMonths( p.mByMonths ), |
781 | mBySetPos( p.mBySetPos ), |
782 | mWeekStart( p.mWeekStart ), |
783 | |
784 | mIsReadOnly( p.mIsReadOnly ), |
785 | mAllDay( p.mAllDay ) |
786 | { |
787 | setDirty(); |
788 | } |
789 | |
790 | RecurrenceRule::Private &RecurrenceRule::Private::operator=( const Private &p ) |
791 | { |
792 | // check for self assignment |
793 | if ( &p == this ) { |
794 | return *this; |
795 | } |
796 | |
797 | mRRule = p.mRRule; |
798 | mPeriod = p.mPeriod; |
799 | mDateStart = p.mDateStart; |
800 | mFrequency = p.mFrequency; |
801 | mDuration = p.mDuration; |
802 | mDateEnd = p.mDateEnd; |
803 | |
804 | mBySeconds = p.mBySeconds; |
805 | mByMinutes = p.mByMinutes; |
806 | mByHours = p.mByHours; |
807 | mByDays = p.mByDays; |
808 | mByMonthDays = p.mByMonthDays; |
809 | mByYearDays = p.mByYearDays; |
810 | mByWeekNumbers = p.mByWeekNumbers; |
811 | mByMonths = p.mByMonths; |
812 | mBySetPos = p.mBySetPos; |
813 | mWeekStart = p.mWeekStart; |
814 | |
815 | mIsReadOnly = p.mIsReadOnly; |
816 | mAllDay = p.mAllDay; |
817 | |
818 | setDirty(); |
819 | |
820 | return *this; |
821 | } |
822 | |
823 | bool RecurrenceRule::Private::operator==( const Private &r ) const |
824 | { |
825 | return |
826 | mPeriod == r.mPeriod && |
827 | mDateStart == r.mDateStart && |
828 | mDuration == r.mDuration && |
829 | mDateEnd == r.mDateEnd && |
830 | mFrequency == r.mFrequency && |
831 | mIsReadOnly == r.mIsReadOnly && |
832 | mAllDay == r.mAllDay && |
833 | mBySeconds == r.mBySeconds && |
834 | mByMinutes == r.mByMinutes && |
835 | mByHours == r.mByHours && |
836 | mByDays == r.mByDays && |
837 | mByMonthDays == r.mByMonthDays && |
838 | mByYearDays == r.mByYearDays && |
839 | mByWeekNumbers == r.mByWeekNumbers && |
840 | mByMonths == r.mByMonths && |
841 | mBySetPos == r.mBySetPos && |
842 | mWeekStart == r.mWeekStart; |
843 | } |
844 | |
845 | void RecurrenceRule::Private::clear() |
846 | { |
847 | if ( mIsReadOnly ) { |
848 | return; |
849 | } |
850 | mPeriod = rNone; |
851 | mBySeconds.clear(); |
852 | mByMinutes.clear(); |
853 | mByHours.clear(); |
854 | mByDays.clear(); |
855 | mByMonthDays.clear(); |
856 | mByYearDays.clear(); |
857 | mByWeekNumbers.clear(); |
858 | mByMonths.clear(); |
859 | mBySetPos.clear(); |
860 | mWeekStart = 1; |
861 | |
862 | setDirty(); |
863 | } |
864 | |
865 | void RecurrenceRule::Private::setDirty() |
866 | { |
867 | buildConstraints(); |
868 | mCached = false; |
869 | mCachedDates.clear(); |
870 | for ( int i = 0, iend = mObservers.count(); i < iend; ++i ) { |
871 | if ( mObservers[i] ) { |
872 | mObservers[i]->recurrenceChanged( mParent ); |
873 | } |
874 | } |
875 | } |
876 | //@endcond |
877 | |
878 | /************************************************************************** |
879 | * RecurrenceRule * |
880 | **************************************************************************/ |
881 | |
882 | RecurrenceRule::RecurrenceRule() |
883 | : d( new Private( this ) ) |
884 | { |
885 | } |
886 | |
887 | RecurrenceRule::RecurrenceRule( const RecurrenceRule &r ) |
888 | : d( new Private( this, *r.d ) ) |
889 | { |
890 | } |
891 | |
892 | RecurrenceRule::~RecurrenceRule() |
893 | { |
894 | delete d; |
895 | } |
896 | |
897 | bool RecurrenceRule::operator==( const RecurrenceRule &r ) const |
898 | { |
899 | return *d == *r.d; |
900 | } |
901 | |
902 | RecurrenceRule &RecurrenceRule::operator=( const RecurrenceRule &r ) |
903 | { |
904 | // check for self assignment |
905 | if ( &r == this ) { |
906 | return *this; |
907 | } |
908 | |
909 | *d = *r.d; |
910 | |
911 | return *this; |
912 | } |
913 | |
914 | void RecurrenceRule::addObserver( RuleObserver *observer ) |
915 | { |
916 | if ( !d->mObservers.contains( observer ) ) { |
917 | d->mObservers.append( observer ); |
918 | } |
919 | } |
920 | |
921 | void RecurrenceRule::removeObserver( RuleObserver *observer ) |
922 | { |
923 | if ( d->mObservers.contains( observer ) ) { |
924 | d->mObservers.removeAll( observer ); |
925 | } |
926 | } |
927 | |
928 | void RecurrenceRule::setRecurrenceType( PeriodType period ) |
929 | { |
930 | if ( isReadOnly() ) { |
931 | return; |
932 | } |
933 | d->mPeriod = period; |
934 | d->setDirty(); |
935 | } |
936 | |
937 | KDateTime RecurrenceRule::endDt( bool *result ) const |
938 | { |
939 | if ( result ) { |
940 | *result = false; |
941 | } |
942 | if ( d->mPeriod == rNone ) { |
943 | return KDateTime(); |
944 | } |
945 | if ( d->mDuration < 0 ) { |
946 | return KDateTime(); |
947 | } |
948 | if ( d->mDuration == 0 ) { |
949 | if ( result ) { |
950 | *result = true; |
951 | } |
952 | return d->mDateEnd; |
953 | } |
954 | |
955 | // N occurrences. Check if we have a full cache. If so, return the cached end date. |
956 | if ( !d->mCached ) { |
957 | // If not enough occurrences can be found (i.e. inconsistent constraints) |
958 | if ( !d->buildCache() ) { |
959 | return KDateTime(); |
960 | } |
961 | } |
962 | if ( result ) { |
963 | *result = true; |
964 | } |
965 | return d->mCachedDateEnd; |
966 | } |
967 | |
968 | void RecurrenceRule::setEndDt( const KDateTime &dateTime ) |
969 | { |
970 | if ( isReadOnly() ) { |
971 | return; |
972 | } |
973 | d->mDateEnd = dateTime; |
974 | d->mDuration = 0; // set to 0 because there is an end date/time |
975 | d->setDirty(); |
976 | } |
977 | |
978 | void RecurrenceRule::setDuration( int duration ) |
979 | { |
980 | if ( isReadOnly() ) { |
981 | return; |
982 | } |
983 | d->mDuration = duration; |
984 | d->setDirty(); |
985 | } |
986 | |
987 | void RecurrenceRule::setAllDay( bool allDay ) |
988 | { |
989 | if ( isReadOnly() ) { |
990 | return; |
991 | } |
992 | d->mAllDay = allDay; |
993 | d->setDirty(); |
994 | } |
995 | |
996 | void RecurrenceRule::clear() |
997 | { |
998 | d->clear(); |
999 | } |
1000 | |
1001 | void RecurrenceRule::setDirty() |
1002 | { |
1003 | d->setDirty(); |
1004 | } |
1005 | |
1006 | void RecurrenceRule::setStartDt( const KDateTime &start ) |
1007 | { |
1008 | if ( isReadOnly() ) { |
1009 | return; |
1010 | } |
1011 | d->mDateStart = start; |
1012 | d->setDirty(); |
1013 | } |
1014 | |
1015 | void RecurrenceRule::setFrequency( int freq ) |
1016 | { |
1017 | if ( isReadOnly() || freq <= 0 ) { |
1018 | return; |
1019 | } |
1020 | d->mFrequency = freq; |
1021 | d->setDirty(); |
1022 | } |
1023 | |
1024 | void RecurrenceRule::setBySeconds( const QList<int> bySeconds ) |
1025 | { |
1026 | if ( isReadOnly() ) { |
1027 | return; |
1028 | } |
1029 | d->mBySeconds = bySeconds; |
1030 | d->setDirty(); |
1031 | } |
1032 | |
1033 | void RecurrenceRule::setByMinutes( const QList<int> byMinutes ) |
1034 | { |
1035 | if ( isReadOnly() ) { |
1036 | return; |
1037 | } |
1038 | d->mByMinutes = byMinutes; |
1039 | d->setDirty(); |
1040 | } |
1041 | |
1042 | void RecurrenceRule::setByHours( const QList<int> byHours ) |
1043 | { |
1044 | if ( isReadOnly() ) { |
1045 | return; |
1046 | } |
1047 | d->mByHours = byHours; |
1048 | d->setDirty(); |
1049 | } |
1050 | |
1051 | void RecurrenceRule::setByDays( const QList<WDayPos> byDays ) |
1052 | { |
1053 | if ( isReadOnly() ) { |
1054 | return; |
1055 | } |
1056 | d->mByDays = byDays; |
1057 | d->setDirty(); |
1058 | } |
1059 | |
1060 | void RecurrenceRule::setByMonthDays( const QList<int> byMonthDays ) |
1061 | { |
1062 | if ( isReadOnly() ) { |
1063 | return; |
1064 | } |
1065 | d->mByMonthDays = byMonthDays; |
1066 | d->setDirty(); |
1067 | } |
1068 | |
1069 | void RecurrenceRule::setByYearDays( const QList<int> byYearDays ) |
1070 | { |
1071 | if ( isReadOnly() ) { |
1072 | return; |
1073 | } |
1074 | d->mByYearDays = byYearDays; |
1075 | d->setDirty(); |
1076 | } |
1077 | |
1078 | void RecurrenceRule::setByWeekNumbers( const QList<int> byWeekNumbers ) |
1079 | { |
1080 | if ( isReadOnly() ) { |
1081 | return; |
1082 | } |
1083 | d->mByWeekNumbers = byWeekNumbers; |
1084 | d->setDirty(); |
1085 | } |
1086 | |
1087 | void RecurrenceRule::setByMonths( const QList<int> byMonths ) |
1088 | { |
1089 | if ( isReadOnly() ) { |
1090 | return; |
1091 | } |
1092 | d->mByMonths = byMonths; |
1093 | d->setDirty(); |
1094 | } |
1095 | |
1096 | void RecurrenceRule::setBySetPos( const QList<int> bySetPos ) |
1097 | { |
1098 | if ( isReadOnly() ) { |
1099 | return; |
1100 | } |
1101 | d->mBySetPos = bySetPos; |
1102 | d->setDirty(); |
1103 | } |
1104 | |
1105 | void RecurrenceRule::setWeekStart( short weekStart ) |
1106 | { |
1107 | if ( isReadOnly() ) { |
1108 | return; |
1109 | } |
1110 | d->mWeekStart = weekStart; |
1111 | d->setDirty(); |
1112 | } |
1113 | |
1114 | void RecurrenceRule::shiftTimes( const KDateTime::Spec &oldSpec, const KDateTime::Spec &newSpec ) |
1115 | { |
1116 | d->mDateStart = d->mDateStart.toTimeSpec( oldSpec ); |
1117 | d->mDateStart.setTimeSpec( newSpec ); |
1118 | if ( d->mDuration == 0 ) { |
1119 | d->mDateEnd = d->mDateEnd.toTimeSpec( oldSpec ); |
1120 | d->mDateEnd.setTimeSpec( newSpec ); |
1121 | } |
1122 | d->setDirty(); |
1123 | } |
1124 | |
1125 | // Taken from recurrence.cpp |
1126 | // int RecurrenceRule::maxIterations() const |
1127 | // { |
1128 | // /* Find the maximum number of iterations which may be needed to reach the |
1129 | // * next actual occurrence of a monthly or yearly recurrence. |
1130 | // * More than one iteration may be needed if, for example, it's the 29th February, |
1131 | // * the 31st day of the month or the 5th Monday, and the month being checked is |
1132 | // * February or a 30-day month. |
1133 | // * The following recurrences may never occur: |
1134 | // * - For rMonthlyDay: if the frequency is a whole number of years. |
1135 | // * - For rMonthlyPos: if the frequency is an even whole number of years. |
1136 | // * - For rYearlyDay, rYearlyMonth: if the frequeny is a multiple of 4 years. |
1137 | // * - For rYearlyPos: if the frequency is an even number of years. |
1138 | // * The maximum number of iterations needed, assuming that it does actually occur, |
1139 | // * was found empirically. |
1140 | // */ |
1141 | // switch (recurs) { |
1142 | // case rMonthlyDay: |
1143 | // return (rFreq % 12) ? 6 : 8; |
1144 | // |
1145 | // case rMonthlyPos: |
1146 | // if (rFreq % 12 == 0) { |
1147 | // // Some of these frequencies may never occur |
1148 | // return (rFreq % 84 == 0) ? 364 // frequency = multiple of 7 years |
1149 | // : (rFreq % 48 == 0) ? 7 // frequency = multiple of 4 years |
1150 | // : (rFreq % 24 == 0) ? 14 : 28; // frequency = multiple of 2 or 1 year |
1151 | // } |
1152 | // // All other frequencies will occur sometime |
1153 | // if (rFreq > 120) |
1154 | // return 364; // frequencies of > 10 years will hit the date limit first |
1155 | // switch (rFreq) { |
1156 | // case 23: return 50; |
1157 | // case 46: return 38; |
1158 | // case 56: return 138; |
1159 | // case 66: return 36; |
1160 | // case 89: return 54; |
1161 | // case 112: return 253; |
1162 | // default: return 25; // most frequencies will need < 25 iterations |
1163 | // } |
1164 | // |
1165 | // case rYearlyMonth: |
1166 | // case rYearlyDay: |
1167 | // return 8; // only 29th Feb or day 366 will need more than one iteration |
1168 | // |
1169 | // case rYearlyPos: |
1170 | // if (rFreq % 7 == 0) |
1171 | // return 364; // frequencies of a multiple of 7 years will hit the date limit first |
1172 | // if (rFreq % 2 == 0) { |
1173 | // // Some of these frequencies may never occur |
1174 | // return (rFreq % 4 == 0) ? 7 : 14; // frequency = even number of years |
1175 | // } |
1176 | // return 28; |
1177 | // } |
1178 | // return 1; |
1179 | // } |
1180 | |
1181 | //@cond PRIVATE |
1182 | void RecurrenceRule::Private::buildConstraints() |
1183 | { |
1184 | mTimedRepetition = 0; |
1185 | mNoByRules = mBySetPos.isEmpty(); |
1186 | mConstraints.clear(); |
1187 | Constraint con( mDateStart.timeSpec() ); |
1188 | if ( mWeekStart > 0 ) { |
1189 | con.setWeekstart( mWeekStart ); |
1190 | } |
1191 | mConstraints.append( con ); |
1192 | |
1193 | int c, cend; |
1194 | int i, iend; |
1195 | Constraint::List tmp; |
1196 | |
1197 | #define intConstraint( list, setElement ) \ |
1198 | if ( !list.isEmpty() ) { \ |
1199 | mNoByRules = false; \ |
1200 | iend = list.count(); \ |
1201 | if ( iend == 1 ) { \ |
1202 | for ( c = 0, cend = mConstraints.count(); c < cend; ++c ) { \ |
1203 | mConstraints[c].setElement( list[0] ); \ |
1204 | } \ |
1205 | } else { \ |
1206 | for ( c = 0, cend = mConstraints.count(); c < cend; ++c ) { \ |
1207 | for ( i = 0; i < iend; ++i ) { \ |
1208 | con = mConstraints[c]; \ |
1209 | con.setElement( list[i] ); \ |
1210 | tmp.append( con ); \ |
1211 | } \ |
1212 | } \ |
1213 | mConstraints = tmp; \ |
1214 | tmp.clear(); \ |
1215 | } \ |
1216 | } |
1217 | |
1218 | intConstraint( mBySeconds, setSecond ); |
1219 | intConstraint( mByMinutes, setMinute ); |
1220 | intConstraint( mByHours, setHour ); |
1221 | intConstraint( mByMonthDays, setDay ); |
1222 | intConstraint( mByMonths, setMonth ); |
1223 | intConstraint( mByYearDays, setYearday ); |
1224 | intConstraint( mByWeekNumbers, setWeeknumber ); |
1225 | #undef intConstraint |
1226 | |
1227 | if ( !mByDays.isEmpty() ) { |
1228 | mNoByRules = false; |
1229 | for ( c = 0, cend = mConstraints.count(); c < cend; ++c ) { |
1230 | for ( i = 0, iend = mByDays.count(); i < iend; ++i ) { |
1231 | con = mConstraints[c]; |
1232 | con.setWeekday( mByDays[i].day() ); |
1233 | con.setWeekdaynr( mByDays[i].pos() ); |
1234 | tmp.append( con ); |
1235 | } |
1236 | } |
1237 | mConstraints = tmp; |
1238 | tmp.clear(); |
1239 | } |
1240 | |
1241 | #define fixConstraint( setElement, value ) \ |
1242 | { \ |
1243 | for ( c = 0, cend = mConstraints.count(); c < cend; ++c ) { \ |
1244 | mConstraints[c].setElement( value ); \ |
1245 | } \ |
1246 | } |
1247 | // Now determine missing values from DTSTART. This can speed up things, |
1248 | // because we have more restrictions and save some loops. |
1249 | |
1250 | // TODO: Does RFC 2445 intend to restrict the weekday in all cases of weekly? |
1251 | if ( mPeriod == rWeekly && mByDays.isEmpty() ) { |
1252 | fixConstraint( setWeekday, mDateStart.date().dayOfWeek() ); |
1253 | } |
1254 | |
1255 | // Really fall through in the cases, because all smaller time intervals are |
1256 | // constrained from dtstart |
1257 | switch ( mPeriod ) { |
1258 | case rYearly: |
1259 | if ( mByDays.isEmpty() && mByWeekNumbers.isEmpty() && |
1260 | mByYearDays.isEmpty() && mByMonths.isEmpty() ) { |
1261 | fixConstraint( setMonth, mDateStart.date().month() ); |
1262 | } |
1263 | case rMonthly: |
1264 | if ( mByDays.isEmpty() && mByWeekNumbers.isEmpty() && |
1265 | mByYearDays.isEmpty() && mByMonthDays.isEmpty() ) { |
1266 | fixConstraint( setDay, mDateStart.date().day() ); |
1267 | } |
1268 | case rWeekly: |
1269 | case rDaily: |
1270 | if ( mByHours.isEmpty() ) { |
1271 | fixConstraint( setHour, mDateStart.time().hour() ); |
1272 | } |
1273 | case rHourly: |
1274 | if ( mByMinutes.isEmpty() ) { |
1275 | fixConstraint( setMinute, mDateStart.time().minute() ); |
1276 | } |
1277 | case rMinutely: |
1278 | if ( mBySeconds.isEmpty() ) { |
1279 | fixConstraint( setSecond, mDateStart.time().second() ); |
1280 | } |
1281 | case rSecondly: |
1282 | default: |
1283 | break; |
1284 | } |
1285 | #undef fixConstraint |
1286 | |
1287 | if ( mNoByRules ) { |
1288 | switch ( mPeriod ) { |
1289 | case rHourly: |
1290 | mTimedRepetition = mFrequency * 3600; |
1291 | break; |
1292 | case rMinutely: |
1293 | mTimedRepetition = mFrequency * 60; |
1294 | break; |
1295 | case rSecondly: |
1296 | mTimedRepetition = mFrequency; |
1297 | break; |
1298 | default: |
1299 | break; |
1300 | } |
1301 | } else { |
1302 | for ( c = 0, cend = mConstraints.count(); c < cend; ) { |
1303 | if ( mConstraints[c].isConsistent( mPeriod ) ) { |
1304 | ++c; |
1305 | } else { |
1306 | mConstraints.removeAt( c ); |
1307 | --cend; |
1308 | } |
1309 | } |
1310 | } |
1311 | } |
1312 | |
1313 | // Build and cache a list of all occurrences. |
1314 | // Only call buildCache() if mDuration > 0. |
1315 | bool RecurrenceRule::Private::buildCache() const |
1316 | { |
1317 | // Build the list of all occurrences of this event (we need that to determine |
1318 | // the end date!) |
1319 | Constraint interval( getNextValidDateInterval( mDateStart, mPeriod ) ); |
1320 | QDateTime next; |
1321 | |
1322 | DateTimeList dts = datesForInterval( interval, mPeriod ); |
1323 | // Only use dates after the event has started (start date is only included |
1324 | // if it matches) |
1325 | int i = dts.findLT( mDateStart ); |
1326 | if ( i >= 0 ) { |
1327 | dts.erase( dts.begin(), dts.begin() + i + 1 ); |
1328 | } |
1329 | |
1330 | int loopnr = 0; |
1331 | int dtnr = dts.count(); |
1332 | // some validity checks to avoid infinite loops (i.e. if we have |
1333 | // done this loop already 10000 times, bail out ) |
1334 | while ( loopnr < LOOP_LIMIT && dtnr < mDuration ) { |
1335 | interval.increase( mPeriod, mFrequency ); |
1336 | // The returned date list is already sorted! |
1337 | dts += datesForInterval( interval, mPeriod ); |
1338 | dtnr = dts.count(); |
1339 | ++loopnr; |
1340 | } |
1341 | if ( dts.count() > mDuration ) { |
1342 | // we have picked up more occurrences than necessary, remove them |
1343 | dts.erase( dts.begin() + mDuration, dts.end() ); |
1344 | } |
1345 | mCached = true; |
1346 | mCachedDates = dts; |
1347 | |
1348 | // it = dts.begin(); |
1349 | // while ( it != dts.end() ) { |
1350 | // kDebug() << " -=>" << dumpTime(*it); |
1351 | // ++it; |
1352 | // } |
1353 | if ( int( dts.count() ) == mDuration ) { |
1354 | mCachedDateEnd = dts.last(); |
1355 | return true; |
1356 | } else { |
1357 | // The cached date list is incomplete |
1358 | mCachedDateEnd = KDateTime(); |
1359 | mCachedLastDate = interval.intervalDateTime( mPeriod ); |
1360 | return false; |
1361 | } |
1362 | } |
1363 | //@endcond |
1364 | |
1365 | bool RecurrenceRule::dateMatchesRules( const KDateTime &kdt ) const |
1366 | { |
1367 | KDateTime dt = kdt.toTimeSpec( d->mDateStart.timeSpec() ); |
1368 | for ( int i = 0, iend = d->mConstraints.count(); i < iend; ++i ) { |
1369 | if ( d->mConstraints[i].matches( dt, recurrenceType() ) ) { |
1370 | return true; |
1371 | } |
1372 | } |
1373 | return false; |
1374 | } |
1375 | |
1376 | bool RecurrenceRule::recursOn( const QDate &qd, const KDateTime::Spec &timeSpec ) const |
1377 | { |
1378 | int i, iend; |
1379 | if ( allDay() ) { |
1380 | // It's a date-only rule, so it has no time specification. |
1381 | // Therefore ignore 'timeSpec'. |
1382 | if ( qd < d->mDateStart.date() ) { |
1383 | return false; |
1384 | } |
1385 | // Start date is only included if it really matches |
1386 | QDate endDate; |
1387 | if ( d->mDuration >= 0 ) { |
1388 | endDate = endDt().date(); |
1389 | if ( qd > endDate ) { |
1390 | return false; |
1391 | } |
1392 | } |
1393 | |
1394 | // The date must be in an appropriate interval (getNextValidDateInterval), |
1395 | // Plus it must match at least one of the constraints |
1396 | bool match = false; |
1397 | for ( i = 0, iend = d->mConstraints.count(); i < iend && !match; ++i ) { |
1398 | match = d->mConstraints[i].matches( qd, recurrenceType() ); |
1399 | } |
1400 | if ( !match ) { |
1401 | return false; |
1402 | } |
1403 | |
1404 | KDateTime start( qd, QTime( 0, 0, 0 ), d->mDateStart.timeSpec() ); |
1405 | Constraint interval( d->getNextValidDateInterval( start, recurrenceType() ) ); |
1406 | // Constraint::matches is quite efficient, so first check if it can occur at |
1407 | // all before we calculate all actual dates. |
1408 | if ( !interval.matches( qd, recurrenceType() ) ) { |
1409 | return false; |
1410 | } |
1411 | // We really need to obtain the list of dates in this interval, since |
1412 | // otherwise BYSETPOS will not work (i.e. the date will match the interval, |
1413 | // but BYSETPOS selects only one of these matching dates! |
1414 | KDateTime end = start.addDays(1); |
1415 | do { |
1416 | DateTimeList dts = d->datesForInterval( interval, recurrenceType() ); |
1417 | for ( i = 0, iend = dts.count(); i < iend; ++i ) { |
1418 | if ( dts[i].date() >= qd ) { |
1419 | return dts[i].date() == qd; |
1420 | } |
1421 | } |
1422 | interval.increase( recurrenceType(), frequency() ); |
1423 | } while ( interval.intervalDateTime( recurrenceType() ) < end ); |
1424 | return false; |
1425 | } |
1426 | |
1427 | // It's a date-time rule, so we need to take the time specification into account. |
1428 | KDateTime start( qd, QTime( 0, 0, 0 ), timeSpec ); |
1429 | KDateTime end = start.addDays( 1 ).toTimeSpec( d->mDateStart.timeSpec() ); |
1430 | start = start.toTimeSpec( d->mDateStart.timeSpec() ); |
1431 | if ( end < d->mDateStart ) { |
1432 | return false; |
1433 | } |
1434 | if ( start < d->mDateStart ) { |
1435 | start = d->mDateStart; |
1436 | } |
1437 | |
1438 | // Start date is only included if it really matches |
1439 | if ( d->mDuration >= 0 ) { |
1440 | KDateTime endRecur = endDt(); |
1441 | if ( endRecur.isValid() ) { |
1442 | if ( start > endRecur ) { |
1443 | return false; |
1444 | } |
1445 | if ( end > endRecur ) { |
1446 | end = endRecur; // limit end-of-day time to end of recurrence rule |
1447 | } |
1448 | } |
1449 | } |
1450 | |
1451 | if ( d->mTimedRepetition ) { |
1452 | // It's a simple sub-daily recurrence with no constraints |
1453 | int n = static_cast<int>( ( d->mDateStart.secsTo_long( start ) - 1 ) % d->mTimedRepetition ); |
1454 | return start.addSecs( d->mTimedRepetition - n ) < end; |
1455 | } |
1456 | |
1457 | // Find the start and end dates in the time spec for the rule |
1458 | QDate startDay = start.date(); |
1459 | QDate endDay = end.addSecs( -1 ).date(); |
1460 | int dayCount = startDay.daysTo( endDay ) + 1; |
1461 | |
1462 | // The date must be in an appropriate interval (getNextValidDateInterval), |
1463 | // Plus it must match at least one of the constraints |
1464 | bool match = false; |
1465 | for ( i = 0, iend = d->mConstraints.count(); i < iend && !match; ++i ) { |
1466 | match = d->mConstraints[i].matches( startDay, recurrenceType() ); |
1467 | for ( int day = 1; day < dayCount && !match; ++day ) { |
1468 | match = d->mConstraints[i].matches( startDay.addDays( day ), recurrenceType() ); |
1469 | } |
1470 | } |
1471 | if ( !match ) { |
1472 | return false; |
1473 | } |
1474 | |
1475 | Constraint interval( d->getNextValidDateInterval( start, recurrenceType() ) ); |
1476 | // Constraint::matches is quite efficient, so first check if it can occur at |
1477 | // all before we calculate all actual dates. |
1478 | match = false; |
1479 | Constraint intervalm = interval; |
1480 | do { |
1481 | match = intervalm.matches( startDay, recurrenceType() ); |
1482 | for ( int day = 1; day < dayCount && !match; ++day ) { |
1483 | match = intervalm.matches( startDay.addDays( day ), recurrenceType() ); |
1484 | } |
1485 | if ( match ) { |
1486 | break; |
1487 | } |
1488 | intervalm.increase( recurrenceType(), frequency() ); |
1489 | } while ( intervalm.intervalDateTime( recurrenceType() ) < end ); |
1490 | if ( !match ) { |
1491 | return false; |
1492 | } |
1493 | |
1494 | // We really need to obtain the list of dates in this interval, since |
1495 | // otherwise BYSETPOS will not work (i.e. the date will match the interval, |
1496 | // but BYSETPOS selects only one of these matching dates! |
1497 | do { |
1498 | DateTimeList dts = d->datesForInterval( interval, recurrenceType() ); |
1499 | int i = dts.findGE( start ); |
1500 | if ( i >= 0 ) { |
1501 | return dts[i] <= end; |
1502 | } |
1503 | interval.increase( recurrenceType(), frequency() ); |
1504 | } while ( interval.intervalDateTime( recurrenceType() ) < end ); |
1505 | |
1506 | return false; |
1507 | } |
1508 | |
1509 | bool RecurrenceRule::recursAt( const KDateTime &kdt ) const |
1510 | { |
1511 | // Convert to the time spec used by this recurrence rule |
1512 | KDateTime dt( kdt.toTimeSpec( d->mDateStart.timeSpec() ) ); |
1513 | |
1514 | if ( allDay() ) { |
1515 | return recursOn( dt.date(), dt.timeSpec() ); |
1516 | } |
1517 | if ( dt < d->mDateStart ) { |
1518 | return false; |
1519 | } |
1520 | // Start date is only included if it really matches |
1521 | if ( d->mDuration >= 0 && dt > endDt() ) { |
1522 | return false; |
1523 | } |
1524 | |
1525 | if ( d->mTimedRepetition ) { |
1526 | // It's a simple sub-daily recurrence with no constraints |
1527 | return !( d->mDateStart.secsTo_long( dt ) % d->mTimedRepetition ); |
1528 | } |
1529 | |
1530 | // The date must be in an appropriate interval (getNextValidDateInterval), |
1531 | // Plus it must match at least one of the constraints |
1532 | if ( !dateMatchesRules( dt ) ) { |
1533 | return false; |
1534 | } |
1535 | // if it recurs every interval, speed things up... |
1536 | // if ( d->mFrequency == 1 && d->mBySetPos.isEmpty() && d->mByDays.isEmpty() ) return true; |
1537 | Constraint interval( d->getNextValidDateInterval( dt, recurrenceType() ) ); |
1538 | // TODO_Recurrence: Does this work with BySetPos??? |
1539 | if ( interval.matches( dt, recurrenceType() ) ) { |
1540 | return true; |
1541 | } |
1542 | return false; |
1543 | } |
1544 | |
1545 | TimeList RecurrenceRule::recurTimesOn( const QDate &date, const KDateTime::Spec &timeSpec ) const |
1546 | { |
1547 | TimeList lst; |
1548 | if ( allDay() ) { |
1549 | return lst; |
1550 | } |
1551 | KDateTime start( date, QTime( 0, 0, 0 ), timeSpec ); |
1552 | KDateTime end = start.addDays( 1 ).addSecs( -1 ); |
1553 | DateTimeList dts = timesInInterval( start, end ); // returns between start and end inclusive |
1554 | for ( int i = 0, iend = dts.count(); i < iend; ++i ) { |
1555 | lst += dts[i].toTimeSpec( timeSpec ).time(); |
1556 | } |
1557 | return lst; |
1558 | } |
1559 | |
1560 | /** Returns the number of recurrences up to and including the date/time specified. */ |
1561 | int RecurrenceRule::durationTo( const KDateTime &dt ) const |
1562 | { |
1563 | // Convert to the time spec used by this recurrence rule |
1564 | KDateTime toDate( dt.toTimeSpec( d->mDateStart.timeSpec() ) ); |
1565 | // Easy cases: |
1566 | // either before start, or after all recurrences and we know their number |
1567 | if ( toDate < d->mDateStart ) { |
1568 | return 0; |
1569 | } |
1570 | // Start date is only included if it really matches |
1571 | if ( d->mDuration > 0 && toDate >= endDt() ) { |
1572 | return d->mDuration; |
1573 | } |
1574 | |
1575 | if ( d->mTimedRepetition ) { |
1576 | // It's a simple sub-daily recurrence with no constraints |
1577 | return static_cast<int>( d->mDateStart.secsTo_long( toDate ) / d->mTimedRepetition ); |
1578 | } |
1579 | |
1580 | return timesInInterval( d->mDateStart, toDate ).count(); |
1581 | } |
1582 | |
1583 | int RecurrenceRule::durationTo( const QDate &date ) const |
1584 | { |
1585 | return durationTo( KDateTime( date, QTime( 23, 59, 59 ), d->mDateStart.timeSpec() ) ); |
1586 | } |
1587 | |
1588 | KDateTime RecurrenceRule::getPreviousDate( const KDateTime &afterDate ) const |
1589 | { |
1590 | // Convert to the time spec used by this recurrence rule |
1591 | KDateTime toDate( afterDate.toTimeSpec( d->mDateStart.timeSpec() ) ); |
1592 | |
1593 | // Invalid starting point, or beyond end of recurrence |
1594 | if ( !toDate.isValid() || toDate < d->mDateStart ) { |
1595 | return KDateTime(); |
1596 | } |
1597 | |
1598 | if ( d->mTimedRepetition ) { |
1599 | // It's a simple sub-daily recurrence with no constraints |
1600 | KDateTime prev = toDate; |
1601 | if ( d->mDuration >= 0 && endDt().isValid() && toDate > endDt() ) { |
1602 | prev = endDt().addSecs( 1 ).toTimeSpec( d->mDateStart.timeSpec() ); |
1603 | } |
1604 | int n = static_cast<int>( ( d->mDateStart.secsTo_long( prev ) - 1 ) % d->mTimedRepetition ); |
1605 | if ( n < 0 ) { |
1606 | return KDateTime(); // before recurrence start |
1607 | } |
1608 | prev = prev.addSecs( -n - 1 ); |
1609 | return prev >= d->mDateStart ? prev : KDateTime(); |
1610 | } |
1611 | |
1612 | // If we have a cache (duration given), use that |
1613 | if ( d->mDuration > 0 ) { |
1614 | if ( !d->mCached ) { |
1615 | d->buildCache(); |
1616 | } |
1617 | int i = d->mCachedDates.findLT( toDate ); |
1618 | if ( i >= 0 ) { |
1619 | return d->mCachedDates[i]; |
1620 | } |
1621 | return KDateTime(); |
1622 | } |
1623 | |
1624 | KDateTime prev = toDate; |
1625 | if ( d->mDuration >= 0 && endDt().isValid() && toDate > endDt() ) { |
1626 | prev = endDt().addSecs( 1 ).toTimeSpec( d->mDateStart.timeSpec() ); |
1627 | } |
1628 | |
1629 | Constraint interval( d->getPreviousValidDateInterval( prev, recurrenceType() ) ); |
1630 | DateTimeList dts = d->datesForInterval( interval, recurrenceType() ); |
1631 | int i = dts.findLT( prev ); |
1632 | if ( i >= 0 ) { |
1633 | return ( dts[i] >= d->mDateStart ) ? dts[i] : KDateTime(); |
1634 | } |
1635 | |
1636 | // Previous interval. As soon as we find an occurrence, we're done. |
1637 | while ( interval.intervalDateTime( recurrenceType() ) > d->mDateStart ) { |
1638 | interval.increase( recurrenceType(), -int( frequency() ) ); |
1639 | // The returned date list is sorted |
1640 | DateTimeList dts = d->datesForInterval( interval, recurrenceType() ); |
1641 | // The list is sorted, so take the last one. |
1642 | if ( !dts.isEmpty() ) { |
1643 | prev = dts.last(); |
1644 | if ( prev.isValid() && prev >= d->mDateStart ) { |
1645 | return prev; |
1646 | } else { |
1647 | return KDateTime(); |
1648 | } |
1649 | } |
1650 | } |
1651 | return KDateTime(); |
1652 | } |
1653 | |
1654 | KDateTime RecurrenceRule::getNextDate( const KDateTime &preDate ) const |
1655 | { |
1656 | // Convert to the time spec used by this recurrence rule |
1657 | KDateTime fromDate( preDate.toTimeSpec( d->mDateStart.timeSpec() ) ); |
1658 | // Beyond end of recurrence |
1659 | if ( d->mDuration >= 0 && endDt().isValid() && fromDate >= endDt() ) { |
1660 | return KDateTime(); |
1661 | } |
1662 | |
1663 | // Start date is only included if it really matches |
1664 | if ( fromDate < d->mDateStart ) { |
1665 | fromDate = d->mDateStart.addSecs( -1 ); |
1666 | } |
1667 | |
1668 | if ( d->mTimedRepetition ) { |
1669 | // It's a simple sub-daily recurrence with no constraints |
1670 | int n = static_cast<int>( ( d->mDateStart.secsTo_long( fromDate ) + 1 ) % d->mTimedRepetition ); |
1671 | KDateTime next = fromDate.addSecs( d->mTimedRepetition - n + 1 ); |
1672 | return d->mDuration < 0 || !endDt().isValid() || next <= endDt() ? next : KDateTime(); |
1673 | } |
1674 | |
1675 | if ( d->mDuration > 0 ) { |
1676 | if ( !d->mCached ) { |
1677 | d->buildCache(); |
1678 | } |
1679 | int i = d->mCachedDates.findGT( fromDate ); |
1680 | if ( i >= 0 ) { |
1681 | return d->mCachedDates[i]; |
1682 | } |
1683 | } |
1684 | |
1685 | KDateTime end = endDt(); |
1686 | Constraint interval( d->getNextValidDateInterval( fromDate, recurrenceType() ) ); |
1687 | DateTimeList dts = d->datesForInterval( interval, recurrenceType() ); |
1688 | int i = dts.findGT( fromDate ); |
1689 | if ( i >= 0 ) { |
1690 | return ( d->mDuration < 0 || dts[i] <= end ) ? dts[i] : KDateTime(); |
1691 | } |
1692 | interval.increase( recurrenceType(), frequency() ); |
1693 | if ( d->mDuration >= 0 && interval.intervalDateTime( recurrenceType() ) > end ) { |
1694 | return KDateTime(); |
1695 | } |
1696 | |
1697 | // Increase the interval. The first occurrence that we find is the result (if |
1698 | // if's before the end date). |
1699 | // TODO: some validity checks to avoid infinite loops for contradictory constraints |
1700 | int loop = 0; |
1701 | do { |
1702 | DateTimeList dts = d->datesForInterval( interval, recurrenceType() ); |
1703 | if ( dts.count() > 0 ) { |
1704 | KDateTime ret( dts[0] ); |
1705 | if ( d->mDuration >= 0 && ret > end ) { |
1706 | return KDateTime(); |
1707 | } else { |
1708 | return ret; |
1709 | } |
1710 | } |
1711 | interval.increase( recurrenceType(), frequency() ); |
1712 | } while ( ++loop < LOOP_LIMIT && |
1713 | ( d->mDuration < 0 || interval.intervalDateTime( recurrenceType() ) < end ) ); |
1714 | return KDateTime(); |
1715 | } |
1716 | |
1717 | DateTimeList RecurrenceRule::timesInInterval( const KDateTime &dtStart, |
1718 | const KDateTime &dtEnd ) const |
1719 | { |
1720 | KDateTime start = dtStart.toTimeSpec( d->mDateStart.timeSpec() ); |
1721 | KDateTime end = dtEnd.toTimeSpec( d->mDateStart.timeSpec() ); |
1722 | DateTimeList result; |
1723 | if ( end < d->mDateStart ) { |
1724 | return result; // before start of recurrence |
1725 | } |
1726 | KDateTime enddt = end; |
1727 | if ( d->mDuration >= 0 ) { |
1728 | KDateTime endRecur = endDt(); |
1729 | if ( endRecur.isValid() ) { |
1730 | if ( start > endRecur ) { |
1731 | return result; // beyond end of recurrence |
1732 | } |
1733 | if ( end > endRecur ) { |
1734 | enddt = endRecur; // limit end time to end of recurrence rule |
1735 | } |
1736 | } |
1737 | } |
1738 | |
1739 | if ( d->mTimedRepetition ) { |
1740 | // It's a simple sub-daily recurrence with no constraints |
1741 | int n = static_cast<int>( ( d->mDateStart.secsTo_long( start ) - 1 ) % d->mTimedRepetition ); |
1742 | KDateTime dt = start.addSecs( d->mTimedRepetition - n ); |
1743 | if ( dt < enddt ) { |
1744 | n = static_cast<int>( ( dt.secsTo_long( enddt ) - 1 ) / d->mTimedRepetition ) + 1; |
1745 | // limit n by a sane value else we can "explode". |
1746 | n = qMin( n, LOOP_LIMIT ); |
1747 | for ( int i = 0; i < n; dt = dt.addSecs( d->mTimedRepetition ), ++i ) { |
1748 | result += dt; |
1749 | } |
1750 | } |
1751 | return result; |
1752 | } |
1753 | |
1754 | KDateTime st = start; |
1755 | bool done = false; |
1756 | if ( d->mDuration > 0 ) { |
1757 | if ( !d->mCached ) { |
1758 | d->buildCache(); |
1759 | } |
1760 | if ( d->mCachedDateEnd.isValid() && start > d->mCachedDateEnd ) { |
1761 | return result; // beyond end of recurrence |
1762 | } |
1763 | int i = d->mCachedDates.findGE( start ); |
1764 | if ( i >= 0 ) { |
1765 | int iend = d->mCachedDates.findGT( enddt, i ); |
1766 | if ( iend < 0 ) { |
1767 | iend = d->mCachedDates.count(); |
1768 | } else { |
1769 | done = true; |
1770 | } |
1771 | while ( i < iend ) { |
1772 | result += d->mCachedDates[i++]; |
1773 | } |
1774 | } |
1775 | if ( d->mCachedDateEnd.isValid() ) { |
1776 | done = true; |
1777 | } else if ( !result.isEmpty() ) { |
1778 | result += KDateTime(); // indicate that the returned list is incomplete |
1779 | done = true; |
1780 | } |
1781 | if ( done ) { |
1782 | return result; |
1783 | } |
1784 | // We don't have any result yet, but we reached the end of the incomplete cache |
1785 | st = d->mCachedLastDate.addSecs( 1 ); |
1786 | } |
1787 | |
1788 | Constraint interval( d->getNextValidDateInterval( st, recurrenceType() ) ); |
1789 | int loop = 0; |
1790 | do { |
1791 | DateTimeList dts = d->datesForInterval( interval, recurrenceType() ); |
1792 | int i = 0; |
1793 | int iend = dts.count(); |
1794 | if ( loop == 0 ) { |
1795 | i = dts.findGE( st ); |
1796 | if ( i < 0 ) { |
1797 | i = iend; |
1798 | } |
1799 | } |
1800 | int j = dts.findGT( enddt, i ); |
1801 | if ( j >= 0 ) { |
1802 | iend = j; |
1803 | loop = LOOP_LIMIT; |
1804 | } |
1805 | while ( i < iend ) { |
1806 | result += dts[i++]; |
1807 | } |
1808 | // Increase the interval. |
1809 | interval.increase( recurrenceType(), frequency() ); |
1810 | } while ( ++loop < LOOP_LIMIT && |
1811 | interval.intervalDateTime( recurrenceType() ) < end ); |
1812 | return result; |
1813 | } |
1814 | |
1815 | //@cond PRIVATE |
1816 | // Find the date/time of the occurrence at or before a date/time, |
1817 | // for a given period type. |
1818 | // Return a constraint whose value appropriate to 'type', is set to |
1819 | // the value contained in the date/time. |
1820 | Constraint RecurrenceRule::Private::getPreviousValidDateInterval( const KDateTime &dt, |
1821 | PeriodType type ) const |
1822 | { |
1823 | long periods = 0; |
1824 | KDateTime start = mDateStart; |
1825 | KDateTime nextValid( start ); |
1826 | int modifier = 1; |
1827 | KDateTime toDate( dt.toTimeSpec( start.timeSpec() ) ); |
1828 | // for super-daily recurrences, don't care about the time part |
1829 | |
1830 | // Find the #intervals since the dtstart and round to the next multiple of |
1831 | // the frequency |
1832 | switch ( type ) { |
1833 | // Really fall through for sub-daily, since the calculations only differ |
1834 | // by the factor 60 and 60*60! Same for weekly and daily (factor 7) |
1835 | case rHourly: |
1836 | modifier *= 60; |
1837 | case rMinutely: |
1838 | modifier *= 60; |
1839 | case rSecondly: |
1840 | periods = static_cast<int>( start.secsTo_long( toDate ) / modifier ); |
1841 | // round it down to the next lower multiple of frequency: |
1842 | if ( mFrequency > 0 ) { |
1843 | periods = ( periods / mFrequency ) * mFrequency; |
1844 | } |
1845 | nextValid = start.addSecs( modifier * periods ); |
1846 | break; |
1847 | case rWeekly: |
1848 | toDate = toDate.addDays( -( 7 + toDate.date().dayOfWeek() - mWeekStart ) % 7 ); |
1849 | start = start.addDays( -( 7 + start.date().dayOfWeek() - mWeekStart ) % 7 ); |
1850 | modifier *= 7; |
1851 | case rDaily: |
1852 | periods = start.daysTo( toDate ) / modifier; |
1853 | // round it down to the next lower multiple of frequency: |
1854 | if ( mFrequency > 0 ) { |
1855 | periods = ( periods / mFrequency ) * mFrequency; |
1856 | } |
1857 | nextValid = start.addDays( modifier * periods ); |
1858 | break; |
1859 | case rMonthly: |
1860 | { |
1861 | periods = 12 * ( toDate.date().year() - start.date().year() ) + |
1862 | ( toDate.date().month() - start.date().month() ); |
1863 | // round it down to the next lower multiple of frequency: |
1864 | if ( mFrequency > 0 ) { |
1865 | periods = ( periods / mFrequency ) * mFrequency; |
1866 | } |
1867 | // set the day to the first day of the month, so we don't have problems |
1868 | // with non-existent days like Feb 30 or April 31 |
1869 | start.setDate( QDate( start.date().year(), start.date().month(), 1 ) ); |
1870 | nextValid.setDate( start.date().addMonths( periods ) ); |
1871 | break; } |
1872 | case rYearly: |
1873 | periods = ( toDate.date().year() - start.date().year() ); |
1874 | // round it down to the next lower multiple of frequency: |
1875 | if ( mFrequency > 0 ) { |
1876 | periods = ( periods / mFrequency ) * mFrequency; |
1877 | } |
1878 | nextValid.setDate( start.date().addYears( periods ) ); |
1879 | break; |
1880 | default: |
1881 | break; |
1882 | } |
1883 | |
1884 | return Constraint( nextValid, type, mWeekStart ); |
1885 | } |
1886 | |
1887 | // Find the date/time of the next occurrence at or after a date/time, |
1888 | // for a given period type. |
1889 | // Return a constraint whose value appropriate to 'type', is set to the |
1890 | // value contained in the date/time. |
1891 | Constraint RecurrenceRule::Private::getNextValidDateInterval( const KDateTime &dt, |
1892 | PeriodType type ) const |
1893 | { |
1894 | // TODO: Simplify this! |
1895 | long periods = 0; |
1896 | KDateTime start = mDateStart; |
1897 | KDateTime nextValid( start ); |
1898 | int modifier = 1; |
1899 | KDateTime toDate( dt.toTimeSpec( start.timeSpec() ) ); |
1900 | // for super-daily recurrences, don't care about the time part |
1901 | |
1902 | // Find the #intervals since the dtstart and round to the next multiple of |
1903 | // the frequency |
1904 | switch ( type ) { |
1905 | // Really fall through for sub-daily, since the calculations only differ |
1906 | // by the factor 60 and 60*60! Same for weekly and daily (factor 7) |
1907 | case rHourly: |
1908 | modifier *= 60; |
1909 | case rMinutely: |
1910 | modifier *= 60; |
1911 | case rSecondly: |
1912 | periods = static_cast<int>( start.secsTo_long( toDate ) / modifier ); |
1913 | periods = qMax( 0L, periods ); |
1914 | if ( periods > 0 && mFrequency > 0 ) { |
1915 | periods += ( mFrequency - 1 - ( ( periods - 1 ) % mFrequency ) ); |
1916 | } |
1917 | nextValid = start.addSecs( modifier * periods ); |
1918 | break; |
1919 | case rWeekly: |
1920 | // correct both start date and current date to start of week |
1921 | toDate = toDate.addDays( -( 7 + toDate.date().dayOfWeek() - mWeekStart ) % 7 ); |
1922 | start = start.addDays( -( 7 + start.date().dayOfWeek() - mWeekStart ) % 7 ); |
1923 | modifier *= 7; |
1924 | case rDaily: |
1925 | periods = start.daysTo( toDate ) / modifier; |
1926 | periods = qMax( 0L, periods ); |
1927 | if ( periods > 0 && mFrequency > 0 ) { |
1928 | periods += ( mFrequency - 1 - ( ( periods - 1 ) % mFrequency ) ); |
1929 | } |
1930 | nextValid = start.addDays( modifier * periods ); |
1931 | break; |
1932 | case rMonthly: |
1933 | { |
1934 | periods = 12 * ( toDate.date().year() - start.date().year() ) + |
1935 | ( toDate.date().month() - start.date().month() ); |
1936 | periods = qMax( 0L, periods ); |
1937 | if ( periods > 0 && mFrequency > 0 ) { |
1938 | periods += ( mFrequency - 1 - ( ( periods - 1 ) % mFrequency ) ); |
1939 | } |
1940 | // set the day to the first day of the month, so we don't have problems |
1941 | // with non-existent days like Feb 30 or April 31 |
1942 | start.setDate( QDate( start.date().year(), start.date().month(), 1 ) ); |
1943 | nextValid.setDate( start.date().addMonths( periods ) ); |
1944 | break; |
1945 | } |
1946 | case rYearly: |
1947 | periods = ( toDate.date().year() - start.date().year() ); |
1948 | periods = qMax( 0L, periods ); |
1949 | if ( periods > 0 && mFrequency > 0 ) { |
1950 | periods += ( mFrequency - 1 - ( ( periods - 1 ) % mFrequency ) ); |
1951 | } |
1952 | nextValid.setDate( start.date().addYears( periods ) ); |
1953 | break; |
1954 | default: |
1955 | break; |
1956 | } |
1957 | |
1958 | return Constraint( nextValid, type, mWeekStart ); |
1959 | } |
1960 | |
1961 | DateTimeList RecurrenceRule::Private::datesForInterval( const Constraint &interval, |
1962 | PeriodType type ) const |
1963 | { |
1964 | /* -) Loop through constraints, |
1965 | -) merge interval with each constraint |
1966 | -) if merged constraint is not consistent => ignore that constraint |
1967 | -) if complete => add that one date to the date list |
1968 | -) Loop through all missing fields => For each add the resulting |
1969 | */ |
1970 | DateTimeList lst; |
1971 | for ( int i = 0, iend = mConstraints.count(); i < iend; ++i ) { |
1972 | Constraint merged( interval ); |
1973 | if ( merged.merge( mConstraints[i] ) ) { |
1974 | // If the information is incomplete, we can't use this constraint |
1975 | if ( merged.year > 0 && merged.hour >= 0 && merged.minute >= 0 && merged.second >= 0 ) { |
1976 | // We have a valid constraint, so get all datetimes that match it andd |
1977 | // append it to all date/times of this interval |
1978 | QList<KDateTime> lstnew = merged.dateTimes( type ); |
1979 | lst += lstnew; |
1980 | } |
1981 | } |
1982 | } |
1983 | // Sort it so we can apply the BySetPos. Also some logic relies on this being sorted |
1984 | lst.sortUnique(); |
1985 | |
1986 | /*if ( lst.isEmpty() ) { |
1987 | kDebug() << " No Dates in Interval"; |
1988 | } else { |
1989 | kDebug() << " Dates:"; |
1990 | for ( int i = 0, iend = lst.count(); i < iend; ++i ) { |
1991 | kDebug()<< " -)" << dumpTime(lst[i]); |
1992 | } |
1993 | kDebug() << " ---------------------"; |
1994 | }*/ |
1995 | if ( !mBySetPos.isEmpty() ) { |
1996 | DateTimeList tmplst = lst; |
1997 | lst.clear(); |
1998 | for ( int i = 0, iend = mBySetPos.count(); i < iend; ++i ) { |
1999 | int pos = mBySetPos[i]; |
2000 | if ( pos > 0 ) { |
2001 | --pos; |
2002 | } |
2003 | if ( pos < 0 ) { |
2004 | pos += tmplst.count(); |
2005 | } |
2006 | if ( pos >= 0 && pos < tmplst.count() ) { |
2007 | lst.append( tmplst[pos] ); |
2008 | } |
2009 | } |
2010 | lst.sortUnique(); |
2011 | } |
2012 | |
2013 | return lst; |
2014 | } |
2015 | //@endcond |
2016 | |
2017 | void RecurrenceRule::dump() const |
2018 | { |
2019 | #ifndef NDEBUG |
2020 | kDebug(); |
2021 | if ( !d->mRRule.isEmpty() ) { |
2022 | kDebug() << " RRULE=" << d->mRRule; |
2023 | } |
2024 | kDebug() << " Read-Only:" << isReadOnly(); |
2025 | |
2026 | kDebug() << " Period type:" << recurrenceType() |
2027 | << ", frequency:" << frequency(); |
2028 | kDebug() << " #occurrences:" << duration(); |
2029 | kDebug() << " start date:" << dumpTime( startDt() ) |
2030 | << ", end date:" << dumpTime( endDt() ); |
2031 | |
2032 | #define dumpByIntList(list,label) \ |
2033 | if ( !list.isEmpty() ) {\ |
2034 | QStringList lst;\ |
2035 | for ( int i = 0, iend = list.count(); i < iend; ++i ) {\ |
2036 | lst.append( QString::number( list[i] ) );\ |
2037 | }\ |
2038 | kDebug() << " " << label << lst.join( ", " );\ |
2039 | } |
2040 | dumpByIntList( d->mBySeconds, "BySeconds: " ); |
2041 | dumpByIntList( d->mByMinutes, "ByMinutes: " ); |
2042 | dumpByIntList( d->mByHours, "ByHours: " ); |
2043 | if ( !d->mByDays.isEmpty() ) { |
2044 | QStringList lst; |
2045 | for ( int i = 0, iend = d->mByDays.count(); i < iend; ++i ) {\ |
2046 | lst.append( ( d->mByDays[i].pos() ? QString::number( d->mByDays[i].pos() ) : "" ) + |
2047 | DateHelper::dayName( d->mByDays[i].day() ) ); |
2048 | } |
2049 | kDebug() << " ByDays: " << lst.join( ", " ); |
2050 | } |
2051 | dumpByIntList( d->mByMonthDays, "ByMonthDays:" ); |
2052 | dumpByIntList( d->mByYearDays, "ByYearDays: " ); |
2053 | dumpByIntList( d->mByWeekNumbers, "ByWeekNr: " ); |
2054 | dumpByIntList( d->mByMonths, "ByMonths: " ); |
2055 | dumpByIntList( d->mBySetPos, "BySetPos: " ); |
2056 | #undef dumpByIntList |
2057 | |
2058 | kDebug() << " Week start:" << DateHelper::dayName( d->mWeekStart ); |
2059 | |
2060 | kDebug() << " Constraints:" ; |
2061 | // dump constraints |
2062 | for ( int i = 0, iend = d->mConstraints.count(); i < iend; ++i ) { |
2063 | d->mConstraints[i].dump(); |
2064 | } |
2065 | #endif |
2066 | } |
2067 | |
2068 | //@cond PRIVATE |
2069 | void Constraint::dump() const |
2070 | { |
2071 | kDebug() << " ~> Y=" << year |
2072 | << ", M=" << month |
2073 | << ", D=" << day |
2074 | << ", H=" << hour |
2075 | << ", m=" << minute |
2076 | << ", S=" << second |
2077 | << ", wd=" << weekday |
2078 | << ",#wd=" << weekdaynr |
2079 | << ", #w=" << weeknumber |
2080 | << ", yd=" << yearday; |
2081 | } |
2082 | //@endcond |
2083 | |
2084 | QString dumpTime( const KDateTime &dt ) |
2085 | { |
2086 | #ifndef NDEBUG |
2087 | if ( !dt.isValid() ) { |
2088 | return QString(); |
2089 | } |
2090 | QString result; |
2091 | if ( dt.isDateOnly() ) { |
2092 | result = dt.toString( "%a %Y-%m-%d %:Z" ); |
2093 | } else { |
2094 | result = dt.toString( "%a %Y-%m-%d %H:%M:%S %:Z" ); |
2095 | if ( dt.isSecondOccurrence() ) { |
2096 | result += QLatin1String( " (2nd)" ); |
2097 | } |
2098 | } |
2099 | if ( dt.timeSpec() == KDateTime::Spec::ClockTime() ) { |
2100 | result += QLatin1String( "Clock" ); |
2101 | } |
2102 | return result; |
2103 | #else |
2104 | Q_UNUSED( dt ); |
2105 | return QString(); |
2106 | #endif |
2107 | } |
2108 | |
2109 | KDateTime RecurrenceRule::startDt() const |
2110 | { |
2111 | return d->mDateStart; |
2112 | } |
2113 | |
2114 | RecurrenceRule::PeriodType RecurrenceRule::recurrenceType() const |
2115 | { |
2116 | return d->mPeriod; |
2117 | } |
2118 | |
2119 | uint RecurrenceRule::frequency() const |
2120 | { |
2121 | return d->mFrequency; |
2122 | } |
2123 | |
2124 | int RecurrenceRule::duration() const |
2125 | { |
2126 | return d->mDuration; |
2127 | } |
2128 | |
2129 | QString RecurrenceRule::rrule() const |
2130 | { |
2131 | return d->mRRule; |
2132 | } |
2133 | |
2134 | void RecurrenceRule::setRRule( const QString &rrule ) |
2135 | { |
2136 | d->mRRule = rrule; |
2137 | } |
2138 | |
2139 | bool RecurrenceRule::isReadOnly() const |
2140 | { |
2141 | return d->mIsReadOnly; |
2142 | } |
2143 | |
2144 | void RecurrenceRule::setReadOnly( bool readOnly ) |
2145 | { |
2146 | d->mIsReadOnly = readOnly; |
2147 | } |
2148 | |
2149 | bool RecurrenceRule::recurs() const |
2150 | { |
2151 | return d->mPeriod != rNone; |
2152 | } |
2153 | |
2154 | bool RecurrenceRule::allDay() const |
2155 | { |
2156 | return d->mAllDay; |
2157 | } |
2158 | |
2159 | const QList<int> &RecurrenceRule::bySeconds() const |
2160 | { |
2161 | return d->mBySeconds; |
2162 | } |
2163 | |
2164 | const QList<int> &RecurrenceRule::byMinutes() const |
2165 | { |
2166 | return d->mByMinutes; |
2167 | } |
2168 | |
2169 | const QList<int> &RecurrenceRule::byHours() const |
2170 | { |
2171 | return d->mByHours; |
2172 | } |
2173 | |
2174 | const QList<RecurrenceRule::WDayPos> &RecurrenceRule::byDays() const |
2175 | { |
2176 | return d->mByDays; |
2177 | } |
2178 | |
2179 | const QList<int> &RecurrenceRule::byMonthDays() const |
2180 | { |
2181 | return d->mByMonthDays; |
2182 | } |
2183 | |
2184 | const QList<int> &RecurrenceRule::byYearDays() const |
2185 | { |
2186 | return d->mByYearDays; |
2187 | } |
2188 | |
2189 | const QList<int> &RecurrenceRule::byWeekNumbers() const |
2190 | { |
2191 | return d->mByWeekNumbers; |
2192 | } |
2193 | |
2194 | const QList<int> &RecurrenceRule::byMonths() const |
2195 | { |
2196 | return d->mByMonths; |
2197 | } |
2198 | |
2199 | const QList<int> &RecurrenceRule::bySetPos() const |
2200 | { |
2201 | return d->mBySetPos; |
2202 | } |
2203 | |
2204 | short RecurrenceRule::weekStart() const |
2205 | { |
2206 | return d->mWeekStart; |
2207 | } |
2208 | |