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