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