]> err.no Git - sope/blob - sope-ical/NGiCal/iCalMonthlyRecurrenceCalculator.m
fixed OGo bug #1829
[sope] / sope-ical / NGiCal / 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 #include "iCalRecurrenceCalculator.h"
24
25 @interface iCalMonthlyRecurrenceCalculator : iCalRecurrenceCalculator
26 @end
27
28 #include <NGExtensions/NGCalendarDateRange.h>
29 #include "iCalRecurrenceRule.h"
30 #include "NSCalendarDate+ICal.h"
31 #include "common.h"
32 #include <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     /* enable all months of the year */
250     YES, YES, YES, YES, YES, YES, 
251     YES, YES, YES, YES, YES, YES
252   };
253   NSArray       *byMonthDay; // array of ints (-31..-1 and 1..31)
254   NGMonthDaySet byMonthDaySet;
255   
256   eventStartDate  = [self->firstRange startDate];
257   eventDayOfMonth = [eventStartDate dayOfMonth];
258   timeZone        = [eventStartDate timeZone];
259   rStart          = [_r startDate];
260   rEnd            = [_r endDate];
261   interval        = [self->rrule repeatInterval];
262   until           = [self lastInstanceStartDate]; // TODO: maybe replace
263   byMonthDay      = [self->rrule byMonthDay];
264   
265
266   /* check whether the range to be processed is beyond the 'until' date */
267   
268   if (until != nil) {
269     if ([until compare:rStart] == NSOrderedAscending) /* until before start */
270       return nil;
271     if ([until compare:rEnd] == NSOrderedDescending) /* end before until */
272       rEnd = until; // TODO: why is that? end is _before_ until?
273   }
274
275   
276   /* precalculate month days (same for all instances) */
277
278   if (byMonthDay != nil) {
279 #if HEAVY_DEBUG
280     NSLog(@"byMonthDay: %@", byMonthDay);
281 #endif
282     NGMonthDaySet_fillWithByMonthDay(&byMonthDaySet, byMonthDay);
283   }
284   
285   
286   // TODO: I think the 'diff' is to skip recurrence which are before the
287   //       requested range. Not sure whether this is actually possible, eg
288   //       the repeatCount must be processed from the start.
289   diff = [eventStartDate monthsBetweenDate:rStart];
290   if ((diff != 0) && [rStart compare:eventStartDate] == NSOrderedAscending)
291     diff = -diff;
292   
293   numberOfMonthsInRange  = [rStart monthsBetweenDate:rEnd] + 1;
294   ranges = [NSMutableArray arrayWithCapacity:numberOfMonthsInRange];
295   
296   /* 
297      Note: we do not add 'eventStartDate', this is intentional, the event date
298            itself is _not_ necessarily part of the sequence, eg with monthly
299            byday recurrences.
300   */
301   
302   for (monthIdxInRange = 0; monthIdxInRange < numberOfMonthsInRange; 
303        monthIdxInRange++) {
304     NSCalendarDate *cursor;
305     unsigned       numDaysInMonth;
306     int            monthIdxInRecurrence, dom;
307     NGMonthDaySet  monthDays;
308     BOOL           didByFill, doCont;
309     
310     monthIdxInRecurrence = diff + monthIdxInRange;
311     
312     if (monthIdxInRecurrence < 0)
313       continue;
314     
315     /* first check whether we are in the interval */
316     
317     if ((monthIdxInRecurrence % interval) != 0)
318       continue;
319
320     /*
321       Then the sequence is:
322       - check whether the month is in the BYMONTH list
323     */
324     
325     /*
326       Note: the function below adds exactly a month, eg:
327             2007-01-30 + 1month => 2007-02-*28*!!
328     */
329     cursor = [eventStartDate dateByAddingYears:0
330                              months:(diff + monthIdxInRange)
331                              days:0];
332     [cursor setTimeZone:timeZone];
333     numDaysInMonth = [cursor numberOfDaysInMonth];
334     
335
336     /* check whether we match the bymonth specification */
337     
338     if (!byMonthList[[cursor monthOfYear] - 1])
339       continue;
340     
341     
342     /* check 'day level' byXYZ rules */
343     
344     didByFill = NO;
345     
346     if (byMonthDay != nil) { /* list of days in the month */
347       NGMonthDaySet_copyOrUnion(&monthDays, &byMonthDaySet, !didByFill);
348       didByFill = YES;
349     }
350     
351     if ([self->rrule byDayMask] != 0) { // TODO: replace the mask with an array
352       NGMonthDaySet ruleset;
353       unsigned firstDoWInMonth;
354       
355       firstDoWInMonth = [[cursor firstDayOfMonth] dayOfWeek];
356       
357       NGMonthDaySet_fillWithByDayX(&ruleset, 
358                                    [self->rrule byDayMask],
359                                    firstDoWInMonth,
360                                    [cursor numberOfDaysInMonth],
361                                    [self->rrule byDayOccurence1]);
362       NGMonthDaySet_copyOrUnion(&monthDays, &ruleset, !didByFill);
363       didByFill = YES;
364     }
365     
366     if (!didByFill) {
367       /* no rules applied, take the dayOfMonth of the startDate */
368       NGMonthDaySet_clear(&monthDays);
369       monthDays[eventDayOfMonth] = YES;
370     }
371     
372     // TODO: add processing of byhour/byminute/bysecond etc
373     
374     /* 
375        Next step is to create NSCalendarDate instances from our 'monthDays'
376        set. We walk over each day of the 'monthDays' set. If its flag isn't
377        set, we continue.
378        If its set, we add the date to the instance.
379        
380        The 'cursor' is the *startdate* of the event (not necessarily a
381        component of the sequence!) plus the currently processed month.
382        Eg:
383          startdate: 2007-01-30
384          cursor[1]: 2007-01-30
385          cursor[2]: 2007-02-28 <== Note: we have February!
386     */
387     
388     for (dom = 1, doCont = YES; dom <= numDaysInMonth && doCont; dom++) {
389       NSCalendarDate *start;
390       
391       if (!monthDays[dom])
392         continue;
393
394       // TODO: what is this good for?
395       /*
396         Here we need to correct the date. Remember that the startdate given in
397         the event is not necessarily a date of the sequence!
398
399         The 'numDaysInMonth' localvar contains the number of days in the
400         current month (eg 31 for Januar, 28 for most February's, etc)
401         
402         Eg: MONTHLY;BYDAY=-1WE (last wednesday, every month)
403         
404           cursor:  2007-01-30 (eventDayOfMonth = 30)
405           =>start: 2007-01-31 (dom = 31)
406           cursor:  2007-02-28 (eventDayOfMonth = 30)
407           =>start: 2007-02-28 (dom = 28)
408         
409         Note: in case the cursor already had an event-day overflow, that is the
410               'eventDayOfMonth' is bigger than the 'numDaysInMonth', the cursor
411               will already be corrected!
412               Eg:
413                 start was:      2007-01-30
414                 cursor will be: 2007-02-28
415       */
416       if (eventDayOfMonth == dom) {
417         start = cursor;
418       }
419       else {
420         int maxDay = 
421           eventDayOfMonth > numDaysInMonth ? numDaysInMonth : eventDayOfMonth;
422         
423         start = [cursor dateByAddingYears:0 months:0 days:(dom - maxDay)];
424       }
425
426       /*
427         Setup for 2007-02-28, MONTHLY;BYDAY=-1WE.
428           dom:             28
429           eventDayOfMonth: 31
430           cursor:          2007-02-28
431           start:           2007-02-25 <== WRONG
432       */
433
434 #if HEAVY_DEBUG
435       NSLog(@"DOM %i EDOM %i NUMDAYS %i START: %@ CURSOR: %@", 
436             dom, eventDayOfMonth, numDaysInMonth,
437             start, cursor);
438 #endif
439       doCont = [self _addInstanceWithStartDate:start
440                      limitDate:until
441                      limitRange:_r
442                      toArray:ranges];
443     }
444     if (!doCont) break; /* reached some limit */
445   }
446   return ranges;
447 }
448
449 - (NSCalendarDate *)lastInstanceStartDate {
450   if ([self->rrule repeatCount] > 0) {
451     NSCalendarDate *until;
452     unsigned       months, interval;
453     
454     interval = [self->rrule repeatInterval];
455     months   = [self->rrule repeatCount] - 1 /* the first counts as one! */;
456     
457     if (interval > 0)
458       months *= interval;
459     
460     until = [[self->firstRange startDate] dateByAddingYears:0
461                                           months:months
462                                           days:0];
463     return until;
464   }
465   return [super lastInstanceStartDate];
466 }
467
468 @end /* iCalMonthlyRecurrenceCalculator */