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 #import <NGExtensions/NSCalendarDate+misc.h>
25 #import "iCalRecurrenceCalculator.h"
27 @interface iCalMonthlyRecurrenceCalculator : iCalRecurrenceCalculator
30 #import <NGExtensions/NGCalendarDateRange.h>
31 #import "iCalRecurrenceRule.h"
32 #import "NSCalendarDate+ICal.h"
35 @interface iCalRecurrenceCalculator(PrivateAPI)
36 - (NSCalendarDate *)lastInstanceStartDate;
39 // #define HEAVY_DEBUG 1
41 @implementation iCalMonthlyRecurrenceCalculator
43 typedef BOOL NGMonthSet[12];
44 typedef BOOL NGMonthDaySet[32]; // 0 is unused
46 static void NGMonthDaySet_clear(NGMonthDaySet *daySet) {
49 for (i = 1; i <= 31; i++)
53 static void NGMonthDaySet_copyOrUnion(NGMonthDaySet *base, NGMonthDaySet *new,
59 memcpy(base, new, sizeof(NGMonthDaySet));
61 for (i = 1; i <= 31; i++) {
68 static BOOL NGMonthDaySet_fillWithByMonthDay(NGMonthDaySet *daySet,
71 /* list of days in the month */
75 NGMonthDaySet_clear(daySet);
77 for (i = 0, count = [byMonthDay count], ok = YES; i < count; i++) {
78 int dayInMonth; /* -31..-1 and 1..31 */
80 if ((dayInMonth = [[byMonthDay objectAtIndex:i] intValue]) == 0) {
82 continue; /* invalid value */
84 if (dayInMonth > 31) {
86 continue; /* error, value to large */
88 if (dayInMonth < -31) {
90 continue; /* error, value to large */
93 /* adjust negative days */
96 /* eg: -1 == last day in month, 30 days => 30 */
97 dayInMonth = 32 - dayInMonth /* because we count from 1 */;
100 (*daySet)[dayInMonth] = YES;
105 static inline unsigned iCalDoWForNSDoW(int dow) {
107 case 0: return iCalWeekDaySunday;
108 case 1: return iCalWeekDayMonday;
109 case 2: return iCalWeekDayTuesday;
110 case 3: return iCalWeekDayWednesday;
111 case 4: return iCalWeekDayThursday;
112 case 5: return iCalWeekDayFriday;
113 case 6: return iCalWeekDaySaturday;
114 case 7: return iCalWeekDaySunday;
120 static NSString *dowEN[8] = {
121 @"SU", @"MO", @"TU", @"WE", @"TH", @"FR", @"SA", @"SU-"
125 static void NGMonthDaySet_fillWithByDayX(NGMonthDaySet *daySet,
127 unsigned firstDoWInMonth,
128 unsigned numberOfDaysInMonth,
131 // TODO: this is called 'X' because the API doesn't allow for full iCalendar
132 // functionality. The daymask must be a list of occurence+dow
133 register unsigned dayInMonth;
134 register int dow; /* current day of the week */
135 int occurrences[7] = { 0, 0, 0, 0, 0, 0, 0 } ;
137 NGMonthDaySet_clear(daySet);
139 if (occurrence1 >= 0) {
140 for (dayInMonth = 1, dow = firstDoWInMonth; dayInMonth<=31; dayInMonth++) {
143 if (dayMask & iCalDoWForNSDoW(dow)) {
144 if (occurrence1 == 0)
145 (*daySet)[dayInMonth] = YES;
146 else { /* occurrence1 > 0 */
147 occurrences[dow] = occurrences[dow] + 1;
149 if (occurrences[dow] == occurrence1)
150 (*daySet)[dayInMonth] = YES;
154 dow = (dow == 6 /* Sat */) ? 0 /* Sun */ : (dow + 1);
158 int lastDoWInMonthSet;
160 /* get the last dow in the set (not necessarily the month!) */
161 for (dayInMonth = 1, dow = firstDoWInMonth;
162 dayInMonth < numberOfDaysInMonth;dayInMonth++)
163 dow = (dow == 6 /* Sat */) ? 0 /* Sun */ : (dow + 1);
164 lastDoWInMonthSet = dow;
167 NSLog(@"LAST DOW IN SET: %i / %@",
168 lastDoWInMonthSet, dowEN[lastDoWInMonthSet]);
170 /* start at the end of the set */
171 for (dayInMonth = numberOfDaysInMonth, dow = lastDoWInMonthSet;
172 dayInMonth >= 1; dayInMonth--) {
176 NSLog(@" CHECK day-of-month %02i, "
177 @" dow=%i/%@ (first=%i/%@, last=%i/%@)",
180 firstDoWInMonth, dowEN[firstDoWInMonth],
181 lastDoWInMonthSet, dowEN[lastDoWInMonthSet]
185 if (dayMask & iCalDoWForNSDoW(dow)) {
186 occurrences[dow] = occurrences[dow] + 1;
188 NSLog(@" MATCH %i/%@ count: %i occurences=%i",
189 dow, dowEN[dow], occurrences[dow], occurrence1);
192 if (occurrences[dow] == -occurrence1) {
194 NSLog(@" COUNT MATCH");
196 (*daySet)[dayInMonth] = YES;
200 dow = (dow == 0 /* Sun */) ? 6 /* Sat */ : (dow - 1);
205 - (BOOL)_addInstanceWithStartDate:(NSCalendarDate *)_startDate
206 limitDate:(NSCalendarDate *)_until
207 limitRange:(NGCalendarDateRange *)_r
208 toArray:(NSMutableArray *)_ranges
210 NGCalendarDateRange *r;
213 /* check whether we are still in the limits */
215 // TODO: I think we should check in here whether we succeeded the
216 // repeatCount. Currently we precalculate that info in the
217 // -lastInstanceStartDate method.
219 /* Note: the 'until' in the rrule is inclusive as per spec */
220 if ([_until compare:_startDate] == NSOrderedAscending)
221 /* start after until */
222 return NO; /* Note: we assume that the algorithm is sequential */
225 /* create end date */
227 end = [_startDate addTimeInterval:[self->firstRange duration]];
228 [end setTimeZone:[_startDate timeZone]];
230 /* create range and check whether its in the requested range */
232 r = [[NGCalendarDateRange alloc] initWithStartDate:_startDate endDate:end];
233 if ([_r containsDateRange:r])
234 [_ranges addObject:r];
235 [r release]; r = nil;
240 - (NSArray *)recurrenceRangesWithinCalendarDateRange:(NGCalendarDateRange *)_r{
242 // TODO: check whether this is OK for multiday-events!
243 NSMutableArray *ranges;
244 NSTimeZone *timeZone;
245 NSCalendarDate *eventStartDate, *rStart, *rEnd, *until;
247 unsigned monthIdxInRange, numberOfMonthsInRange, interval;
249 NGMonthSet byMonthList = { // TODO: fill from rrule, this is the default
250 /* enable all months of the year */
251 YES, YES, YES, YES, YES, YES,
252 YES, YES, YES, YES, YES, YES
254 NSArray *byMonthDay; // array of ints (-31..-1 and 1..31)
255 NGMonthDaySet byMonthDaySet;
257 eventStartDate = [self->firstRange startDate];
258 eventDayOfMonth = [eventStartDate dayOfMonth];
259 timeZone = [eventStartDate timeZone];
260 rStart = [_r startDate];
262 interval = [self->rrule repeatInterval];
263 until = [self lastInstanceStartDate]; // TODO: maybe replace
264 byMonthDay = [self->rrule byMonthDay];
267 /* 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 */