2 Copyright (C) 2004-2007 SKYRIX Software AG
3 Copyright (C) 2007 Helge Hess
5 This file is part of SOPE.
7 SOPE is free software; you can redistribute it and/or modify it under
8 the terms of the GNU Lesser General Public License as published by the
9 Free Software Foundation; either version 2, or (at your option) any
12 SOPE is distributed in the hope that it will be useful, but WITHOUT ANY
13 WARRANTY; without even the implied warranty of MERCHANTABILITY or
14 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
15 License for more details.
17 You should have received a copy of the GNU Lesser General Public
18 License along with SOPE; see the file COPYING. If not, write to the
19 Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
23 #include "iCalRecurrenceCalculator.h"
25 @interface iCalMonthlyRecurrenceCalculator : iCalRecurrenceCalculator
28 #include <NGExtensions/NGCalendarDateRange.h>
29 #include "iCalRecurrenceRule.h"
30 #include "NSCalendarDate+ICal.h"
34 @interface iCalRecurrenceCalculator(PrivateAPI)
35 - (NSCalendarDate *)lastInstanceStartDate;
38 // #define HEAVY_DEBUG 1
40 @implementation iCalMonthlyRecurrenceCalculator
42 typedef BOOL NGMonthSet[12];
43 typedef BOOL NGMonthDaySet[32]; // 0 is unused
45 static void NGMonthDaySet_clear(NGMonthDaySet *daySet) {
48 for (i = 1; i <= 31; i++)
52 static void NGMonthDaySet_copyOrUnion(NGMonthDaySet *base, NGMonthDaySet *new,
58 memcpy(base, new, sizeof(NGMonthDaySet));
60 for (i = 1; i <= 31; i++) {
67 static BOOL NGMonthDaySet_fillWithByMonthDay(NGMonthDaySet *daySet,
70 /* list of days in the month */
74 NGMonthDaySet_clear(daySet);
76 for (i = 0, count = [byMonthDay count], ok = YES; i < count; i++) {
77 int dayInMonth; /* -31..-1 and 1..31 */
79 if ((dayInMonth = [[byMonthDay objectAtIndex:i] intValue]) == 0) {
81 continue; /* invalid value */
83 if (dayInMonth > 31) {
85 continue; /* error, value to large */
87 if (dayInMonth < -31) {
89 continue; /* error, value to large */
92 /* adjust negative days */
95 /* eg: -1 == last day in month, 30 days => 30 */
96 dayInMonth = 32 - dayInMonth /* because we count from 1 */;
99 (*daySet)[dayInMonth] = YES;
104 static inline unsigned iCalDoWForNSDoW(int dow) {
106 case 0: return iCalWeekDaySunday;
107 case 1: return iCalWeekDayMonday;
108 case 2: return iCalWeekDayTuesday;
109 case 3: return iCalWeekDayWednesday;
110 case 4: return iCalWeekDayThursday;
111 case 5: return iCalWeekDayFriday;
112 case 6: return iCalWeekDaySaturday;
113 case 7: return iCalWeekDaySunday;
119 static NSString *dowEN[8] = {
120 @"SU", @"MO", @"TU", @"WE", @"TH", @"FR", @"SA", @"SU-"
124 static void NGMonthDaySet_fillWithByDayX(NGMonthDaySet *daySet,
126 unsigned firstDoWInMonth,
127 unsigned numberOfDaysInMonth,
130 // TODO: this is called 'X' because the API doesn't allow for full iCalendar
131 // functionality. The daymask must be a list of occurence+dow
132 register unsigned dayInMonth;
133 register int dow; /* current day of the week */
134 int occurrences[7] = { 0, 0, 0, 0, 0, 0, 0 } ;
136 NGMonthDaySet_clear(daySet);
138 if (occurrence1 >= 0) {
139 for (dayInMonth = 1, dow = firstDoWInMonth; dayInMonth<=31; dayInMonth++) {
142 if (dayMask & iCalDoWForNSDoW(dow)) {
143 if (occurrence1 == 0)
144 (*daySet)[dayInMonth] = YES;
145 else { /* occurrence1 > 0 */
146 occurrences[dow] = occurrences[dow] + 1;
148 if (occurrences[dow] == occurrence1)
149 (*daySet)[dayInMonth] = YES;
153 dow = (dow == 6 /* Sat */) ? 0 /* Sun */ : (dow + 1);
157 int lastDoWInMonthSet;
159 /* get the last dow in the set (not necessarily the month!) */
160 for (dayInMonth = 1, dow = firstDoWInMonth;
161 dayInMonth < numberOfDaysInMonth;dayInMonth++)
162 dow = (dow == 6 /* Sat */) ? 0 /* Sun */ : (dow + 1);
163 lastDoWInMonthSet = dow;
166 NSLog(@"LAST DOW IN SET: %i / %@",
167 lastDoWInMonthSet, dowEN[lastDoWInMonthSet]);
169 /* start at the end of the set */
170 for (dayInMonth = numberOfDaysInMonth, dow = lastDoWInMonthSet;
171 dayInMonth >= 1; dayInMonth--) {
175 NSLog(@" CHECK day-of-month %02i, "
176 @" dow=%i/%@ (first=%i/%@, last=%i/%@)",
179 firstDoWInMonth, dowEN[firstDoWInMonth],
180 lastDoWInMonthSet, dowEN[lastDoWInMonthSet]
184 if (dayMask & iCalDoWForNSDoW(dow)) {
185 occurrences[dow] = occurrences[dow] + 1;
187 NSLog(@" MATCH %i/%@ count: %i occurences=%i",
188 dow, dowEN[dow], occurrences[dow], occurrence1);
191 if (occurrences[dow] == -occurrence1) {
193 NSLog(@" COUNT MATCH");
195 (*daySet)[dayInMonth] = YES;
199 dow = (dow == 0 /* Sun */) ? 6 /* Sat */ : (dow - 1);
204 - (BOOL)_addInstanceWithStartDate:(NSCalendarDate *)_startDate
205 limitDate:(NSCalendarDate *)_until
206 limitRange:(NGCalendarDateRange *)_r
207 toArray:(NSMutableArray *)_ranges
209 NGCalendarDateRange *r;
212 /* check whether we are still in the limits */
214 // TODO: I think we should check in here whether we succeeded the
215 // repeatCount. Currently we precalculate that info in the
216 // -lastInstanceStartDate method.
218 /* Note: the 'until' in the rrule is inclusive as per spec */
219 if ([_until compare:_startDate] == NSOrderedAscending)
220 /* start after until */
221 return NO; /* Note: we assume that the algorithm is sequential */
224 /* create end date */
226 end = [_startDate addTimeInterval:[self->firstRange duration]];
227 [end setTimeZone:[_startDate timeZone]];
229 /* create range and check whether its in the requested range */
231 r = [[NGCalendarDateRange alloc] initWithStartDate:_startDate endDate:end];
232 if ([_r containsDateRange:r])
233 [_ranges addObject:r];
234 [r release]; r = nil;
239 - (NSArray *)recurrenceRangesWithinCalendarDateRange:(NGCalendarDateRange *)_r{
241 // TODO: check whether this is OK for multiday-events!
242 NSMutableArray *ranges;
243 NSTimeZone *timeZone;
244 NSCalendarDate *eventStartDate, *rStart, *rEnd, *until;
246 unsigned monthIdxInRange, numberOfMonthsInRange, interval;
248 NGMonthSet byMonthList = { // TODO: fill from rrule, this is the default
249 /* enable all months of the year */
250 YES, YES, YES, YES, YES, YES,
251 YES, YES, YES, YES, YES, YES
253 NSArray *byMonthDay; // array of ints (-31..-1 and 1..31)
254 NGMonthDaySet byMonthDaySet;
256 eventStartDate = [self->firstRange startDate];
257 eventDayOfMonth = [eventStartDate dayOfMonth];
258 timeZone = [eventStartDate timeZone];
259 rStart = [_r startDate];
261 interval = [self->rrule repeatInterval];
262 until = [self lastInstanceStartDate]; // TODO: maybe replace
263 byMonthDay = [self->rrule byMonthDay];
266 /* check whether the range to be processed is beyond the 'until' date */
269 if ([until compare:rStart] == NSOrderedAscending) /* until before start */
271 if ([until compare:rEnd] == NSOrderedDescending) /* end before until */
272 rEnd = until; // TODO: why is that? end is _before_ until?
276 /* precalculate month days (same for all instances) */
278 if (byMonthDay != nil) {
280 NSLog(@"byMonthDay: %@", byMonthDay);
282 NGMonthDaySet_fillWithByMonthDay(&byMonthDaySet, byMonthDay);
286 // TODO: I think the 'diff' is to skip recurrence which are before the
287 // requested range. Not sure whether this is actually possible, eg
288 // the repeatCount must be processed from the start.
289 diff = [eventStartDate monthsBetweenDate:rStart];
290 if ((diff != 0) && [rStart compare:eventStartDate] == NSOrderedAscending)
293 numberOfMonthsInRange = [rStart monthsBetweenDate:rEnd] + 1;
294 ranges = [NSMutableArray arrayWithCapacity:numberOfMonthsInRange];
297 Note: we do not add 'eventStartDate', this is intentional, the event date
298 itself is _not_ necessarily part of the sequence, eg with monthly
302 for (monthIdxInRange = 0; monthIdxInRange < numberOfMonthsInRange;
304 NSCalendarDate *cursor;
305 unsigned numDaysInMonth;
306 int monthIdxInRecurrence, dom;
307 NGMonthDaySet monthDays;
308 BOOL didByFill, doCont;
310 monthIdxInRecurrence = diff + monthIdxInRange;
312 if (monthIdxInRecurrence < 0)
315 /* first check whether we are in the interval */
317 if ((monthIdxInRecurrence % interval) != 0)
321 Then the sequence is:
322 - check whether the month is in the BYMONTH list
326 Note: the function below adds exactly a month, eg:
327 2007-01-30 + 1month => 2007-02-*28*!!
329 cursor = [eventStartDate dateByAddingYears:0
330 months:(diff + monthIdxInRange)
332 [cursor setTimeZone:timeZone];
333 numDaysInMonth = [cursor numberOfDaysInMonth];
336 /* check whether we match the bymonth specification */
338 if (!byMonthList[[cursor monthOfYear] - 1])
342 /* check 'day level' byXYZ rules */
346 if (byMonthDay != nil) { /* list of days in the month */
347 NGMonthDaySet_copyOrUnion(&monthDays, &byMonthDaySet, !didByFill);
351 if ([self->rrule byDayMask] != 0) { // TODO: replace the mask with an array
352 NGMonthDaySet ruleset;
353 unsigned firstDoWInMonth;
355 firstDoWInMonth = [[cursor firstDayOfMonth] dayOfWeek];
357 NGMonthDaySet_fillWithByDayX(&ruleset,
358 [self->rrule byDayMask],
360 [cursor numberOfDaysInMonth],
361 [self->rrule byDayOccurence1]);
362 NGMonthDaySet_copyOrUnion(&monthDays, &ruleset, !didByFill);
367 /* no rules applied, take the dayOfMonth of the startDate */
368 NGMonthDaySet_clear(&monthDays);
369 monthDays[eventDayOfMonth] = YES;
372 // TODO: add processing of byhour/byminute/bysecond etc
375 Next step is to create NSCalendarDate instances from our 'monthDays'
376 set. We walk over each day of the 'monthDays' set. If its flag isn't
378 If its set, we add the date to the instance.
380 The 'cursor' is the *startdate* of the event (not necessarily a
381 component of the sequence!) plus the currently processed month.
383 startdate: 2007-01-30
384 cursor[1]: 2007-01-30
385 cursor[2]: 2007-02-28 <== Note: we have February!
388 for (dom = 1, doCont = YES; dom <= numDaysInMonth && doCont; dom++) {
389 NSCalendarDate *start;
394 // TODO: what is this good for?
396 Here we need to correct the date. Remember that the startdate given in
397 the event is not necessarily a date of the sequence!
399 The 'numDaysInMonth' localvar contains the number of days in the
400 current month (eg 31 for Januar, 28 for most February's, etc)
402 Eg: MONTHLY;BYDAY=-1WE (last wednesday, every month)
404 cursor: 2007-01-30 (eventDayOfMonth = 30)
405 =>start: 2007-01-31 (dom = 31)
406 cursor: 2007-02-28 (eventDayOfMonth = 30)
407 =>start: 2007-02-28 (dom = 28)
409 Note: in case the cursor already had an event-day overflow, that is the
410 'eventDayOfMonth' is bigger than the 'numDaysInMonth', the cursor
411 will already be corrected!
413 start was: 2007-01-30
414 cursor will be: 2007-02-28
416 if (eventDayOfMonth == dom) {
421 eventDayOfMonth > numDaysInMonth ? numDaysInMonth : eventDayOfMonth;
423 start = [cursor dateByAddingYears:0 months:0 days:(dom - maxDay)];
427 Setup for 2007-02-28, MONTHLY;BYDAY=-1WE.
431 start: 2007-02-25 <== WRONG
435 NSLog(@"DOM %i EDOM %i NUMDAYS %i START: %@ CURSOR: %@",
436 dom, eventDayOfMonth, numDaysInMonth,
439 doCont = [self _addInstanceWithStartDate:start
444 if (!doCont) break; /* reached some limit */
449 - (NSCalendarDate *)lastInstanceStartDate {
450 if ([self->rrule repeatCount] > 0) {
451 NSCalendarDate *until;
452 unsigned months, interval;
454 interval = [self->rrule repeatInterval];
455 months = [self->rrule repeatCount] - 1 /* the first counts as one! */;
460 until = [[self->firstRange startDate] dateByAddingYears:0
465 return [super lastInstanceStartDate];
468 @end /* iCalMonthlyRecurrenceCalculator */