]> err.no Git - sope/blob - sope-ical/NGiCal/iCalRecurrenceCalculator.m
b141038b9421c89f7b9caa2f9aa3e156f9043891
[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 /* dealloc */
221
222 - (void)dealloc {
223   [self->firstRange release];
224   [self->rrule      release];
225   [super dealloc];
226 }
227
228 /* helpers */
229
230 - (unsigned)offsetFromSundayForJulianNumber:(long)_jn {
231   return (unsigned)((int)(_jn + 1.5)) % 7;
232 }
233
234 - (unsigned)offsetFromSundayForWeekDay:(iCalWeekDay)_weekDay {
235   unsigned offset;
236   
237   switch (_weekDay) {
238     case iCalWeekDaySunday:    offset = 0; break;
239     case iCalWeekDayMonday:    offset = 1; break;
240     case iCalWeekDayTuesday:   offset = 2; break;
241     case iCalWeekDayWednesday: offset = 3; break;
242     case iCalWeekDayThursday:  offset = 4; break;
243     case iCalWeekDayFriday:    offset = 5; break;
244     case iCalWeekDaySaturday:  offset = 6; break;
245     default:                   offset = 0; break;
246   }
247   return offset;
248 }
249
250 - (unsigned)offsetFromSundayForCurrentWeekStart {
251   return [self offsetFromSundayForWeekDay:[self->rrule weekStart]];
252 }
253
254 - (iCalWeekDay)weekDayForJulianNumber:(long)_jn {
255   unsigned    day;
256   iCalWeekDay weekDay;
257
258   day = [self offsetFromSundayForJulianNumber:_jn];
259   switch (day) {
260     case 0:  weekDay = iCalWeekDaySunday;    break;
261     case 1:  weekDay = iCalWeekDayMonday;    break;
262     case 2:  weekDay = iCalWeekDayTuesday;   break;
263     case 3:  weekDay = iCalWeekDayWednesday; break;
264     case 4:  weekDay = iCalWeekDayThursday;  break;
265     case 5:  weekDay = iCalWeekDayFriday;    break;
266     case 6:  weekDay = iCalWeekDaySaturday;  break;
267     default: weekDay = iCalWeekDaySunday;    break; /* keep compiler happy */
268   }
269   return weekDay;
270 }
271
272 /* calculation */
273
274 - (NSArray *)recurrenceRangesWithinCalendarDateRange:(NGCalendarDateRange *)_r {
275   return nil; /* subclass responsibility */
276 }
277 - (BOOL)doesRecurrWithinCalendarDateRange:(NGCalendarDateRange *)_range {
278   NSArray *ranges;
279
280   ranges = [self recurrenceRangesWithinCalendarDateRange:_range];
281   return (ranges == nil || [ranges count] == 0) ? NO : YES;
282 }
283
284 - (NGCalendarDateRange *)firstInstanceCalendarDateRange {
285   return self->firstRange;
286 }
287
288 - (NGCalendarDateRange *)lastInstanceCalendarDateRange {
289   NSCalendarDate *start, *end;
290
291   start = [self lastInstanceStartDate];
292   if (!start)
293     return nil;
294   end   = [start addTimeInterval:[self->firstRange duration]];
295   return [NGCalendarDateRange calendarDateRangeWithStartDate:start
296                               endDate:end];
297 }
298
299 - (NSCalendarDate *)lastInstanceStartDate {
300   NSCalendarDate *until;
301   
302   /* NOTE: this is horribly inaccurate and doesn't even consider the use
303   of repeatCount. It MUST be implemented by subclasses properly! However,
304   it does the trick for SOGO 1.0 - that's why it's left here.
305   */
306   if ((until = [self->rrule untilDate]) != nil)
307     return until;
308   return nil;
309 }
310
311 @end /* iCalRecurrenceCalculator */
312
313
314 @implementation iCalDailyRecurrenceCalculator
315
316 - (NSArray *)recurrenceRangesWithinCalendarDateRange:(NGCalendarDateRange *)_r {
317   NSMutableArray *ranges;
318   NSCalendarDate *firStart;
319   long           i, jnFirst, jnStart, jnEnd, startEndCount;
320   unsigned       interval;
321
322   firStart = [self->firstRange startDate];
323   jnFirst  = [firStart julianNumber];
324   jnEnd    = [[_r endDate] julianNumber];
325   
326   if (jnFirst > jnEnd)
327     return nil;
328   
329   jnStart  = [[_r startDate] julianNumber];
330   interval = [self->rrule repeatInterval];
331   
332   /* if rule is bound, check the bounds */
333   if (![self->rrule isInfinite]) {
334     NSCalendarDate *until;
335     long           jnRuleLast;
336     
337     until = [self->rrule untilDate];
338     if (until) {
339       if ([until compare:[_r startDate]] == NSOrderedAscending)
340         return nil;
341       jnRuleLast = [until julianNumber];
342     }
343     else {
344       jnRuleLast = (interval * [self->rrule repeatCount])
345       + jnFirst;
346       if (jnRuleLast < jnStart)
347         return nil;
348     }
349     /* jnStart < jnRuleLast < jnEnd ? */
350     if (jnEnd > jnRuleLast)
351       jnEnd = jnRuleLast;
352   }
353
354   startEndCount = (jnEnd - jnStart) + 1;
355   ranges        = [NSMutableArray arrayWithCapacity:startEndCount];
356   for (i = 0 ; i < startEndCount; i++) {
357     long jnCurrent;
358     
359     jnCurrent = jnStart + i;
360     if (jnCurrent >= jnFirst) {
361       long jnTest;
362       
363       jnTest = jnCurrent - jnFirst;
364       if ((jnTest % interval) == 0) {
365         NSCalendarDate      *start, *end;
366         NGCalendarDateRange *r;
367       
368         start = [NSCalendarDate dateForJulianNumber:jnCurrent];
369         [start setTimeZone:[firStart timeZone]];
370         start = [start hour:  [firStart hourOfDay]
371                        minute:[firStart minuteOfHour]
372                        second:[firStart secondOfMinute]];
373         end   = [start addTimeInterval:[self->firstRange duration]];
374         r     = [NGCalendarDateRange calendarDateRangeWithStartDate:start
375                                      endDate:end];
376         if ([_r containsDateRange:r])
377           [ranges addObject:r];
378       }
379     }
380   }
381   return ranges;
382 }
383
384 - (NSCalendarDate *)lastInstanceStartDate {
385   if ([self->rrule repeatCount] > 0) {
386     long           jnFirst, jnRuleLast;
387     NSCalendarDate *firStart, *until;
388
389     firStart   = [self->firstRange startDate];
390     jnFirst    = [firStart julianNumber];
391     jnRuleLast = ([self->rrule repeatInterval] *
392                   [self->rrule repeatCount]) +
393                   jnFirst;
394     until      = [NSCalendarDate dateForJulianNumber:jnRuleLast];
395     until      = [until hour:  [firStart hourOfDay]
396                         minute:[firStart minuteOfHour]
397                         second:[firStart secondOfMinute]];
398     return until;
399   }
400   return [super lastInstanceStartDate];
401 }
402
403 @end /* iCalDailyRecurrenceCalculator */
404
405
406 /*
407    TODO: If BYDAY is specified, lastInstanceStartDate and recurrences will
408          differ significantly!
409 */
410 @implementation iCalWeeklyRecurrenceCalculator
411
412 - (NSArray *)recurrenceRangesWithinCalendarDateRange:(NGCalendarDateRange *)_r {
413   NSMutableArray *ranges;
414   NSCalendarDate *firStart;
415   long           i, jnFirst, jnStart, jnEnd, startEndCount;
416   unsigned       interval, byDayMask;
417
418   firStart = [self->firstRange startDate];
419   jnFirst  = [firStart julianNumber];
420   jnEnd    = [[_r endDate] julianNumber];
421   
422   if (jnFirst > jnEnd)
423     return nil;
424   
425   jnStart  = [[_r startDate] julianNumber];
426   interval = [self->rrule repeatInterval];
427   
428   /* if rule is bound, check the bounds */
429   if (![self->rrule isInfinite]) {
430     NSCalendarDate *until;
431     long           jnRuleLast;
432     
433     until = [self->rrule untilDate];
434     if (until) {
435       if ([until compare:[_r startDate]] == NSOrderedAscending)
436         return nil;
437       jnRuleLast = [until julianNumber];
438     }
439     else {
440       jnRuleLast = (interval * [self->rrule repeatCount] * 7)
441       + jnFirst;
442       if (jnRuleLast < jnStart)
443         return nil;
444     }
445     /* jnStart < jnRuleLast < jnEnd ? */
446     if (jnEnd > jnRuleLast)
447       jnEnd = jnRuleLast;
448   }
449   
450   startEndCount = (jnEnd - jnStart) + 1;
451   ranges        = [NSMutableArray arrayWithCapacity:startEndCount];
452   byDayMask     = [self->rrule byDayMask];
453   if (!byDayMask) {
454     for (i = 0 ; i < startEndCount; i++) {
455       long jnCurrent;
456       
457       jnCurrent = jnStart + i;
458       if (jnCurrent >= jnFirst) {
459         long jnDiff;
460         
461         jnDiff = jnCurrent - jnFirst; /* difference in days */
462         if ((jnDiff % (interval * 7)) == 0) {
463           NSCalendarDate      *start, *end;
464           NGCalendarDateRange *r;
465           
466           start = [NSCalendarDate dateForJulianNumber:jnCurrent];
467           [start setTimeZone:[firStart timeZone]];
468           start = [start hour:  [firStart hourOfDay]
469                          minute:[firStart minuteOfHour]
470                          second:[firStart secondOfMinute]];
471           end   = [start addTimeInterval:[self->firstRange duration]];
472           r     = [NGCalendarDateRange calendarDateRangeWithStartDate:start
473                                        endDate:end];
474           if ([_r containsDateRange:r])
475             [ranges addObject:r];
476         }
477       }
478     }
479   }
480   else {
481     long jnFirstWeekStart, weekStartOffset;
482
483     /* calculate jnFirst's week start - this depends on our setting of week
484        start */
485     weekStartOffset = [self offsetFromSundayForJulianNumber:jnFirst] -
486                       [self offsetFromSundayForCurrentWeekStart];
487
488     jnFirstWeekStart = jnFirst - weekStartOffset;
489
490     for (i = 0 ; i < startEndCount; i++) {
491       long jnCurrent;
492
493       jnCurrent = jnStart + i;
494       if (jnCurrent >= jnFirst) {
495         long jnDiff;
496         
497         /* we need to calculate a difference in weeks */
498         jnDiff = (jnCurrent - jnFirstWeekStart) % 7;
499         if ((jnDiff % interval) == 0) {
500           BOOL isRecurrence = NO;
501             
502           if (jnCurrent == jnFirst) {
503             isRecurrence = YES;
504           }
505           else {
506             iCalWeekDay weekDay;
507
508             weekDay = [self weekDayForJulianNumber:jnCurrent];
509             isRecurrence = (weekDay & [self->rrule byDayMask]) ? YES : NO;
510           }
511           if (isRecurrence) {
512             NSCalendarDate      *start, *end;
513             NGCalendarDateRange *r;
514                 
515             start = [NSCalendarDate dateForJulianNumber:jnCurrent];
516             [start setTimeZone:[firStart timeZone]];
517             start = [start hour:  [firStart hourOfDay]
518                            minute:[firStart minuteOfHour]
519                            second:[firStart secondOfMinute]];
520             end   = [start addTimeInterval:[self->firstRange duration]];
521             r     = [NGCalendarDateRange calendarDateRangeWithStartDate:start
522                                          endDate:end];
523             if ([_r containsDateRange:r])
524               [ranges addObject:r];
525           }
526         }
527       }
528     }
529   }
530   return ranges;
531 }
532
533 - (NSCalendarDate *)lastInstanceStartDate {
534   if ([self->rrule repeatCount] > 0) {
535     long           jnFirst, jnRuleLast;
536     NSCalendarDate *firStart, *until;
537     
538     firStart   = [self->firstRange startDate];
539     jnFirst    = [firStart julianNumber];
540     jnRuleLast = ([self->rrule repeatInterval] *
541                   [self->rrule repeatCount] * 7) +
542                   jnFirst;
543     until      = [NSCalendarDate dateForJulianNumber:jnRuleLast];
544     until      = [until hour:  [firStart hourOfDay]
545                         minute:[firStart minuteOfHour]
546                         second:[firStart secondOfMinute]];
547     return until;
548   }
549   return [super lastInstanceStartDate];
550 }
551
552 @end /* iCalWeeklyRecurrenceCalculator */
553
554 @implementation iCalMonthlyRecurrenceCalculator
555
556 - (NSArray *)recurrenceRangesWithinCalendarDateRange:(NGCalendarDateRange *)_r {
557   NSMutableArray *ranges;
558   NSCalendarDate *firStart, *rStart, *rEnd, *until;
559   unsigned       i, count, interval;
560   int            diff;
561
562   firStart = [self->firstRange startDate];
563   rStart   = [_r startDate];
564   rEnd     = [_r endDate];
565   interval = [self->rrule repeatInterval];
566   until    = [self lastInstanceStartDate];
567
568   if (until) {
569     if ([until compare:rStart] == NSOrderedAscending)
570       return nil;
571     if ([until compare:rEnd] == NSOrderedDescending)
572       rEnd = until;
573   }
574
575   diff   = [firStart monthsBetweenDate:rStart];
576   if ((diff != 0) && [rStart compare:firStart] == NSOrderedAscending)
577     diff = -diff;
578
579   count  = [rStart monthsBetweenDate:rEnd] + 1;
580   ranges = [NSMutableArray arrayWithCapacity:count];
581   for (i = 0 ; i < count; i++) {
582     int test;
583     
584     test = diff + i;
585     if ((test >= 0) && (test % interval) == 0) {
586       NSCalendarDate      *start, *end;
587       NGCalendarDateRange *r;
588       
589       start = [firStart dateByAddingYears:0
590                         months:diff + i
591                         days:0];
592       [start setTimeZone:[firStart timeZone]];
593       end   = [start addTimeInterval:[self->firstRange duration]];
594       r     = [NGCalendarDateRange calendarDateRangeWithStartDate:start
595                                    endDate:end];
596       if ([_r containsDateRange:r])
597         [ranges addObject:r];
598     }
599   }
600   return ranges;
601 }
602
603 - (NSCalendarDate *)lastInstanceStartDate {
604   if ([self->rrule repeatCount] > 0) {
605     NSCalendarDate *until;
606     unsigned       months, interval;
607
608     interval = [self->rrule repeatInterval];
609     months   = [self->rrule repeatCount] * interval;
610     until    = [[self->firstRange startDate] dateByAddingYears:0
611                                              months:months
612                                              days:0];
613     return until;
614   }
615   return [super lastInstanceStartDate];
616 }
617
618 @end /* iCalMonthlyRecurrenceCalculator */
619
620 @implementation iCalYearlyRecurrenceCalculator
621
622 - (NSArray *)recurrenceRangesWithinCalendarDateRange:(NGCalendarDateRange *)_r {
623   NSMutableArray *ranges;
624   NSCalendarDate *firStart, *rStart, *rEnd, *until;
625   unsigned       i, count, interval;
626   int            diff;
627   
628   firStart = [self->firstRange startDate];
629   rStart   = [_r startDate];
630   rEnd     = [_r endDate];
631   interval = [self->rrule repeatInterval];
632   until    = [self lastInstanceStartDate];
633   
634   if (until) {
635     if ([until compare:rStart] == NSOrderedAscending)
636       return nil;
637     if ([until compare:rEnd] == NSOrderedDescending)
638       rEnd = until;
639   }
640   
641   diff   = [firStart yearsBetweenDate:rStart];
642   if ((diff != 0) && [rStart compare:firStart] == NSOrderedAscending)
643     diff = -diff;
644
645   count  = [rStart yearsBetweenDate:rEnd] + 1;
646   ranges = [NSMutableArray arrayWithCapacity:count];
647   for (i = 0 ; i < count; i++) {
648     int test;
649
650     test = diff + i;
651     if ((test >= 0) && (test % interval) == 0) {
652       NSCalendarDate      *start, *end;
653       NGCalendarDateRange *r;
654       
655       start = [firStart dateByAddingYears:diff + i
656                         months:0
657                         days:0];
658       [start setTimeZone:[firStart timeZone]];
659       end   = [start addTimeInterval:[self->firstRange duration]];
660       r     = [NGCalendarDateRange calendarDateRangeWithStartDate:start
661                                    endDate:end];
662       if ([_r containsDateRange:r])
663         [ranges addObject:r];
664     }
665   }
666   return ranges;
667 }
668
669 - (NSCalendarDate *)lastInstanceStartDate {
670   if ([self->rrule repeatCount] > 0) {
671     NSCalendarDate *until;
672     unsigned       years, interval;
673     
674     interval = [self->rrule repeatInterval];
675     years    = [self->rrule repeatCount] * interval;
676     until    = [[self->firstRange startDate] dateByAddingYears:years
677                                              months:0
678                                              days:0];
679     return until;
680   }
681   return [super lastInstanceStartDate];
682 }
683
684 @end /* iCalYearlyRecurrenceCalculator */