]> err.no Git - sope/blob - sope-ical/NGiCal/iCalRecurrenceCalculator.m
bugfixes for cycle calculation, modified implementations with future extensions in...
[sope] / sope-ical / NGiCal / iCalRecurrenceCalculator.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 #include <NGExtensions/NGCalendarDateRange.h>
24 #include "iCalRecurrenceRule.h"
25 #include "NSCalendarDate+ICal.h"
26 #include "common.h"
27
28 /* class cluster */
29
30 @interface iCalDailyRecurrenceCalculator : iCalRecurrenceCalculator
31 {
32 }
33 @end
34
35 @interface iCalWeeklyRecurrenceCalculator : iCalRecurrenceCalculator
36 {
37 }
38 @end
39
40 @interface iCalMonthlyRecurrenceCalculator : iCalRecurrenceCalculator
41 {
42 }
43 @end
44
45 @interface iCalYearlyRecurrenceCalculator : iCalRecurrenceCalculator
46 {
47 }
48 @end
49
50 /* Private */
51
52 @interface iCalRecurrenceCalculator (PrivateAPI)
53 - (NSCalendarDate *)lastInstanceStartDate;
54 @end
55
56 @implementation iCalRecurrenceCalculator
57
58 static Class dailyCalcClass   = Nil;
59 static Class weeklyCalcClass  = Nil;
60 static Class monthlyCalcClass = Nil;
61 static Class yearlyCalcClass  = Nil;
62
63 + (void)initialize {
64   static BOOL didInit = NO;
65   
66   if (didInit) return;
67   didInit = YES;
68
69   dailyCalcClass   = [iCalDailyRecurrenceCalculator   class];
70   weeklyCalcClass  = [iCalWeeklyRecurrenceCalculator  class];
71   monthlyCalcClass = [iCalMonthlyRecurrenceCalculator class];
72   yearlyCalcClass  = [iCalYearlyRecurrenceCalculator  class];
73 }
74
75 + (id)recurrenceCalculatorForRecurrenceRule:(iCalRecurrenceRule *)_rrule
76          withFirstInstanceCalendarDateRange:(NGCalendarDateRange *)_range
77 {
78   return [[[self alloc] initWithRecurrenceRule:_rrule
79                         firstInstanceCalendarDateRange:_range] autorelease];
80 }
81
82 - (id)initWithRecurrenceRule:(iCalRecurrenceRule *)_rrule
83   firstInstanceCalendarDateRange:(NGCalendarDateRange *)_range
84 {
85   iCalRecurrenceFrequency freq;
86   Class calcClass = Nil;
87
88   freq = [_rrule frequency];
89   if (freq == iCalRecurrenceFrequenceDaily)
90     calcClass = dailyCalcClass;
91   else if (freq == iCalRecurrenceFrequenceWeekly)
92     calcClass = weeklyCalcClass;
93   else if (freq == iCalRecurrenceFrequenceMonthly)
94     calcClass = monthlyCalcClass;
95   else if (freq == iCalRecurrenceFrequenceYearly)
96     calcClass = yearlyCalcClass;
97
98   [self autorelease];
99   if (calcClass == Nil)
100     return nil;
101
102   self = [[calcClass alloc] init];
103   ASSIGN(self->rrule, _rrule);
104   ASSIGN(self->firstRange, _range);
105   return self;  
106 }
107
108
109 /* calculation */
110
111 - (NSArray *)recurrenceRangesWithinCalendarDateRange:(NGCalendarDateRange *)_r {
112   return nil; /* subclass responsibility */
113 }
114 - (BOOL)doesRecurrWithinCalendarDateRange:(NGCalendarDateRange *)_range {
115   NSArray *ranges;
116
117   ranges = [self recurrenceRangesWithinCalendarDateRange:_range];
118   return (ranges == nil || [ranges count] == 0) ? NO : YES;
119 }
120
121 - (NGCalendarDateRange *)firstInstanceCalendarDateRange {
122   return self->firstRange;
123 }
124
125 - (NGCalendarDateRange *)lastInstanceCalendarDateRange {
126   NSCalendarDate *start, *end;
127
128   start = [self lastInstanceStartDate];
129   if (!start)
130     return nil;
131   end   = [start addTimeInterval:[self->firstRange duration]];
132   return [NGCalendarDateRange calendarDateRangeWithStartDate:start
133                               endDate:end];
134 }
135
136 - (NSCalendarDate *)lastInstanceStartDate {
137   NSCalendarDate *until;
138   
139   /* NOTE: this is horribly inaccurate and doesn't even consider the use
140   of repeatCount. It MUST be implemented by subclasses properly! However,
141   it does the trick for SOGO 1.0 - that's why it's left here.
142   */
143   if ((until = [self->rrule untilDate]) != nil)
144     return until;
145   return nil;
146 }
147
148 @end /* iCalRecurrenceCalculator */
149
150
151 @implementation iCalDailyRecurrenceCalculator
152
153 - (NSArray *)recurrenceRangesWithinCalendarDateRange:(NGCalendarDateRange *)_r {
154   NSMutableArray *ranges;
155   NSCalendarDate *firStart;
156   long           i, jnFirst, jnStart, jnEnd, startEndCount;
157   unsigned       interval;
158
159   firStart = [self->firstRange startDate];
160   jnFirst  = [firStart julianNumber];
161   jnEnd    = [[_r endDate] julianNumber];
162   
163   if (jnFirst > jnEnd)
164     return nil;
165   
166   jnStart  = [[_r startDate] julianNumber];
167   interval = [self->rrule repeatInterval];
168   
169   /* if rule is bound, check the bounds */
170   if (![self->rrule isInfinite]) {
171     NSCalendarDate *until;
172     long           jnRuleLast;
173     
174     until = [self->rrule untilDate];
175     if (until) {
176       if ([until compare:[_r startDate]] == NSOrderedAscending)
177         return nil;
178       jnRuleLast = [until julianNumber];
179     }
180     else {
181       jnRuleLast = (interval * [self->rrule repeatCount])
182       + jnFirst;
183       if (jnRuleLast < jnStart)
184         return nil;
185     }
186     /* jnStart < jnRuleLast < jnEnd ? */
187     if (jnEnd > jnRuleLast)
188       jnEnd = jnRuleLast;
189   }
190
191   startEndCount = (jnEnd - jnStart) + 1;
192   ranges        = [NSMutableArray arrayWithCapacity:startEndCount];
193   for (i = 0 ; i < startEndCount; i++) {
194     long jnCurrent;
195     
196     jnCurrent = jnStart + i;
197     if (jnCurrent >= jnFirst) {
198       long jnTest;
199       
200       jnTest = jnCurrent - jnFirst;
201       if ((jnTest % interval) == 0) {
202         NSCalendarDate      *start, *end;
203         NGCalendarDateRange *r;
204       
205         start = [NSCalendarDate dateForJulianNumber:jnCurrent];
206         start = [start hour:  [firStart hourOfDay]
207                        minute:[firStart minuteOfHour]
208                        second:[firStart secondOfMinute]];
209         end   = [start addTimeInterval:[self->firstRange duration]];
210         r     = [NGCalendarDateRange calendarDateRangeWithStartDate:start
211                                      endDate:end];
212         [ranges addObject:r];
213       }
214     }
215   }
216   return ranges;
217 }
218
219 - (NSCalendarDate *)lastInstanceStartDate {
220   if ([self->rrule repeatCount] > 0) {
221     long           jnFirst, jnRuleLast;
222     NSCalendarDate *firStart, *until;
223
224     firStart   = [self->firstRange startDate];
225     jnFirst    = [firStart julianNumber];
226     jnRuleLast = ([self->rrule repeatInterval] *
227                   [self->rrule repeatCount]) +
228                   jnFirst;
229     until      = [NSCalendarDate dateForJulianNumber:jnRuleLast];
230     until      = [until hour:  [firStart hourOfDay]
231                         minute:[firStart minuteOfHour]
232                         second:[firStart secondOfMinute]];
233     return until;
234   }
235   return [super lastInstanceStartDate];
236 }
237
238 @end /* iCalDailyRecurrenceCalculator */
239
240
241 /*
242    TODO: If BYDAY is specified, lastInstanceStartDate and recurrences will
243          differ significantly!
244 */
245 @implementation iCalWeeklyRecurrenceCalculator
246
247 - (NSArray *)recurrenceRangesWithinCalendarDateRange:(NGCalendarDateRange *)_r {
248   NSMutableArray *ranges;
249   NSCalendarDate *firStart;
250   long           i, jnFirst, jnStart, jnEnd, startEndCount;
251   unsigned       interval, byDayMask;
252
253   firStart = [self->firstRange startDate];
254   jnFirst  = [firStart julianNumber];
255   jnEnd    = [[_r endDate] julianNumber];
256   
257   if (jnFirst > jnEnd)
258     return nil;
259   
260   jnStart  = [[_r startDate] julianNumber];
261   interval = [self->rrule repeatInterval];
262   
263   /* if rule is bound, check the bounds */
264   if (![self->rrule isInfinite]) {
265     NSCalendarDate *until;
266     long           jnRuleLast;
267     
268     until = [self->rrule untilDate];
269     if (until) {
270       if ([until compare:[_r startDate]] == NSOrderedAscending)
271         return nil;
272       jnRuleLast = [until julianNumber];
273     }
274     else {
275       jnRuleLast = (interval * [self->rrule repeatCount] * 7)
276       + jnFirst;
277       if (jnRuleLast < jnStart)
278         return nil;
279     }
280     /* jnStart < jnRuleLast < jnEnd ? */
281     if (jnEnd > jnRuleLast)
282       jnEnd = jnRuleLast;
283   }
284   
285   startEndCount = (jnEnd - jnStart) + 1;
286   ranges        = [NSMutableArray arrayWithCapacity:startEndCount];
287   byDayMask     = [self->rrule byDayMask];
288   if (!byDayMask) {
289     for (i = 0 ; i < startEndCount; i++) {
290       long jnCurrent;
291       
292       jnCurrent = jnStart + i;
293       if (jnCurrent >= jnFirst) {
294         long jnDiff;
295         
296         jnDiff = jnCurrent - jnFirst; /* difference in days */
297         if ((jnDiff % (interval * 7)) == 0) {
298           NSCalendarDate      *start, *end;
299           NGCalendarDateRange *r;
300           
301           start = [NSCalendarDate dateForJulianNumber:jnCurrent];
302           start = [start hour:  [firStart hourOfDay]
303                          minute:[firStart minuteOfHour]
304                          second:[firStart secondOfMinute]];
305           end   = [start addTimeInterval:[self->firstRange duration]];
306           r     = [NGCalendarDateRange calendarDateRangeWithStartDate:start
307                                        endDate:end];
308           [ranges addObject:r];
309         }
310       }
311     }
312   }
313   else {
314     [self errorWithFormat:@"Cannot calculate rules with byDayMask, yet!"];
315   }
316   return ranges;
317 }
318
319 - (NSCalendarDate *)lastInstanceStartDate {
320   if ([self->rrule repeatCount] > 0) {
321     long           jnFirst, jnRuleLast;
322     NSCalendarDate *firStart, *until;
323     
324     firStart   = [self->firstRange startDate];
325     jnFirst    = [firStart julianNumber];
326     jnRuleLast = ([self->rrule repeatInterval] *
327                   [self->rrule repeatCount] * 7) +
328                   jnFirst;
329     until      = [NSCalendarDate dateForJulianNumber:jnRuleLast];
330     until      = [until hour:  [firStart hourOfDay]
331                         minute:[firStart minuteOfHour]
332                         second:[firStart secondOfMinute]];
333     return until;
334   }
335   return [super lastInstanceStartDate];
336 }
337
338 @end /* iCalWeeklyRecurrenceCalculator */
339
340 @implementation iCalMonthlyRecurrenceCalculator
341
342 - (NSArray *)recurrenceRangesWithinCalendarDateRange:(NGCalendarDateRange *)_r {
343   NSMutableArray *ranges;
344   NSCalendarDate *firStart, *rStart, *rEnd, *until;
345   unsigned       i, count, interval, diff;
346
347   firStart = [self->firstRange startDate];
348   rStart   = [_r startDate];
349   rEnd     = [_r endDate];
350   interval = [self->rrule repeatInterval];
351   until    = [self lastInstanceStartDate];
352
353   if (until) {
354     if ([until compare:rStart] == NSOrderedAscending)
355       return nil;
356     if ([until compare:rEnd] == NSOrderedDescending)
357       rEnd = until;
358   }
359
360   diff   = [firStart monthsBetweenDate:rStart];
361   count  = [rStart monthsBetweenDate:rEnd] + 1;
362   ranges = [NSMutableArray arrayWithCapacity:count];
363   for (i = 0 ; i < count; i++) {
364     unsigned test;
365     
366     test = diff + i;
367     if ((test % interval) == 0) {
368       NSCalendarDate      *start, *end;
369       NGCalendarDateRange *r;
370       
371       start = [firStart dateByAddingYears:0
372                         months:diff + i
373                         days:0];
374       end   = [start addTimeInterval:[self->firstRange duration]];
375       r     = [NGCalendarDateRange calendarDateRangeWithStartDate:start
376                                    endDate:end];
377       [ranges addObject:r];
378     }
379   }
380   return ranges;
381 }
382
383 - (NSCalendarDate *)lastInstanceStartDate {
384   if ([self->rrule repeatCount] > 0) {
385     NSCalendarDate *until;
386     unsigned       months, interval;
387
388     interval = [self->rrule repeatInterval];
389     months   = [self->rrule repeatCount] * interval;
390     until    = [[self->firstRange startDate] dateByAddingYears:0
391                                              months:months
392                                              days:0];
393     return until;
394   }
395   return [super lastInstanceStartDate];
396 }
397
398 @end /* iCalMonthlyRecurrenceCalculator */
399
400 @implementation iCalYearlyRecurrenceCalculator
401
402 - (NSArray *)recurrenceRangesWithinCalendarDateRange:(NGCalendarDateRange *)_r {
403   NSMutableArray *ranges;
404   NSCalendarDate *firStart, *rStart, *rEnd, *until;
405   unsigned       i, count, interval, diff;
406   
407   firStart = [self->firstRange startDate];
408   rStart   = [_r startDate];
409   rEnd     = [_r endDate];
410   interval = [self->rrule repeatInterval];
411   until    = [self lastInstanceStartDate];
412   
413   if (until) {
414     if ([until compare:rStart] == NSOrderedAscending)
415       return nil;
416     if ([until compare:rEnd] == NSOrderedDescending)
417       rEnd = until;
418   }
419   
420   diff   = [firStart yearsBetweenDate:rStart];
421   count  = [rStart yearsBetweenDate:rEnd] + 1;
422   ranges = [NSMutableArray arrayWithCapacity:count];
423   for (i = 0 ; i < count; i++) {
424     unsigned test;
425     
426     test = diff + i;
427     if ((test % interval) == 0) {
428       NSCalendarDate      *start, *end;
429       NGCalendarDateRange *r;
430       
431       start = [firStart dateByAddingYears:diff + i
432                         months:0
433                         days:0];
434       end   = [start addTimeInterval:[self->firstRange duration]];
435       r     = [NGCalendarDateRange calendarDateRangeWithStartDate:start
436                                    endDate:end];
437       [ranges addObject:r];
438     }
439   }
440   return ranges;
441 }
442
443 - (NSCalendarDate *)lastInstanceStartDate {
444   if ([self->rrule repeatCount] > 0) {
445     NSCalendarDate *until;
446     unsigned       years, interval;
447     
448     interval = [self->rrule repeatInterval];
449     years    = [self->rrule repeatCount] * interval;
450     until    = [[self->firstRange startDate] dateByAddingYears:years
451                                              months:0
452                                              days:0];
453     return until;
454   }
455   return [super lastInstanceStartDate];
456 }
457
458 @end /* iCalYearlyRecurrenceCalculator */