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