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