]> err.no Git - sope/blob - sope-ical/NGiCal/iCalMonthlyRecurrenceCalculator.m
2bfcdf5d6ce3f44b52031fe9af41c9a68db6dc5d
[sope] / sope-ical / NGiCal / iCalMonthlyRecurrenceCalculator.m
1 /*
2   Copyright (C) 2004-2005 SKYRIX Software AG
3   
4   This file is part of SOPE.
5   
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
9   later version.
10   
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.
15   
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
19   02111-1307, USA.
20 */
21
22 #include "iCalRecurrenceCalculator.h"
23
24 @interface iCalMonthlyRecurrenceCalculator : iCalRecurrenceCalculator
25 @end
26
27 #include <NGExtensions/NGCalendarDateRange.h>
28 #include "iCalRecurrenceRule.h"
29 #include "NSCalendarDate+ICal.h"
30 #include "common.h"
31
32 @interface iCalRecurrenceCalculator(PrivateAPI)
33 - (NSCalendarDate *)lastInstanceStartDate;
34 @end
35
36 @implementation iCalMonthlyRecurrenceCalculator
37
38 typedef BOOL NGMonthSet[12];
39 typedef BOOL NGMonthDaySet[32]; // 0 is unused
40
41 static void NGMonthDaySet_clear(NGMonthDaySet *daySet) {
42   register unsigned i;
43   
44   for (i = 1; i <= 31; i++)
45     (*daySet)[i] = NO;
46 }
47
48 static void NGMonthDaySet_copyOrUnion(NGMonthDaySet *base, NGMonthDaySet *new,
49                                       BOOL doCopy)
50 {
51   register unsigned i;
52   
53   if (doCopy)
54     memcpy(base, new, sizeof(NGMonthDaySet));
55   else {
56     for (i = 1; i <= 31; i++) {
57       if (!(*new)[i])
58         (*base)[i] = NO;
59     }
60   }
61 }
62
63 static BOOL NGMonthDaySet_fillWithByMonthDay(NGMonthDaySet *daySet, 
64                                              NSArray *byMonthDay)
65 {
66   /* list of days in the month */
67   unsigned i, count;
68   BOOL ok;
69   
70   NGMonthDaySet_clear(daySet);
71
72   for (i = 0, count = [byMonthDay count], ok = YES; i < count; i++) {
73     int dayInMonth; /* -31..-1 and 1..31 */
74         
75     if ((dayInMonth = [[byMonthDay objectAtIndex:i] intValue]) == 0) {
76       ok = NO;
77       continue; /* invalid value */
78     }
79     if (dayInMonth > 31) {
80       ok = NO;
81       continue; /* error, value to large */
82     }
83     if (dayInMonth < -31) {
84       ok = NO;
85       continue; /* error, value to large */
86     }
87     
88     /* adjust negative days */
89         
90     if (dayInMonth < 0) {
91       /* eg: -1 == last day in month, 30 days => 30 */
92       dayInMonth = 32 - dayInMonth /* because we count from 1 */;
93     }
94     
95     (*daySet)[dayInMonth] = YES;
96   }
97   return ok;
98 }
99
100 static inline unsigned iCalDoWForNSDoW(int dow) {
101   switch (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;
110   default: return 0;
111   }
112 }
113
114 static void NGMonthDaySet_fillWithByDayX(NGMonthDaySet *daySet, 
115                                          unsigned dayMask,
116                                          unsigned firstDoWInMonth,
117                                          int occurrence1)
118 {
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 } ;
124   
125   NGMonthDaySet_clear(daySet);
126   
127   for (dayInMonth = 1, dow = firstDoWInMonth; dayInMonth <= 31; dayInMonth++) {
128     // TODO: complete me
129     
130     if (dayMask & iCalDoWForNSDoW(dow)) {
131       if (occurrence1 == 0)
132         (*daySet)[dayInMonth] = YES;
133       else {
134         occurrences[dow] = occurrences[dow] + 1;
135         
136         if (occurrences[dow] == occurrence1) 
137           (*daySet)[dayInMonth] = YES;
138       }
139     }
140     
141     dow = (dow == 6 /* Sat */) ? 0 /* Sun */ : (dow + 1);
142   }
143 }
144
145 - (BOOL)_addInstanceWithStartDate:(NSCalendarDate *)_startDate
146   limitDate:(NSCalendarDate *)_until
147   limitRange:(NGCalendarDateRange *)_r
148   toArray:(NSMutableArray *)_ranges
149 {
150   NGCalendarDateRange *r;
151   NSCalendarDate *end;
152   
153   /* check whether we are still in the limits */
154
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.
158   if (_until != nil) {
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 */
163   }
164
165   /* create end date */
166
167   end = [_startDate addTimeInterval:[self->firstRange duration]];
168   [end setTimeZone:[_startDate timeZone]];
169     
170   /* create range and check whether its in the requested range */
171   
172   r = [[NGCalendarDateRange alloc] initWithStartDate:_startDate endDate:end];
173   if ([_r containsDateRange:r])
174     [_ranges addObject:r];
175   [r release]; r = nil;
176   
177   return YES;
178 }
179
180 - (NSArray *)recurrenceRangesWithinCalendarDateRange:(NGCalendarDateRange *)_r{
181   /* main entry */
182   // TODO: check whether this is OK for multiday-events!
183   NSMutableArray *ranges;
184   NSTimeZone     *timeZone;
185   NSCalendarDate *eventStartDate, *rStart, *rEnd, *until;
186   int            eventDayOfMonth;
187   unsigned       monthIdxInRange, numberOfMonthsInRange, interval;
188   int            diff;
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
192   };
193   NSArray *byMonthDay = nil; // array of ints (-31..-1 and 1..31)
194   NGMonthDaySet byMonthDaySet;
195   
196   eventStartDate  = [self->firstRange startDate];
197   eventDayOfMonth = [eventStartDate dayOfMonth];
198   timeZone   = [eventStartDate timeZone];
199   rStart     = [_r startDate];
200   rEnd       = [_r endDate];
201   interval   = [self->rrule repeatInterval];
202   until      = [self lastInstanceStartDate]; // TODO: maybe replace
203   byMonthDay = [self->rrule byMonthDay];
204   
205
206   /* check whether the range to be processed is beyond the 'until' date */
207   
208   if (until != nil) {
209     if ([until compare:rStart] == NSOrderedAscending) /* until before start */
210       return nil;
211     if ([until compare:rEnd] == NSOrderedDescending) /* end before until */
212       rEnd = until; // TODO: why is that? end is _before_ until?
213   }
214
215   
216   /* precalculate month days (same for all instances) */
217
218   if (byMonthDay != nil)
219     NGMonthDaySet_fillWithByMonthDay(&byMonthDaySet, byMonthDay);
220   
221   
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)
227     diff = -diff;
228   
229   numberOfMonthsInRange  = [rStart monthsBetweenDate:rEnd] + 1;
230   ranges = [NSMutableArray arrayWithCapacity:numberOfMonthsInRange];
231   
232   /* 
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
235            byday recurrences.
236   */
237   
238   for (monthIdxInRange = 0; monthIdxInRange < numberOfMonthsInRange; 
239        monthIdxInRange++) {
240     NSCalendarDate *cursor;
241     unsigned       numDaysInMonth;
242     int            monthIdxInRecurrence, dom;
243     NGMonthDaySet  monthDays;
244     BOOL           didByFill, doCont;
245     
246     monthIdxInRecurrence = diff + monthIdxInRange;
247     
248     if (monthIdxInRecurrence < 0)
249       continue;
250     
251     /* first check whether we are in the interval */
252     
253     if ((monthIdxInRecurrence % interval) != 0)
254       continue;
255
256     /*
257       Then the sequence is:
258       - check whether the month is in the BYMONTH list
259     */
260     
261     cursor = [eventStartDate dateByAddingYears:0
262                              months:(diff + monthIdxInRange)
263                              days:0];
264     [cursor setTimeZone:timeZone];
265     numDaysInMonth = [cursor numberOfDaysInMonth];
266     
267
268     /* check whether we match the bymonth specification */
269     
270     if (!byMonthList[[cursor monthOfYear] - 1])
271       continue;
272     
273     
274     /* check 'day level' byXYZ rules */
275     
276     didByFill = NO;
277     
278     if (byMonthDay != nil) { /* list of days in the month */
279       NGMonthDaySet_copyOrUnion(&monthDays, &byMonthDaySet, !didByFill);
280       didByFill = YES;
281     }
282     
283     if ([self->rrule byDayMask] != 0) { // TODO: replace the mask with an array
284       NGMonthDaySet ruleset;
285       unsigned firstDoWInMonth;
286       
287       firstDoWInMonth = [[cursor firstDayOfMonth] dayOfWeek];
288       
289       NGMonthDaySet_fillWithByDayX(&ruleset, 
290                                    [self->rrule byDayMask],
291                                    firstDoWInMonth,
292                                    [self->rrule byDayOccurence1]);
293       NGMonthDaySet_copyOrUnion(&monthDays, &ruleset, !didByFill);
294       didByFill = YES;
295     }
296     
297     if (!didByFill) {
298       /* no rules applied, take the dayOfMonth of the startDate */
299       NGMonthDaySet_clear(&monthDays);
300       monthDays[eventDayOfMonth] = YES;
301     }
302     
303     // TODO: add processing of byhour/byminute/bysecond etc
304     
305     for (dom = 1, doCont = YES; dom <= numDaysInMonth && doCont; dom++) {
306       NSCalendarDate *start;
307       
308       if (!monthDays[dom])
309         continue;
310       
311       if (eventDayOfMonth == dom)
312         start = cursor;
313       else {
314         start = [cursor dateByAddingYears:0 months:0
315                         days:(dom - eventDayOfMonth)];
316       }
317       
318       doCont = [self _addInstanceWithStartDate:start
319                      limitDate:until
320                      limitRange:_r
321                      toArray:ranges];
322     }
323     if (!doCont) break; /* reached some limit */
324   }
325   return ranges;
326 }
327
328 - (NSCalendarDate *)lastInstanceStartDate {
329   if ([self->rrule repeatCount] > 0) {
330     NSCalendarDate *until;
331     unsigned       months, interval;
332     
333     interval = [self->rrule repeatInterval];
334     months   = [self->rrule repeatCount] - 1 /* the first counts as one! */;
335     
336     if (interval > 0)
337       months *= interval;
338     
339     until = [[self->firstRange startDate] dateByAddingYears:0
340                                           months:months
341                                           days:0];
342     return until;
343   }
344   return [super lastInstanceStartDate];
345 }
346
347 @end /* iCalMonthlyRecurrenceCalculator */