]> err.no Git - sope/blob - sope-ical/NGiCal/iCalRecurrenceCalculator.m
797037e1ab4290e38964d449ed37699f8b3a67ec
[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
55 - (unsigned)offsetFromSundayForJulianNumber:(long)_jn;
56 - (unsigned)offsetFromSundayForWeekDay:(iCalWeekDay)_weekDay;
57 - (unsigned)offsetFromSundayForCurrentWeekStart;
58   
59 - (iCalWeekDay)weekDayForJulianNumber:(long)_jn;
60 @end
61
62 @implementation iCalRecurrenceCalculator
63
64 static Class NSCalendarDateClass     = Nil;
65 static Class iCalRecurrenceRuleClass = Nil;
66 static Class dailyCalcClass   = Nil;
67 static Class weeklyCalcClass  = Nil;
68 static Class monthlyCalcClass = Nil;
69 static Class yearlyCalcClass  = Nil;
70
71 + (void)initialize {
72   static BOOL didInit = NO;
73   
74   if (didInit) return;
75   didInit = YES;
76
77   NSCalendarDateClass     = [NSCalendarDate class];
78   iCalRecurrenceRuleClass = [iCalRecurrenceRule class];
79
80   dailyCalcClass   = [iCalDailyRecurrenceCalculator   class];
81   weeklyCalcClass  = [iCalWeeklyRecurrenceCalculator  class];
82   monthlyCalcClass = [iCalMonthlyRecurrenceCalculator class];
83   yearlyCalcClass  = [iCalYearlyRecurrenceCalculator  class];
84 }
85
86 /* factory */
87
88 + (id)recurrenceCalculatorForRecurrenceRule:(iCalRecurrenceRule *)_rrule
89          withFirstInstanceCalendarDateRange:(NGCalendarDateRange *)_range
90 {
91   return [[[self alloc] initWithRecurrenceRule:_rrule
92                         firstInstanceCalendarDateRange:_range] autorelease];
93 }
94
95 /* complex calculation convenience */
96
97 + (NSArray *)recurrenceRangesWithinCalendarDateRange:(NGCalendarDateRange *)_r
98   firstInstanceCalendarDateRange:(NGCalendarDateRange *)_fir
99   recurrenceRules:(NSArray *)_rRules
100   exceptionRules:(NSArray *)_exRules
101   exceptionDates:(NSArray *)_exDates
102 {
103   id                       rule;
104   iCalRecurrenceCalculator *calc;
105   NSMutableArray           *ranges;
106   NSMutableArray           *exDates;
107   unsigned                 i, count, rCount;
108   
109   ranges = [NSMutableArray array];
110   count  = [_rRules count];
111   for (i = 0; i < count; i++) {
112     NSArray *rs;
113
114     rule = [_rRules objectAtIndex:i];
115     if (![rule isKindOfClass:iCalRecurrenceRuleClass])
116       rule = [iCalRecurrenceRule recurrenceRuleWithICalRepresentation:rule];
117   
118     calc = [self recurrenceCalculatorForRecurrenceRule:rule
119                  withFirstInstanceCalendarDateRange:_fir];
120     rs   = [calc recurrenceRangesWithinCalendarDateRange:_r];
121     [ranges addObjectsFromArray:rs];
122   }
123   
124   if (![ranges count])
125     return nil;
126   
127   /* test if any exceptions do match */
128   count = [_exRules count];
129   for (i = 0; i < count; i++) {
130     NSArray *rs;
131
132     rule = [_exRules objectAtIndex:i];
133     if (![rule isKindOfClass:iCalRecurrenceRuleClass])
134       rule = [iCalRecurrenceRule recurrenceRuleWithICalRepresentation:rule];
135
136     calc = [self recurrenceCalculatorForRecurrenceRule:rule
137                  withFirstInstanceCalendarDateRange:_fir];
138     rs   = [calc recurrenceRangesWithinCalendarDateRange:_r];
139     [ranges removeObjectsInArray:rs];
140   }
141   
142   if (![ranges count])
143     return nil;
144   
145   /* exception dates */
146
147   count  = [_exDates count];
148   if (!count) return ranges;
149
150   /* sort out exDates not within range */
151
152   exDates = [NSMutableArray arrayWithCapacity:count];
153   for (i = 0; i < count; i++) {
154     id exDate;
155
156     exDate = [_exDates objectAtIndex:i];
157     if (![exDate isKindOfClass:NSCalendarDateClass]) {
158       exDate = [NSCalendarDate calendarDateWithICalRepresentation:exDate];
159     }
160     if ([_r containsDate:exDate])
161       [exDates addObject:exDate];
162   }
163
164   /* remove matching exDates from ranges */
165
166   count  = [exDates count];
167   if (!count) return ranges;
168
169   rCount = [ranges count];
170   for (i = 0; i < count; i++) {
171     NSCalendarDate      *exDate;
172     NGCalendarDateRange *r;
173     unsigned            k;
174
175     exDate = [exDates objectAtIndex:i];
176     for (k = 0; k < rCount; k++) {
177       unsigned rIdx;
178       
179       rIdx = (rCount - k) - 1;
180       r    = [ranges objectAtIndex:rIdx];
181       if ([r containsDate:exDate]) {
182         [ranges removeObjectAtIndex:rIdx];
183         rCount--;
184         break; /* this is safe because we know that ranges don't overlap */
185       }
186     }
187   }
188   return ranges;
189 }
190
191
192 /* init */
193
194 - (id)initWithRecurrenceRule:(iCalRecurrenceRule *)_rrule
195   firstInstanceCalendarDateRange:(NGCalendarDateRange *)_range
196 {
197   iCalRecurrenceFrequency freq;
198   Class calcClass = Nil;
199
200   freq = [_rrule frequency];
201   if (freq == iCalRecurrenceFrequenceDaily)
202     calcClass = dailyCalcClass;
203   else if (freq == iCalRecurrenceFrequenceWeekly)
204     calcClass = weeklyCalcClass;
205   else if (freq == iCalRecurrenceFrequenceMonthly)
206     calcClass = monthlyCalcClass;
207   else if (freq == iCalRecurrenceFrequenceYearly)
208     calcClass = yearlyCalcClass;
209
210   [self autorelease];
211   if (calcClass == Nil)
212     return nil;
213
214   self = [[calcClass alloc] init];
215   ASSIGN(self->rrule, _rrule);
216   ASSIGN(self->firstRange, _range);
217   return self;  
218 }
219
220 /* helpers */
221
222 - (unsigned)offsetFromSundayForJulianNumber:(long)_jn {
223   return (unsigned)((int)(_jn + 1.5)) % 7;
224 }
225
226 - (unsigned)offsetFromSundayForWeekDay:(iCalWeekDay)_weekDay {
227   unsigned offset;
228   
229   switch (_weekDay) {
230     case iCalWeekDaySunday:    offset = 0; break;
231     case iCalWeekDayMonday:    offset = 1; break;
232     case iCalWeekDayTuesday:   offset = 2; break;
233     case iCalWeekDayWednesday: offset = 3; break;
234     case iCalWeekDayThursday:  offset = 4; break;
235     case iCalWeekDayFriday:    offset = 5; break;
236     case iCalWeekDaySaturday:  offset = 6; break;
237     default:                   offset = 0; break;
238   }
239   return offset;
240 }
241
242 - (unsigned)offsetFromSundayForCurrentWeekStart {
243   return [self offsetFromSundayForWeekDay:[self->rrule weekStart]];
244 }
245
246 - (iCalWeekDay)weekDayForJulianNumber:(long)_jn {
247   unsigned    day;
248   iCalWeekDay weekDay;
249
250   day = [self offsetFromSundayForJulianNumber:_jn];
251   switch (day) {
252     case 0:  weekDay = iCalWeekDaySunday;    break;
253     case 1:  weekDay = iCalWeekDayMonday;    break;
254     case 2:  weekDay = iCalWeekDayTuesday;   break;
255     case 3:  weekDay = iCalWeekDayWednesday; break;
256     case 4:  weekDay = iCalWeekDayThursday;  break;
257     case 5:  weekDay = iCalWeekDayFriday;    break;
258     case 6:  weekDay = iCalWeekDaySaturday;  break;
259     default: weekDay = iCalWeekDaySunday;    break; /* keep compiler happy */
260   }
261   return weekDay;
262 }
263
264 /* calculation */
265
266 - (NSArray *)recurrenceRangesWithinCalendarDateRange:(NGCalendarDateRange *)_r {
267   return nil; /* subclass responsibility */
268 }
269 - (BOOL)doesRecurrWithinCalendarDateRange:(NGCalendarDateRange *)_range {
270   NSArray *ranges;
271
272   ranges = [self recurrenceRangesWithinCalendarDateRange:_range];
273   return (ranges == nil || [ranges count] == 0) ? NO : YES;
274 }
275
276 - (NGCalendarDateRange *)firstInstanceCalendarDateRange {
277   return self->firstRange;
278 }
279
280 - (NGCalendarDateRange *)lastInstanceCalendarDateRange {
281   NSCalendarDate *start, *end;
282
283   start = [self lastInstanceStartDate];
284   if (!start)
285     return nil;
286   end   = [start addTimeInterval:[self->firstRange duration]];
287   return [NGCalendarDateRange calendarDateRangeWithStartDate:start
288                               endDate:end];
289 }
290
291 - (NSCalendarDate *)lastInstanceStartDate {
292   NSCalendarDate *until;
293   
294   /* NOTE: this is horribly inaccurate and doesn't even consider the use
295   of repeatCount. It MUST be implemented by subclasses properly! However,
296   it does the trick for SOGO 1.0 - that's why it's left here.
297   */
298   if ((until = [self->rrule untilDate]) != nil)
299     return until;
300   return nil;
301 }
302
303 @end /* iCalRecurrenceCalculator */
304
305
306 @implementation iCalDailyRecurrenceCalculator
307
308 - (NSArray *)recurrenceRangesWithinCalendarDateRange:(NGCalendarDateRange *)_r {
309   NSMutableArray *ranges;
310   NSCalendarDate *firStart;
311   long           i, jnFirst, jnStart, jnEnd, startEndCount;
312   unsigned       interval;
313
314   firStart = [self->firstRange startDate];
315   jnFirst  = [firStart julianNumber];
316   jnEnd    = [[_r endDate] julianNumber];
317   
318   if (jnFirst > jnEnd)
319     return nil;
320   
321   jnStart  = [[_r startDate] julianNumber];
322   interval = [self->rrule repeatInterval];
323   
324   /* if rule is bound, check the bounds */
325   if (![self->rrule isInfinite]) {
326     NSCalendarDate *until;
327     long           jnRuleLast;
328     
329     until = [self->rrule untilDate];
330     if (until) {
331       if ([until compare:[_r startDate]] == NSOrderedAscending)
332         return nil;
333       jnRuleLast = [until julianNumber];
334     }
335     else {
336       jnRuleLast = (interval * [self->rrule repeatCount])
337       + jnFirst;
338       if (jnRuleLast < jnStart)
339         return nil;
340     }
341     /* jnStart < jnRuleLast < jnEnd ? */
342     if (jnEnd > jnRuleLast)
343       jnEnd = jnRuleLast;
344   }
345
346   startEndCount = (jnEnd - jnStart) + 1;
347   ranges        = [NSMutableArray arrayWithCapacity:startEndCount];
348   for (i = 0 ; i < startEndCount; i++) {
349     long jnCurrent;
350     
351     jnCurrent = jnStart + i;
352     if (jnCurrent >= jnFirst) {
353       long jnTest;
354       
355       jnTest = jnCurrent - jnFirst;
356       if ((jnTest % interval) == 0) {
357         NSCalendarDate      *start, *end;
358         NGCalendarDateRange *r;
359       
360         start = [NSCalendarDate dateForJulianNumber:jnCurrent];
361         [start setTimeZone:[firStart timeZone]];
362         start = [start hour:  [firStart hourOfDay]
363                        minute:[firStart minuteOfHour]
364                        second:[firStart secondOfMinute]];
365         end   = [start addTimeInterval:[self->firstRange duration]];
366         r     = [NGCalendarDateRange calendarDateRangeWithStartDate:start
367                                      endDate:end];
368         if ([_r containsDateRange:r])
369           [ranges addObject:r];
370       }
371     }
372   }
373   return ranges;
374 }
375
376 - (NSCalendarDate *)lastInstanceStartDate {
377   if ([self->rrule repeatCount] > 0) {
378     long           jnFirst, jnRuleLast;
379     NSCalendarDate *firStart, *until;
380
381     firStart   = [self->firstRange startDate];
382     jnFirst    = [firStart julianNumber];
383     jnRuleLast = ([self->rrule repeatInterval] *
384                   [self->rrule repeatCount]) +
385                   jnFirst;
386     until      = [NSCalendarDate dateForJulianNumber:jnRuleLast];
387     until      = [until hour:  [firStart hourOfDay]
388                         minute:[firStart minuteOfHour]
389                         second:[firStart secondOfMinute]];
390     return until;
391   }
392   return [super lastInstanceStartDate];
393 }
394
395 @end /* iCalDailyRecurrenceCalculator */
396
397
398 /*
399    TODO: If BYDAY is specified, lastInstanceStartDate and recurrences will
400          differ significantly!
401 */
402 @implementation iCalWeeklyRecurrenceCalculator
403
404 - (NSArray *)recurrenceRangesWithinCalendarDateRange:(NGCalendarDateRange *)_r {
405   NSMutableArray *ranges;
406   NSCalendarDate *firStart;
407   long           i, jnFirst, jnStart, jnEnd, startEndCount;
408   unsigned       interval, byDayMask;
409
410   firStart = [self->firstRange startDate];
411   jnFirst  = [firStart julianNumber];
412   jnEnd    = [[_r endDate] julianNumber];
413   
414   if (jnFirst > jnEnd)
415     return nil;
416   
417   jnStart  = [[_r startDate] julianNumber];
418   interval = [self->rrule repeatInterval];
419   
420   /* if rule is bound, check the bounds */
421   if (![self->rrule isInfinite]) {
422     NSCalendarDate *until;
423     long           jnRuleLast;
424     
425     until = [self->rrule untilDate];
426     if (until) {
427       if ([until compare:[_r startDate]] == NSOrderedAscending)
428         return nil;
429       jnRuleLast = [until julianNumber];
430     }
431     else {
432       jnRuleLast = (interval * [self->rrule repeatCount] * 7)
433       + jnFirst;
434       if (jnRuleLast < jnStart)
435         return nil;
436     }
437     /* jnStart < jnRuleLast < jnEnd ? */
438     if (jnEnd > jnRuleLast)
439       jnEnd = jnRuleLast;
440   }
441   
442   startEndCount = (jnEnd - jnStart) + 1;
443   ranges        = [NSMutableArray arrayWithCapacity:startEndCount];
444   byDayMask     = [self->rrule byDayMask];
445   if (!byDayMask) {
446     for (i = 0 ; i < startEndCount; i++) {
447       long jnCurrent;
448       
449       jnCurrent = jnStart + i;
450       if (jnCurrent >= jnFirst) {
451         long jnDiff;
452         
453         jnDiff = jnCurrent - jnFirst; /* difference in days */
454         if ((jnDiff % (interval * 7)) == 0) {
455           NSCalendarDate      *start, *end;
456           NGCalendarDateRange *r;
457           
458           start = [NSCalendarDate dateForJulianNumber:jnCurrent];
459           [start setTimeZone:[firStart timeZone]];
460           start = [start hour:  [firStart hourOfDay]
461                          minute:[firStart minuteOfHour]
462                          second:[firStart secondOfMinute]];
463           end   = [start addTimeInterval:[self->firstRange duration]];
464           r     = [NGCalendarDateRange calendarDateRangeWithStartDate:start
465                                        endDate:end];
466           if ([_r containsDateRange:r])
467             [ranges addObject:r];
468         }
469       }
470     }
471   }
472   else {
473     long jnFirstWeekStart, weekStartOffset;
474
475     /* calculate jnFirst's week start - this depends on our setting of week
476        start */
477     weekStartOffset = [self offsetFromSundayForJulianNumber:jnFirst] -
478                       [self offsetFromSundayForCurrentWeekStart];
479
480     jnFirstWeekStart = jnFirst - weekStartOffset;
481
482     for (i = 0 ; i < startEndCount; i++) {
483       long jnCurrent;
484
485       jnCurrent = jnStart + i;
486       if (jnCurrent >= jnFirst) {
487         long jnDiff;
488         
489         /* we need to calculate a difference in weeks */
490         jnDiff = (jnCurrent - jnFirstWeekStart) % 7;
491         if ((jnDiff % interval) == 0) {
492           BOOL isRecurrence = NO;
493             
494           if (jnCurrent == jnFirst) {
495             isRecurrence = YES;
496           }
497           else {
498             iCalWeekDay weekDay;
499
500             weekDay = [self weekDayForJulianNumber:jnCurrent];
501             isRecurrence = (weekDay & [self->rrule byDayMask]) ? YES : NO;
502           }
503           if (isRecurrence) {
504             NSCalendarDate      *start, *end;
505             NGCalendarDateRange *r;
506                 
507             start = [NSCalendarDate dateForJulianNumber:jnCurrent];
508             [start setTimeZone:[firStart timeZone]];
509             start = [start hour:  [firStart hourOfDay]
510                            minute:[firStart minuteOfHour]
511                            second:[firStart secondOfMinute]];
512             end   = [start addTimeInterval:[self->firstRange duration]];
513             r     = [NGCalendarDateRange calendarDateRangeWithStartDate:start
514                                          endDate:end];
515             if ([_r containsDateRange:r])
516               [ranges addObject:r];
517           }
518         }
519       }
520     }
521   }
522   return ranges;
523 }
524
525 - (NSCalendarDate *)lastInstanceStartDate {
526   if ([self->rrule repeatCount] > 0) {
527     long           jnFirst, jnRuleLast;
528     NSCalendarDate *firStart, *until;
529     
530     firStart   = [self->firstRange startDate];
531     jnFirst    = [firStart julianNumber];
532     jnRuleLast = ([self->rrule repeatInterval] *
533                   [self->rrule repeatCount] * 7) +
534                   jnFirst;
535     until      = [NSCalendarDate dateForJulianNumber:jnRuleLast];
536     until      = [until hour:  [firStart hourOfDay]
537                         minute:[firStart minuteOfHour]
538                         second:[firStart secondOfMinute]];
539     return until;
540   }
541   return [super lastInstanceStartDate];
542 }
543
544 @end /* iCalWeeklyRecurrenceCalculator */
545
546 @implementation iCalMonthlyRecurrenceCalculator
547
548 - (NSArray *)recurrenceRangesWithinCalendarDateRange:(NGCalendarDateRange *)_r {
549   NSMutableArray *ranges;
550   NSCalendarDate *firStart, *rStart, *rEnd, *until;
551   unsigned       i, count, interval, diff;
552
553   firStart = [self->firstRange startDate];
554   rStart   = [_r startDate];
555   rEnd     = [_r endDate];
556   interval = [self->rrule repeatInterval];
557   until    = [self lastInstanceStartDate];
558
559   if (until) {
560     if ([until compare:rStart] == NSOrderedAscending)
561       return nil;
562     if ([until compare:rEnd] == NSOrderedDescending)
563       rEnd = until;
564   }
565
566   diff   = [firStart monthsBetweenDate:rStart];
567   count  = [rStart monthsBetweenDate:rEnd] + 1;
568   ranges = [NSMutableArray arrayWithCapacity:count];
569   for (i = 0 ; i < count; i++) {
570     unsigned test;
571     
572     test = diff + i;
573     if ((test % interval) == 0) {
574       NSCalendarDate      *start, *end;
575       NGCalendarDateRange *r;
576       
577       start = [firStart dateByAddingYears:0
578                         months:diff + i
579                         days:0];
580       [start setTimeZone:[firStart timeZone]];
581       end   = [start addTimeInterval:[self->firstRange duration]];
582       r     = [NGCalendarDateRange calendarDateRangeWithStartDate:start
583                                    endDate:end];
584       if ([_r containsDateRange:r])
585         [ranges addObject:r];
586     }
587   }
588   return ranges;
589 }
590
591 - (NSCalendarDate *)lastInstanceStartDate {
592   if ([self->rrule repeatCount] > 0) {
593     NSCalendarDate *until;
594     unsigned       months, interval;
595
596     interval = [self->rrule repeatInterval];
597     months   = [self->rrule repeatCount] * interval;
598     until    = [[self->firstRange startDate] dateByAddingYears:0
599                                              months:months
600                                              days:0];
601     return until;
602   }
603   return [super lastInstanceStartDate];
604 }
605
606 @end /* iCalMonthlyRecurrenceCalculator */
607
608 @implementation iCalYearlyRecurrenceCalculator
609
610 - (NSArray *)recurrenceRangesWithinCalendarDateRange:(NGCalendarDateRange *)_r {
611   NSMutableArray *ranges;
612   NSCalendarDate *firStart, *rStart, *rEnd, *until;
613   unsigned       i, count, interval, diff;
614   
615   firStart = [self->firstRange startDate];
616   rStart   = [_r startDate];
617   rEnd     = [_r endDate];
618   interval = [self->rrule repeatInterval];
619   until    = [self lastInstanceStartDate];
620   
621   if (until) {
622     if ([until compare:rStart] == NSOrderedAscending)
623       return nil;
624     if ([until compare:rEnd] == NSOrderedDescending)
625       rEnd = until;
626   }
627   
628   diff   = [firStart yearsBetweenDate:rStart];
629   count  = [rStart yearsBetweenDate:rEnd] + 1;
630   ranges = [NSMutableArray arrayWithCapacity:count];
631   for (i = 0 ; i < count; i++) {
632     unsigned test;
633
634     test = diff + i;
635     if ((test % interval) == 0) {
636       NSCalendarDate      *start, *end;
637       NGCalendarDateRange *r;
638       
639       start = [firStart dateByAddingYears:diff + i
640                         months:0
641                         days:0];
642       [start setTimeZone:[firStart timeZone]];
643       end   = [start addTimeInterval:[self->firstRange duration]];
644       r     = [NGCalendarDateRange calendarDateRangeWithStartDate:start
645                                    endDate:end];
646       if ([_r containsDateRange:r])
647         [ranges addObject:r];
648     }
649   }
650   return ranges;
651 }
652
653 - (NSCalendarDate *)lastInstanceStartDate {
654   if ([self->rrule repeatCount] > 0) {
655     NSCalendarDate *until;
656     unsigned       years, interval;
657     
658     interval = [self->rrule repeatInterval];
659     years    = [self->rrule repeatCount] * interval;
660     until    = [[self->firstRange startDate] dateByAddingYears:years
661                                              months:0
662                                              days:0];
663     return until;
664   }
665   return [super lastInstanceStartDate];
666 }
667
668 @end /* iCalYearlyRecurrenceCalculator */