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 */
270 if ([until compare:rStart] == NSOrderedAscending) /* until before start */
272 if ([until compare:rEnd] == NSOrderedDescending) /* end before until */
273 rEnd = until; // TODO: why is that? end is _before_ until?
277 /* precalculate month days (same for all instances) */
279 if (byMonthDay != nil) {
281 NSLog(@"byMonthDay: %@", byMonthDay);
283 NGMonthDaySet_fillWithByMonthDay(&byMonthDaySet, byMonthDay);
287 // TODO: I think the 'diff' is to skip recurrence which are before the
288 // requested range. Not sure whether this is actually possible, eg
289 // the repeatCount must be processed from the start.
290 diff = [eventStartDate monthsBetweenDate:rStart];
291 if ((diff != 0) && [rStart compare:eventStartDate] == NSOrderedAscending)
294 numberOfMonthsInRange = [rStart monthsBetweenDate:rEnd] + 1;
295 ranges = [NSMutableArray arrayWithCapacity:numberOfMonthsInRange];
298 Note: we do not add 'eventStartDate', this is intentional, the event date
299 itself is _not_ necessarily part of the sequence, eg with monthly
303 for (monthIdxInRange = 0; monthIdxInRange < numberOfMonthsInRange;
305 NSCalendarDate *cursor;
306 unsigned numDaysInMonth;
307 int monthIdxInRecurrence, dom;
308 NGMonthDaySet monthDays;
309 BOOL didByFill, doCont;
311 monthIdxInRecurrence = diff + monthIdxInRange;
313 if (monthIdxInRecurrence < 0)
316 /* first check whether we are in the interval */
318 if ((monthIdxInRecurrence % interval) != 0)
322 Then the sequence is:
323 - check whether the month is in the BYMONTH list
327 Note: the function below adds exactly a month, eg:
328 2007-01-30 + 1month => 2007-02-*28*!!
330 cursor = [eventStartDate dateByAddingYears:0
331 months:(diff + monthIdxInRange)
333 [cursor setTimeZone:timeZone];
334 numDaysInMonth = [cursor numberOfDaysInMonth];
337 /* check whether we match the bymonth specification */
339 if (!byMonthList[[cursor monthOfYear] - 1])
343 /* check 'day level' byXYZ rules */
347 if (byMonthDay != nil) { /* list of days in the month */
348 NGMonthDaySet_copyOrUnion(&monthDays, &byMonthDaySet, !didByFill);
352 if ([self->rrule byDayMask] != 0) { // TODO: replace the mask with an array
353 NGMonthDaySet ruleset;
354 unsigned firstDoWInMonth;
356 firstDoWInMonth = [[cursor firstDayOfMonth] dayOfWeek];
358 NGMonthDaySet_fillWithByDayX(&ruleset,
359 [self->rrule byDayMask],
361 [cursor numberOfDaysInMonth],
362 [self->rrule byDayOccurence1]);
363 NGMonthDaySet_copyOrUnion(&monthDays, &ruleset, !didByFill);
368 /* no rules applied, take the dayOfMonth of the startDate */
369 NGMonthDaySet_clear(&monthDays);
370 monthDays[eventDayOfMonth] = YES;
373 // TODO: add processing of byhour/byminute/bysecond etc
376 Next step is to create NSCalendarDate instances from our 'monthDays'
377 set. We walk over each day of the 'monthDays' set. If its flag isn't
379 If its set, we add the date to the instance.
381 The 'cursor' is the *startdate* of the event (not necessarily a
382 component of the sequence!) plus the currently processed month.
384 startdate: 2007-01-30
385 cursor[1]: 2007-01-30
386 cursor[2]: 2007-02-28 <== Note: we have February!
389 for (dom = 1, doCont = YES; dom <= numDaysInMonth && doCont; dom++) {
390 NSCalendarDate *start;
395 // TODO: what is this good for?
397 Here we need to correct the date. Remember that the startdate given in
398 the event is not necessarily a date of the sequence!
400 The 'numDaysInMonth' localvar contains the number of days in the
401 current month (eg 31 for Januar, 28 for most February's, etc)
403 Eg: MONTHLY;BYDAY=-1WE (last wednesday, every month)
405 cursor: 2007-01-30 (eventDayOfMonth = 30)
406 =>start: 2007-01-31 (dom = 31)
407 cursor: 2007-02-28 (eventDayOfMonth = 30)
408 =>start: 2007-02-28 (dom = 28)
410 Note: in case the cursor already had an event-day overflow, that is the
411 'eventDayOfMonth' is bigger than the 'numDaysInMonth', the cursor
412 will already be corrected!
414 start was: 2007-01-30
415 cursor will be: 2007-02-28
417 if (eventDayOfMonth == dom) {
422 eventDayOfMonth > numDaysInMonth ? numDaysInMonth : eventDayOfMonth;
424 start = [cursor dateByAddingYears:0 months:0 days:(dom - maxDay)];
428 Setup for 2007-02-28, MONTHLY;BYDAY=-1WE.
432 start: 2007-02-25 <== WRONG
436 NSLog(@"DOM %i EDOM %i NUMDAYS %i START: %@ CURSOR: %@",
437 dom, eventDayOfMonth, numDaysInMonth,
440 doCont = [self _addInstanceWithStartDate:start
445 if (!doCont) break; /* reached some limit */
450 - (NSCalendarDate *)lastInstanceStartDate {
451 if ([self->rrule repeatCount] > 0) {
452 NSCalendarDate *until;
453 unsigned months, interval;
455 interval = [self->rrule repeatInterval];
456 months = [self->rrule repeatCount] - 1 /* the first counts as one! */;
461 until = [[self->firstRange startDate] dateByAddingYears:0
466 return [super lastInstanceStartDate];
469 @end /* iCalMonthlyRecurrenceCalculator */