2 Copyright (C) 2004-2005 SKYRIX Software AG
4 This file is part of SOPE.
6 SOPE is free software; you can redistribute it and/or modify it under
7 the terms of the GNU Lesser General Public License as published by the
8 Free Software Foundation; either version 2, or (at your option) any
11 SOPE is distributed in the hope that it will be useful, but WITHOUT ANY
12 WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
14 License for more details.
16 You should have received a copy of the GNU Lesser General Public
17 License along with SOPE; see the file COPYING. If not, write to the
18 Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
22 #import <NGExtensions/NSCalendarDate+misc.h>
24 #import "iCalRecurrenceCalculator.h"
26 @interface iCalMonthlyRecurrenceCalculator : iCalRecurrenceCalculator
29 #import <NGExtensions/NGCalendarDateRange.h>
30 #import "iCalRecurrenceRule.h"
31 #import "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 YES, YES, YES, YES, YES, YES,
250 YES, YES, YES, YES, YES, YES
252 NSArray *byMonthDay = nil; // array of ints (-31..-1 and 1..31)
253 NGMonthDaySet byMonthDaySet;
255 eventStartDate = [self->firstRange startDate];
256 eventDayOfMonth = [eventStartDate dayOfMonth];
257 timeZone = [eventStartDate timeZone];
258 rStart = [_r startDate];
260 interval = [self->rrule repeatInterval];
261 until = [self lastInstanceStartDate]; // TODO: maybe replace
262 byMonthDay = [self->rrule byMonthDay];
265 /* check whether the range to be processed is beyond the 'until' date */
268 if ([until compare:rStart] == NSOrderedAscending) /* until before start */
270 if ([until compare:rEnd] == NSOrderedDescending) /* end before until */
271 rEnd = until; // TODO: why is that? end is _before_ until?
275 /* precalculate month days (same for all instances) */
277 if (byMonthDay != nil)
278 NGMonthDaySet_fillWithByMonthDay(&byMonthDaySet, byMonthDay);
281 // TODO: I think the 'diff' is to skip recurrence which are before the
282 // requested range. Not sure whether this is actually possible, eg
283 // the repeatCount must be processed from the start.
284 diff = [eventStartDate monthsBetweenDate:rStart];
285 if ((diff != 0) && [rStart compare:eventStartDate] == NSOrderedAscending)
288 numberOfMonthsInRange = [rStart monthsBetweenDate:rEnd] + 1;
289 ranges = [NSMutableArray arrayWithCapacity:numberOfMonthsInRange];
292 Note: we do not add 'eventStartDate', this is intentional, the event date
293 itself is _not_ necessarily part of the sequence, eg with monthly
297 for (monthIdxInRange = 0; monthIdxInRange < numberOfMonthsInRange;
299 NSCalendarDate *cursor;
300 unsigned numDaysInMonth;
301 int monthIdxInRecurrence, dom;
302 NGMonthDaySet monthDays;
303 BOOL didByFill, doCont;
305 monthIdxInRecurrence = diff + monthIdxInRange;
307 if (monthIdxInRecurrence < 0)
310 /* first check whether we are in the interval */
312 if ((monthIdxInRecurrence % interval) != 0)
316 Then the sequence is:
317 - check whether the month is in the BYMONTH list
320 cursor = [eventStartDate dateByAddingYears:0
321 months:(diff + monthIdxInRange)
323 [cursor setTimeZone:timeZone];
324 numDaysInMonth = [cursor numberOfDaysInMonth];
327 /* check whether we match the bymonth specification */
329 if (!byMonthList[[cursor monthOfYear] - 1])
333 /* check 'day level' byXYZ rules */
337 if (byMonthDay != nil) { /* list of days in the month */
338 NGMonthDaySet_copyOrUnion(&monthDays, &byMonthDaySet, !didByFill);
342 if ([self->rrule byDayMask] != 0) { // TODO: replace the mask with an array
343 NGMonthDaySet ruleset;
344 unsigned firstDoWInMonth;
346 firstDoWInMonth = [[cursor firstDayOfMonth] dayOfWeek];
348 NGMonthDaySet_fillWithByDayX(&ruleset,
349 [self->rrule byDayMask],
351 [cursor numberOfDaysInMonth],
352 [self->rrule byDayOccurence1]);
353 NGMonthDaySet_copyOrUnion(&monthDays, &ruleset, !didByFill);
358 /* no rules applied, take the dayOfMonth of the startDate */
359 NGMonthDaySet_clear(&monthDays);
360 monthDays[eventDayOfMonth] = YES;
363 // TODO: add processing of byhour/byminute/bysecond etc
365 for (dom = 1, doCont = YES; dom <= numDaysInMonth && doCont; dom++) {
366 NSCalendarDate *start;
371 if (eventDayOfMonth == dom)
374 start = [cursor dateByAddingYears:0 months:0
375 days:(dom - eventDayOfMonth)];
378 doCont = [self _addInstanceWithStartDate:start
383 if (!doCont) break; /* reached some limit */
388 - (NSCalendarDate *)lastInstanceStartDate {
389 if ([self->rrule repeatCount] > 0) {
390 NSCalendarDate *until;
391 unsigned months, interval;
393 interval = [self->rrule repeatInterval];
394 months = [self->rrule repeatCount] - 1 /* the first counts as one! */;
399 until = [[self->firstRange startDate] dateByAddingYears:0
404 return [super lastInstanceStartDate];
407 @end /* iCalMonthlyRecurrenceCalculator */