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 #include "iCalRecurrenceCalculator.h"
24 @interface iCalMonthlyRecurrenceCalculator : iCalRecurrenceCalculator
27 #include <NGExtensions/NGCalendarDateRange.h>
28 #include "iCalRecurrenceRule.h"
29 #include "NSCalendarDate+ICal.h"
32 @interface iCalRecurrenceCalculator(PrivateAPI)
33 - (NSCalendarDate *)lastInstanceStartDate;
36 @implementation iCalMonthlyRecurrenceCalculator
38 typedef BOOL NGMonthSet[12];
39 typedef BOOL NGMonthDaySet[32]; // 0 is unused
41 static void NGMonthDaySet_clear(NGMonthDaySet *daySet) {
44 for (i = 1; i <= 31; i++)
48 static void NGMonthDaySet_copyOrUnion(NGMonthDaySet *base, NGMonthDaySet *new,
54 memcpy(base, new, sizeof(NGMonthDaySet));
56 for (i = 1; i <= 31; i++) {
63 static BOOL NGMonthDaySet_fillWithByMonthDay(NGMonthDaySet *daySet,
66 /* list of days in the month */
70 NGMonthDaySet_clear(daySet);
72 for (i = 0, count = [byMonthDay count], ok = YES; i < count; i++) {
73 int dayInMonth; /* -31..-1 and 1..31 */
75 if ((dayInMonth = [[byMonthDay objectAtIndex:i] intValue]) == 0) {
77 continue; /* invalid value */
79 if (dayInMonth > 31) {
81 continue; /* error, value to large */
83 if (dayInMonth < -31) {
85 continue; /* error, value to large */
88 /* adjust negative days */
91 /* eg: -1 == last day in month, 30 days => 30 */
92 dayInMonth = 32 - dayInMonth /* because we count from 1 */;
95 (*daySet)[dayInMonth] = YES;
100 static inline unsigned iCalDoWForNSDoW(int dow) {
102 case 0: return iCalWeekDaySunday;
103 case 1: return iCalWeekDayMonday;
104 case 2: return iCalWeekDayTuesday;
105 case 3: return iCalWeekDayWednesday;
106 case 4: return iCalWeekDayThursday;
107 case 5: return iCalWeekDayFriday;
108 case 6: return iCalWeekDaySaturday;
109 case 7: return iCalWeekDaySunday;
114 static void NGMonthDaySet_fillWithByDayX(NGMonthDaySet *daySet,
116 unsigned firstDoWInMonth,
119 // TODO: this is called 'X' because the API doesn't allow for full iCalendar
120 // functionality. The daymask must be a list of occurence+dow
121 register unsigned dayInMonth;
122 register unsigned dow; /* current day of the week */
123 int occurrences[7] = { 0, 0, 0, 0, 0, 0, 0 } ;
125 NGMonthDaySet_clear(daySet);
127 for (dayInMonth = 1, dow = firstDoWInMonth; dayInMonth <= 31; dayInMonth++) {
130 if (dayMask & iCalDoWForNSDoW(dow)) {
131 if (occurrence1 == 0)
132 (*daySet)[dayInMonth] = YES;
134 occurrences[dow] = occurrences[dow] + 1;
136 if (occurrences[dow] == occurrence1)
137 (*daySet)[dayInMonth] = YES;
141 dow = (dow == 6 /* Sat */) ? 0 /* Sun */ : (dow + 1);
145 - (BOOL)_addInstanceWithStartDate:(NSCalendarDate *)_startDate
146 limitDate:(NSCalendarDate *)_until
147 limitRange:(NGCalendarDateRange *)_r
148 toArray:(NSMutableArray *)_ranges
150 NGCalendarDateRange *r;
153 /* check whether we are still in the limits */
155 // TODO: I think we should check in here whether we succeeded the
156 // repeatCount. Currently we precalculate that info in the
157 // -lastInstanceStartDate method.
159 /* Note: the 'until' in the rrule is inclusive as per spec */
160 if ([_until compare:_startDate] == NSOrderedAscending)
161 /* start after until */
162 return NO; /* Note: we assume that the algorithm is sequential */
165 /* create end date */
167 end = [_startDate addTimeInterval:[self->firstRange duration]];
168 [end setTimeZone:[_startDate timeZone]];
170 /* create range and check whether its in the requested range */
172 r = [[NGCalendarDateRange alloc] initWithStartDate:_startDate endDate:end];
173 if ([_r containsDateRange:r])
174 [_ranges addObject:r];
175 [r release]; r = nil;
180 - (NSArray *)recurrenceRangesWithinCalendarDateRange:(NGCalendarDateRange *)_r{
182 // TODO: check whether this is OK for multiday-events!
183 NSMutableArray *ranges;
184 NSTimeZone *timeZone;
185 NSCalendarDate *eventStartDate, *rStart, *rEnd, *until;
187 unsigned monthIdxInRange, numberOfMonthsInRange, interval;
189 NGMonthSet byMonthList = { // TODO: fill from rrule, this is the default
190 YES, YES, YES, YES, YES, YES,
191 YES, YES, YES, YES, YES, YES
193 NSArray *byMonthDay = nil; // array of ints (-31..-1 and 1..31)
194 NGMonthDaySet byMonthDaySet;
196 eventStartDate = [self->firstRange startDate];
197 eventDayOfMonth = [eventStartDate dayOfMonth];
198 timeZone = [eventStartDate timeZone];
199 rStart = [_r startDate];
201 interval = [self->rrule repeatInterval];
202 until = [self lastInstanceStartDate]; // TODO: maybe replace
203 byMonthDay = [self->rrule byMonthDay];
206 /* check whether the range to be processed is beyond the 'until' date */
209 if ([until compare:rStart] == NSOrderedAscending) /* until before start */
211 if ([until compare:rEnd] == NSOrderedDescending) /* end before until */
212 rEnd = until; // TODO: why is that? end is _before_ until?
216 /* precalculate month days (same for all instances) */
218 if (byMonthDay != nil)
219 NGMonthDaySet_fillWithByMonthDay(&byMonthDaySet, byMonthDay);
222 // TODO: I think the 'diff' is to skip recurrence which are before the
223 // requested range. Not sure whether this is actually possible, eg
224 // the repeatCount must be processed from the start.
225 diff = [eventStartDate monthsBetweenDate:rStart];
226 if ((diff != 0) && [rStart compare:eventStartDate] == NSOrderedAscending)
229 numberOfMonthsInRange = [rStart monthsBetweenDate:rEnd] + 1;
230 ranges = [NSMutableArray arrayWithCapacity:numberOfMonthsInRange];
233 Note: we do not add 'eventStartDate', this is intentional, the event date
234 itself is _not_ necessarily part of the sequence, eg with monthly
238 for (monthIdxInRange = 0; monthIdxInRange < numberOfMonthsInRange;
240 NSCalendarDate *cursor;
241 unsigned numDaysInMonth;
242 int monthIdxInRecurrence, dom;
243 NGMonthDaySet monthDays;
244 BOOL didByFill, doCont;
246 monthIdxInRecurrence = diff + monthIdxInRange;
248 if (monthIdxInRecurrence < 0)
251 /* first check whether we are in the interval */
253 if ((monthIdxInRecurrence % interval) != 0)
257 Then the sequence is:
258 - check whether the month is in the BYMONTH list
261 cursor = [eventStartDate dateByAddingYears:0
262 months:(diff + monthIdxInRange)
264 [cursor setTimeZone:timeZone];
265 numDaysInMonth = [cursor numberOfDaysInMonth];
268 /* check whether we match the bymonth specification */
270 if (!byMonthList[[cursor monthOfYear] - 1])
274 /* check 'day level' byXYZ rules */
278 if (byMonthDay != nil) { /* list of days in the month */
279 NGMonthDaySet_copyOrUnion(&monthDays, &byMonthDaySet, !didByFill);
283 if ([self->rrule byDayMask] != 0) { // TODO: replace the mask with an array
284 NGMonthDaySet ruleset;
285 unsigned firstDoWInMonth;
287 firstDoWInMonth = [[cursor firstDayOfMonth] dayOfWeek];
289 NGMonthDaySet_fillWithByDayX(&ruleset,
290 [self->rrule byDayMask],
292 [self->rrule byDayOccurence1]);
293 NGMonthDaySet_copyOrUnion(&monthDays, &ruleset, !didByFill);
298 /* no rules applied, take the dayOfMonth of the startDate */
299 NGMonthDaySet_clear(&monthDays);
300 monthDays[eventDayOfMonth] = YES;
303 // TODO: add processing of byhour/byminute/bysecond etc
305 for (dom = 1, doCont = YES; dom <= numDaysInMonth && doCont; dom++) {
306 NSCalendarDate *start;
311 if (eventDayOfMonth == dom)
314 start = [cursor dateByAddingYears:0 months:0
315 days:(dom - eventDayOfMonth)];
318 doCont = [self _addInstanceWithStartDate:start
323 if (!doCont) break; /* reached some limit */
328 - (NSCalendarDate *)lastInstanceStartDate {
329 if ([self->rrule repeatCount] > 0) {
330 NSCalendarDate *until;
331 unsigned months, interval;
333 interval = [self->rrule repeatInterval];
334 months = [self->rrule repeatCount] - 1 /* the first counts as one! */;
339 until = [[self->firstRange startDate] dateByAddingYears:0
344 return [super lastInstanceStartDate];
347 @end /* iCalMonthlyRecurrenceCalculator */