]> err.no Git - scalable-opengroupware.org/blob - SOPE/NGCards/iCalMonthlyRecurrenceCalculator.m
git-svn-id: http://svn.opengroupware.org/SOGo/inverse/trunk@1112 d1b88da0-ebda-0310...
[scalable-opengroupware.org] / SOPE / NGCards / 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 #import <NGExtensions/NSCalendarDate+misc.h>
23
24 #import "iCalRecurrenceCalculator.h"
25
26 @interface iCalMonthlyRecurrenceCalculator : iCalRecurrenceCalculator
27 @end
28
29 #import <NGExtensions/NGCalendarDateRange.h>
30 #import "iCalRecurrenceRule.h"
31 #import "NSCalendarDate+ICal.h"
32 #import <string.h>
33
34 @interface iCalRecurrenceCalculator(PrivateAPI)
35 - (NSCalendarDate *)lastInstanceStartDate;
36 @end
37
38 // #define HEAVY_DEBUG 1
39
40 @implementation iCalMonthlyRecurrenceCalculator
41
42 typedef BOOL NGMonthSet[12];
43 typedef BOOL NGMonthDaySet[32]; // 0 is unused
44
45 static void NGMonthDaySet_clear(NGMonthDaySet *daySet) {
46   register unsigned i;
47   
48   for (i = 1; i <= 31; i++)
49     (*daySet)[i] = NO;
50 }
51
52 static void NGMonthDaySet_copyOrUnion(NGMonthDaySet *base, NGMonthDaySet *new,
53                                       BOOL doCopy)
54 {
55   register unsigned i;
56   
57   if (doCopy)
58     memcpy(base, new, sizeof(NGMonthDaySet));
59   else {
60     for (i = 1; i <= 31; i++) {
61       if (!(*new)[i])
62         (*base)[i] = NO;
63     }
64   }
65 }
66
67 static BOOL NGMonthDaySet_fillWithByMonthDay(NGMonthDaySet *daySet, 
68                                              NSArray *byMonthDay)
69 {
70   /* list of days in the month */
71   unsigned i, count;
72   BOOL ok;
73   
74   NGMonthDaySet_clear(daySet);
75
76   for (i = 0, count = [byMonthDay count], ok = YES; i < count; i++) {
77     int dayInMonth; /* -31..-1 and 1..31 */
78         
79     if ((dayInMonth = [[byMonthDay objectAtIndex:i] intValue]) == 0) {
80       ok = NO;
81       continue; /* invalid value */
82     }
83     if (dayInMonth > 31) {
84       ok = NO;
85       continue; /* error, value to large */
86     }
87     if (dayInMonth < -31) {
88       ok = NO;
89       continue; /* error, value to large */
90     }
91     
92     /* adjust negative days */
93         
94     if (dayInMonth < 0) {
95       /* eg: -1 == last day in month, 30 days => 30 */
96       dayInMonth = 32 - dayInMonth /* because we count from 1 */;
97     }
98     
99     (*daySet)[dayInMonth] = YES;
100   }
101   return ok;
102 }
103
104 static inline unsigned iCalDoWForNSDoW(int dow) {
105   switch (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;
114   default: return 0;
115   }
116 }
117
118 #if HEAVY_DEBUG
119 static NSString *dowEN[8] = { 
120   @"SU", @"MO", @"TU", @"WE", @"TH", @"FR", @"SA", @"SU-"
121 };
122 #endif
123
124 static void NGMonthDaySet_fillWithByDayX(NGMonthDaySet *daySet, 
125                                          unsigned dayMask,
126                                          unsigned firstDoWInMonth,
127                                          unsigned numberOfDaysInMonth,
128                                          int occurrence1)
129 {
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 } ;
135   
136   NGMonthDaySet_clear(daySet);
137   
138   if (occurrence1 >= 0) {
139     for (dayInMonth = 1, dow = firstDoWInMonth; dayInMonth<=31; dayInMonth++) {
140       // TODO: complete me
141       
142       if (dayMask & iCalDoWForNSDoW(dow)) {
143         if (occurrence1 == 0)
144           (*daySet)[dayInMonth] = YES;
145         else { /* occurrence1 > 0 */
146           occurrences[dow] = occurrences[dow] + 1;
147           
148           if (occurrences[dow] == occurrence1) 
149             (*daySet)[dayInMonth] = YES;
150         }
151       }
152       
153       dow = (dow == 6 /* Sat */) ? 0 /* Sun */ : (dow + 1);
154     }
155   }
156   else {
157     int lastDoWInMonthSet;
158     
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;
164     
165 #if HEAVY_DEBUG
166     NSLog(@"LAST DOW IN SET: %i / %@", 
167           lastDoWInMonthSet, dowEN[lastDoWInMonthSet]);
168 #endif
169     /* start at the end of the set */
170     for (dayInMonth = numberOfDaysInMonth, dow = lastDoWInMonthSet; 
171          dayInMonth >= 1; dayInMonth--) {
172       // TODO: complete me
173       
174 #if HEAVY_DEBUG
175       NSLog(@"  CHECK day-of-month %02i, "
176             @" dow=%i/%@ (first=%i/%@, last=%i/%@)",
177             dayInMonth, 
178             dow, dowEN[dow],
179             firstDoWInMonth, dowEN[firstDoWInMonth],
180             lastDoWInMonthSet, dowEN[lastDoWInMonthSet]
181             );
182 #endif
183       
184       if (dayMask & iCalDoWForNSDoW(dow)) {
185         occurrences[dow] = occurrences[dow] + 1;
186 #if HEAVY_DEBUG
187         NSLog(@"    MATCH %i/%@ count: %i occurences=%i",
188               dow, dowEN[dow], occurrences[dow], occurrence1);
189 #endif
190           
191         if (occurrences[dow] == -occurrence1) {
192 #if HEAVY_DEBUG
193           NSLog(@"    COUNT MATCH");
194 #endif
195           (*daySet)[dayInMonth] = YES;
196         }
197       }
198       
199       dow = (dow == 0 /* Sun */) ? 6 /* Sat */ : (dow - 1);
200     }
201   }
202 }
203
204 - (BOOL)_addInstanceWithStartDate:(NSCalendarDate *)_startDate
205   limitDate:(NSCalendarDate *)_until
206   limitRange:(NGCalendarDateRange *)_r
207   toArray:(NSMutableArray *)_ranges
208 {
209   NGCalendarDateRange *r;
210   NSCalendarDate *end;
211   
212   /* check whether we are still in the limits */
213
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.
217   if (_until != nil) {
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 */
222   }
223
224   /* create end date */
225
226   end = [_startDate addTimeInterval:[self->firstRange duration]];
227   [end setTimeZone:[_startDate timeZone]];
228     
229   /* create range and check whether its in the requested range */
230   
231   r = [[NGCalendarDateRange alloc] initWithStartDate:_startDate endDate:end];
232   if ([_r containsDateRange:r])
233     [_ranges addObject:r];
234   [r release]; r = nil;
235   
236   return YES;
237 }
238
239 - (NSArray *)recurrenceRangesWithinCalendarDateRange:(NGCalendarDateRange *)_r{
240   /* main entry */
241   // TODO: check whether this is OK for multiday-events!
242   NSMutableArray *ranges;
243   NSTimeZone     *timeZone;
244   NSCalendarDate *eventStartDate, *rStart, *rEnd, *until;
245   int            eventDayOfMonth;
246   unsigned       monthIdxInRange, numberOfMonthsInRange, interval;
247   int            diff;
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
251   };
252   NSArray *byMonthDay = nil; // array of ints (-31..-1 and 1..31)
253   NGMonthDaySet byMonthDaySet;
254   
255   eventStartDate  = [self->firstRange startDate];
256   eventDayOfMonth = [eventStartDate dayOfMonth];
257   timeZone   = [eventStartDate timeZone];
258   rStart     = [_r startDate];
259   rEnd       = [_r endDate];
260   interval   = [self->rrule repeatInterval];
261   until      = [self lastInstanceStartDate]; // TODO: maybe replace
262   byMonthDay = [self->rrule byMonthDay];
263   
264
265   /* check whether the range to be processed is beyond the 'until' date */
266   
267   if (until != nil) {
268     if ([until compare:rStart] == NSOrderedAscending) /* until before start */
269       return nil;
270     if ([until compare:rEnd] == NSOrderedDescending) /* end before until */
271       rEnd = until; // TODO: why is that? end is _before_ until?
272   }
273
274   
275   /* precalculate month days (same for all instances) */
276
277   if (byMonthDay != nil)
278     NGMonthDaySet_fillWithByMonthDay(&byMonthDaySet, byMonthDay);
279   
280   
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)
286     diff = -diff;
287   
288   numberOfMonthsInRange  = [rStart monthsBetweenDate:rEnd] + 1;
289   ranges = [NSMutableArray arrayWithCapacity:numberOfMonthsInRange];
290   
291   /* 
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
294            byday recurrences.
295   */
296   
297   for (monthIdxInRange = 0; monthIdxInRange < numberOfMonthsInRange; 
298        monthIdxInRange++) {
299     NSCalendarDate *cursor;
300     unsigned       numDaysInMonth;
301     int            monthIdxInRecurrence, dom;
302     NGMonthDaySet  monthDays;
303     BOOL           didByFill, doCont;
304     
305     monthIdxInRecurrence = diff + monthIdxInRange;
306     
307     if (monthIdxInRecurrence < 0)
308       continue;
309     
310     /* first check whether we are in the interval */
311     
312     if ((monthIdxInRecurrence % interval) != 0)
313       continue;
314
315     /*
316       Then the sequence is:
317       - check whether the month is in the BYMONTH list
318     */
319     
320     cursor = [eventStartDate dateByAddingYears:0
321                              months:(diff + monthIdxInRange)
322                              days:0];
323     [cursor setTimeZone:timeZone];
324     numDaysInMonth = [cursor numberOfDaysInMonth];
325     
326
327     /* check whether we match the bymonth specification */
328     
329     if (!byMonthList[[cursor monthOfYear] - 1])
330       continue;
331     
332     
333     /* check 'day level' byXYZ rules */
334     
335     didByFill = NO;
336     
337     if (byMonthDay != nil) { /* list of days in the month */
338       NGMonthDaySet_copyOrUnion(&monthDays, &byMonthDaySet, !didByFill);
339       didByFill = YES;
340     }
341     
342     if ([self->rrule byDayMask] != 0) { // TODO: replace the mask with an array
343       NGMonthDaySet ruleset;
344       unsigned firstDoWInMonth;
345       
346       firstDoWInMonth = [[cursor firstDayOfMonth] dayOfWeek];
347       
348       NGMonthDaySet_fillWithByDayX(&ruleset, 
349                                    [self->rrule byDayMask],
350                                    firstDoWInMonth,
351                                    [cursor numberOfDaysInMonth],
352                                    [self->rrule byDayOccurence1]);
353       NGMonthDaySet_copyOrUnion(&monthDays, &ruleset, !didByFill);
354       didByFill = YES;
355     }
356     
357     if (!didByFill) {
358       /* no rules applied, take the dayOfMonth of the startDate */
359       NGMonthDaySet_clear(&monthDays);
360       monthDays[eventDayOfMonth] = YES;
361     }
362     
363     // TODO: add processing of byhour/byminute/bysecond etc
364     
365     for (dom = 1, doCont = YES; dom <= numDaysInMonth && doCont; dom++) {
366       NSCalendarDate *start;
367       
368       if (!monthDays[dom])
369         continue;
370       
371       if (eventDayOfMonth == dom)
372         start = cursor;
373       else {
374         start = [cursor dateByAddingYears:0 months:0
375                         days:(dom - eventDayOfMonth)];
376       }
377       
378       doCont = [self _addInstanceWithStartDate:start
379                      limitDate:until
380                      limitRange:_r
381                      toArray:ranges];
382     }
383     if (!doCont) break; /* reached some limit */
384   }
385   return ranges;
386 }
387
388 - (NSCalendarDate *)lastInstanceStartDate {
389   if ([self->rrule repeatCount] > 0) {
390     NSCalendarDate *until;
391     unsigned       months, interval;
392     
393     interval = [self->rrule repeatInterval];
394     months   = [self->rrule repeatCount] - 1 /* the first counts as one! */;
395     
396     if (interval > 0)
397       months *= interval;
398     
399     until = [[self->firstRange startDate] dateByAddingYears:0
400                                           months:months
401                                           days:0];
402     return until;
403   }
404   return [super lastInstanceStartDate];
405 }
406
407 @end /* iCalMonthlyRecurrenceCalculator */