]> err.no Git - scalable-opengroupware.org/blob - SOPE/NGCards/iCalMonthlyRecurrenceCalculator.m
git-svn-id: http://svn.opengroupware.org/SOGo/inverse/trunk@1178 d1b88da0-ebda-0310...
[scalable-opengroupware.org] / SOPE / NGCards / iCalMonthlyRecurrenceCalculator.m
1 /*
2   Copyright (C) 2004-2007 SKYRIX Software AG
3   Copyright (C) 2007      Helge Hess
4   
5   This file is part of SOPE.
6   
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
10   later version.
11   
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.
16   
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
20   02111-1307, USA.
21 */
22
23 #import <NGExtensions/NSCalendarDate+misc.h>
24
25 #import "iCalRecurrenceCalculator.h"
26
27 @interface iCalMonthlyRecurrenceCalculator : iCalRecurrenceCalculator
28 @end
29
30 #import <NGExtensions/NGCalendarDateRange.h>
31 #import "iCalRecurrenceRule.h"
32 #import "NSCalendarDate+ICal.h"
33 #import <string.h>
34
35 @interface iCalRecurrenceCalculator(PrivateAPI)
36 - (NSCalendarDate *)lastInstanceStartDate;
37 @end
38
39 // #define HEAVY_DEBUG 1
40
41 @implementation iCalMonthlyRecurrenceCalculator
42
43 typedef BOOL NGMonthSet[12];
44 typedef BOOL NGMonthDaySet[32]; // 0 is unused
45
46 static void NGMonthDaySet_clear(NGMonthDaySet *daySet) {
47   register unsigned i;
48   
49   for (i = 1; i <= 31; i++)
50     (*daySet)[i] = NO;
51 }
52
53 static void NGMonthDaySet_copyOrUnion(NGMonthDaySet *base, NGMonthDaySet *new,
54                                       BOOL doCopy)
55 {
56   register unsigned i;
57   
58   if (doCopy)
59     memcpy(base, new, sizeof(NGMonthDaySet));
60   else {
61     for (i = 1; i <= 31; i++) {
62       if (!(*new)[i])
63         (*base)[i] = NO;
64     }
65   }
66 }
67
68 static BOOL NGMonthDaySet_fillWithByMonthDay(NGMonthDaySet *daySet, 
69                                              NSArray *byMonthDay)
70 {
71   /* list of days in the month */
72   unsigned i, count;
73   BOOL ok;
74   
75   NGMonthDaySet_clear(daySet);
76
77   for (i = 0, count = [byMonthDay count], ok = YES; i < count; i++) {
78     int dayInMonth; /* -31..-1 and 1..31 */
79         
80     if ((dayInMonth = [[byMonthDay objectAtIndex:i] intValue]) == 0) {
81       ok = NO;
82       continue; /* invalid value */
83     }
84     if (dayInMonth > 31) {
85       ok = NO;
86       continue; /* error, value to large */
87     }
88     if (dayInMonth < -31) {
89       ok = NO;
90       continue; /* error, value to large */
91     }
92     
93     /* adjust negative days */
94         
95     if (dayInMonth < 0) {
96       /* eg: -1 == last day in month, 30 days => 30 */
97       dayInMonth = 32 - dayInMonth /* because we count from 1 */;
98     }
99     
100     (*daySet)[dayInMonth] = YES;
101   }
102   return ok;
103 }
104
105 static inline unsigned iCalDoWForNSDoW(int dow) {
106   switch (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;
115   default: return 0;
116   }
117 }
118
119 #if HEAVY_DEBUG
120 static NSString *dowEN[8] = { 
121   @"SU", @"MO", @"TU", @"WE", @"TH", @"FR", @"SA", @"SU-"
122 };
123 #endif
124
125 static void NGMonthDaySet_fillWithByDayX(NGMonthDaySet *daySet, 
126                                          unsigned dayMask,
127                                          unsigned firstDoWInMonth,
128                                          unsigned numberOfDaysInMonth,
129                                          int occurrence1)
130 {
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 } ;
136   
137   NGMonthDaySet_clear(daySet);
138   
139   if (occurrence1 >= 0) {
140     for (dayInMonth = 1, dow = firstDoWInMonth; dayInMonth<=31; dayInMonth++) {
141       // TODO: complete me
142       
143       if (dayMask & iCalDoWForNSDoW(dow)) {
144         if (occurrence1 == 0)
145           (*daySet)[dayInMonth] = YES;
146         else { /* occurrence1 > 0 */
147           occurrences[dow] = occurrences[dow] + 1;
148           
149           if (occurrences[dow] == occurrence1) 
150             (*daySet)[dayInMonth] = YES;
151         }
152       }
153       
154       dow = (dow == 6 /* Sat */) ? 0 /* Sun */ : (dow + 1);
155     }
156   }
157   else {
158     int lastDoWInMonthSet;
159     
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;
165     
166 #if HEAVY_DEBUG
167     NSLog(@"LAST DOW IN SET: %i / %@", 
168           lastDoWInMonthSet, dowEN[lastDoWInMonthSet]);
169 #endif
170     /* start at the end of the set */
171     for (dayInMonth = numberOfDaysInMonth, dow = lastDoWInMonthSet; 
172          dayInMonth >= 1; dayInMonth--) {
173       // TODO: complete me
174       
175 #if HEAVY_DEBUG
176       NSLog(@"  CHECK day-of-month %02i, "
177             @" dow=%i/%@ (first=%i/%@, last=%i/%@)",
178             dayInMonth, 
179             dow, dowEN[dow],
180             firstDoWInMonth, dowEN[firstDoWInMonth],
181             lastDoWInMonthSet, dowEN[lastDoWInMonthSet]
182             );
183 #endif
184       
185       if (dayMask & iCalDoWForNSDoW(dow)) {
186         occurrences[dow] = occurrences[dow] + 1;
187 #if HEAVY_DEBUG
188         NSLog(@"    MATCH %i/%@ count: %i occurences=%i",
189               dow, dowEN[dow], occurrences[dow], occurrence1);
190 #endif
191           
192         if (occurrences[dow] == -occurrence1) {
193 #if HEAVY_DEBUG
194           NSLog(@"    COUNT MATCH");
195 #endif
196           (*daySet)[dayInMonth] = YES;
197         }
198       }
199       
200       dow = (dow == 0 /* Sun */) ? 6 /* Sat */ : (dow - 1);
201     }
202   }
203 }
204
205 - (BOOL)_addInstanceWithStartDate:(NSCalendarDate *)_startDate
206   limitDate:(NSCalendarDate *)_until
207   limitRange:(NGCalendarDateRange *)_r
208   toArray:(NSMutableArray *)_ranges
209 {
210   NGCalendarDateRange *r;
211   NSCalendarDate *end;
212   
213   /* check whether we are still in the limits */
214
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.
218   if (_until != nil) {
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 */
223   }
224
225   /* create end date */
226
227   end = [_startDate addTimeInterval:[self->firstRange duration]];
228   [end setTimeZone:[_startDate timeZone]];
229     
230   /* create range and check whether its in the requested range */
231   
232   r = [[NGCalendarDateRange alloc] initWithStartDate:_startDate endDate:end];
233   if ([_r containsDateRange:r])
234     [_ranges addObject:r];
235   [r release]; r = nil;
236   
237   return YES;
238 }
239
240 - (NSArray *)recurrenceRangesWithinCalendarDateRange:(NGCalendarDateRange *)_r{
241   /* main entry */
242   // TODO: check whether this is OK for multiday-events!
243   NSMutableArray *ranges;
244   NSTimeZone     *timeZone;
245   NSCalendarDate *eventStartDate, *rStart, *rEnd, *until;
246   int            eventDayOfMonth;
247   unsigned       monthIdxInRange, numberOfMonthsInRange, interval;
248   int            diff;
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
253   };
254   NSArray       *byMonthDay; // array of ints (-31..-1 and 1..31)
255   NGMonthDaySet byMonthDaySet;
256   
257   eventStartDate  = [self->firstRange startDate];
258   eventDayOfMonth = [eventStartDate dayOfMonth];
259   timeZone        = [eventStartDate timeZone];
260   rStart          = [_r startDate];
261   rEnd            = [_r endDate];
262   interval        = [self->rrule repeatInterval];
263   until           = [self lastInstanceStartDate]; // TODO: maybe replace
264   byMonthDay      = [self->rrule byMonthDay];
265   
266
267   /* check whether the range to be processed is beyond the 'until' date */
268   
269   if (until != nil) {
270     if ([until compare:rStart] == NSOrderedAscending) /* until before start */
271       return nil;
272     if ([until compare:rEnd] == NSOrderedDescending) /* end before until */
273       rEnd = until; // TODO: why is that? end is _before_ until?
274   }
275
276   
277   /* precalculate month days (same for all instances) */
278
279   if (byMonthDay != nil) {
280 #if HEAVY_DEBUG
281     NSLog(@"byMonthDay: %@", byMonthDay);
282 #endif
283     NGMonthDaySet_fillWithByMonthDay(&byMonthDaySet, byMonthDay);
284   }
285   
286   
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)
292     diff = -diff;
293   
294   numberOfMonthsInRange  = [rStart monthsBetweenDate:rEnd] + 1;
295   ranges = [NSMutableArray arrayWithCapacity:numberOfMonthsInRange];
296   
297   /* 
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
300            byday recurrences.
301   */
302   
303   for (monthIdxInRange = 0; monthIdxInRange < numberOfMonthsInRange; 
304        monthIdxInRange++) {
305     NSCalendarDate *cursor;
306     unsigned       numDaysInMonth;
307     int            monthIdxInRecurrence, dom;
308     NGMonthDaySet  monthDays;
309     BOOL           didByFill, doCont;
310     
311     monthIdxInRecurrence = diff + monthIdxInRange;
312     
313     if (monthIdxInRecurrence < 0)
314       continue;
315     
316     /* first check whether we are in the interval */
317     
318     if ((monthIdxInRecurrence % interval) != 0)
319       continue;
320
321     /*
322       Then the sequence is:
323       - check whether the month is in the BYMONTH list
324     */
325     
326     /*
327       Note: the function below adds exactly a month, eg:
328             2007-01-30 + 1month => 2007-02-*28*!!
329     */
330     cursor = [eventStartDate dateByAddingYears:0
331                              months:(diff + monthIdxInRange)
332                              days:0];
333     [cursor setTimeZone:timeZone];
334     numDaysInMonth = [cursor numberOfDaysInMonth];
335     
336
337     /* check whether we match the bymonth specification */
338     
339     if (!byMonthList[[cursor monthOfYear] - 1])
340       continue;
341     
342     
343     /* check 'day level' byXYZ rules */
344     
345     didByFill = NO;
346     
347     if (byMonthDay != nil) { /* list of days in the month */
348       NGMonthDaySet_copyOrUnion(&monthDays, &byMonthDaySet, !didByFill);
349       didByFill = YES;
350     }
351     
352     if ([self->rrule byDayMask] != 0) { // TODO: replace the mask with an array
353       NGMonthDaySet ruleset;
354       unsigned firstDoWInMonth;
355       
356       firstDoWInMonth = [[cursor firstDayOfMonth] dayOfWeek];
357       
358       NGMonthDaySet_fillWithByDayX(&ruleset, 
359                                    [self->rrule byDayMask],
360                                    firstDoWInMonth,
361                                    [cursor numberOfDaysInMonth],
362                                    [self->rrule byDayOccurence1]);
363       NGMonthDaySet_copyOrUnion(&monthDays, &ruleset, !didByFill);
364       didByFill = YES;
365     }
366     
367     if (!didByFill) {
368       /* no rules applied, take the dayOfMonth of the startDate */
369       NGMonthDaySet_clear(&monthDays);
370       monthDays[eventDayOfMonth] = YES;
371     }
372     
373     // TODO: add processing of byhour/byminute/bysecond etc
374     
375     /* 
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
378        set, we continue.
379        If its set, we add the date to the instance.
380        
381        The 'cursor' is the *startdate* of the event (not necessarily a
382        component of the sequence!) plus the currently processed month.
383        Eg:
384          startdate: 2007-01-30
385          cursor[1]: 2007-01-30
386          cursor[2]: 2007-02-28 <== Note: we have February!
387     */
388     
389     for (dom = 1, doCont = YES; dom <= numDaysInMonth && doCont; dom++) {
390       NSCalendarDate *start;
391       
392       if (!monthDays[dom])
393         continue;
394
395       // TODO: what is this good for?
396       /*
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!
399
400         The 'numDaysInMonth' localvar contains the number of days in the
401         current month (eg 31 for Januar, 28 for most February's, etc)
402         
403         Eg: MONTHLY;BYDAY=-1WE (last wednesday, every month)
404         
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)
409         
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!
413               Eg:
414                 start was:      2007-01-30
415                 cursor will be: 2007-02-28
416       */
417       if (eventDayOfMonth == dom) {
418         start = cursor;
419       }
420       else {
421         int maxDay = 
422           eventDayOfMonth > numDaysInMonth ? numDaysInMonth : eventDayOfMonth;
423         
424         start = [cursor dateByAddingYears:0 months:0 days:(dom - maxDay)];
425       }
426
427       /*
428         Setup for 2007-02-28, MONTHLY;BYDAY=-1WE.
429           dom:             28
430           eventDayOfMonth: 31
431           cursor:          2007-02-28
432           start:           2007-02-25 <== WRONG
433       */
434
435 #if HEAVY_DEBUG
436       NSLog(@"DOM %i EDOM %i NUMDAYS %i START: %@ CURSOR: %@", 
437             dom, eventDayOfMonth, numDaysInMonth,
438             start, cursor);
439 #endif
440       doCont = [self _addInstanceWithStartDate:start
441                      limitDate:until
442                      limitRange:_r
443                      toArray:ranges];
444     }
445     if (!doCont) break; /* reached some limit */
446   }
447   return ranges;
448 }
449
450 - (NSCalendarDate *)lastInstanceStartDate {
451   if ([self->rrule repeatCount] > 0) {
452     NSCalendarDate *until;
453     unsigned       months, interval;
454     
455     interval = [self->rrule repeatInterval];
456     months   = [self->rrule repeatCount] - 1 /* the first counts as one! */;
457     
458     if (interval > 0)
459       months *= interval;
460     
461     until = [[self->firstRange startDate] dateByAddingYears:0
462                                           months:months
463                                           days:0];
464     return until;
465   }
466   return [super lastInstanceStartDate];
467 }
468
469 @end /* iCalMonthlyRecurrenceCalculator */