2 Copyright (C) 2004-2005 SKYRIX Software AG
4 This file is part of SOPE.
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
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.
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
22 #include "iCalRecurrenceCalculator.h"
23 #include <NGExtensions/NGCalendarDateRange.h>
24 #include "iCalRecurrenceRule.h"
25 #include "NSCalendarDate+ICal.h"
30 @interface iCalDailyRecurrenceCalculator : iCalRecurrenceCalculator
35 @interface iCalWeeklyRecurrenceCalculator : iCalRecurrenceCalculator
40 @interface iCalMonthlyRecurrenceCalculator : iCalRecurrenceCalculator
45 @interface iCalYearlyRecurrenceCalculator : iCalRecurrenceCalculator
52 @interface iCalRecurrenceCalculator (PrivateAPI)
53 - (NSCalendarDate *)lastInstanceStartDate;
55 - (unsigned)offsetFromSundayForJulianNumber:(long)_jn;
56 - (unsigned)offsetFromSundayForWeekDay:(iCalWeekDay)_weekDay;
57 - (unsigned)offsetFromSundayForCurrentWeekStart;
59 - (iCalWeekDay)weekDayForJulianNumber:(long)_jn;
62 @implementation iCalRecurrenceCalculator
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;
72 static BOOL didInit = NO;
77 NSCalendarDateClass = [NSCalendarDate class];
78 iCalRecurrenceRuleClass = [iCalRecurrenceRule class];
80 dailyCalcClass = [iCalDailyRecurrenceCalculator class];
81 weeklyCalcClass = [iCalWeeklyRecurrenceCalculator class];
82 monthlyCalcClass = [iCalMonthlyRecurrenceCalculator class];
83 yearlyCalcClass = [iCalYearlyRecurrenceCalculator class];
88 + (id)recurrenceCalculatorForRecurrenceRule:(iCalRecurrenceRule *)_rrule
89 withFirstInstanceCalendarDateRange:(NGCalendarDateRange *)_range
91 return [[[self alloc] initWithRecurrenceRule:_rrule
92 firstInstanceCalendarDateRange:_range] autorelease];
95 /* complex calculation convenience */
97 + (NSArray *)recurrenceRangesWithinCalendarDateRange:(NGCalendarDateRange *)_r
98 firstInstanceCalendarDateRange:(NGCalendarDateRange *)_fir
99 recurrenceRules:(NSArray *)_rRules
100 exceptionRules:(NSArray *)_exRules
101 exceptionDates:(NSArray *)_exDates
104 iCalRecurrenceCalculator *calc;
105 NSMutableArray *ranges;
106 NSMutableArray *exDates;
107 unsigned i, count, rCount;
109 ranges = [NSMutableArray array];
110 count = [_rRules count];
111 for (i = 0; i < count; i++) {
114 rule = [_rRules objectAtIndex:i];
115 if (![rule isKindOfClass:iCalRecurrenceRuleClass])
116 rule = [iCalRecurrenceRule recurrenceRuleWithICalRepresentation:rule];
118 calc = [self recurrenceCalculatorForRecurrenceRule:rule
119 withFirstInstanceCalendarDateRange:_fir];
120 rs = [calc recurrenceRangesWithinCalendarDateRange:_r];
121 [ranges addObjectsFromArray:rs];
127 /* test if any exceptions do match */
128 count = [_exRules count];
129 for (i = 0; i < count; i++) {
132 rule = [_exRules objectAtIndex:i];
133 if (![rule isKindOfClass:iCalRecurrenceRuleClass])
134 rule = [iCalRecurrenceRule recurrenceRuleWithICalRepresentation:rule];
136 calc = [self recurrenceCalculatorForRecurrenceRule:rule
137 withFirstInstanceCalendarDateRange:_fir];
138 rs = [calc recurrenceRangesWithinCalendarDateRange:_r];
139 [ranges removeObjectsInArray:rs];
145 /* exception dates */
147 count = [_exDates count];
148 if (!count) return ranges;
150 /* sort out exDates not within range */
152 exDates = [NSMutableArray arrayWithCapacity:count];
153 for (i = 0; i < count; i++) {
156 exDate = [_exDates objectAtIndex:i];
157 if (![exDate isKindOfClass:NSCalendarDateClass]) {
158 exDate = [NSCalendarDate calendarDateWithICalRepresentation:exDate];
160 if ([_r containsDate:exDate])
161 [exDates addObject:exDate];
164 /* remove matching exDates from ranges */
166 count = [exDates count];
167 if (!count) return ranges;
169 rCount = [ranges count];
170 for (i = 0; i < count; i++) {
171 NSCalendarDate *exDate;
172 NGCalendarDateRange *r;
175 exDate = [exDates objectAtIndex:i];
176 for (k = 0; k < rCount; k++) {
179 rIdx = (rCount - k) - 1;
180 r = [ranges objectAtIndex:rIdx];
181 if ([r containsDate:exDate]) {
182 [ranges removeObjectAtIndex:rIdx];
184 break; /* this is safe because we know that ranges don't overlap */
194 - (id)initWithRecurrenceRule:(iCalRecurrenceRule *)_rrule
195 firstInstanceCalendarDateRange:(NGCalendarDateRange *)_range
197 iCalRecurrenceFrequency freq;
198 Class calcClass = Nil;
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;
211 if (calcClass == Nil)
214 self = [[calcClass alloc] init];
215 ASSIGN(self->rrule, _rrule);
216 ASSIGN(self->firstRange, _range);
223 [self->firstRange release];
224 [self->rrule release];
230 - (unsigned)offsetFromSundayForJulianNumber:(long)_jn {
231 return (unsigned)((int)(_jn + 1.5)) % 7;
234 - (unsigned)offsetFromSundayForWeekDay:(iCalWeekDay)_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;
250 - (unsigned)offsetFromSundayForCurrentWeekStart {
251 return [self offsetFromSundayForWeekDay:[self->rrule weekStart]];
254 - (iCalWeekDay)weekDayForJulianNumber:(long)_jn {
258 day = [self offsetFromSundayForJulianNumber:_jn];
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 */
274 - (NSArray *)recurrenceRangesWithinCalendarDateRange:(NGCalendarDateRange *)_r {
275 return nil; /* subclass responsibility */
277 - (BOOL)doesRecurrWithinCalendarDateRange:(NGCalendarDateRange *)_range {
280 ranges = [self recurrenceRangesWithinCalendarDateRange:_range];
281 return (ranges == nil || [ranges count] == 0) ? NO : YES;
284 - (NGCalendarDateRange *)firstInstanceCalendarDateRange {
285 return self->firstRange;
288 - (NGCalendarDateRange *)lastInstanceCalendarDateRange {
289 NSCalendarDate *start, *end;
291 start = [self lastInstanceStartDate];
294 end = [start addTimeInterval:[self->firstRange duration]];
295 return [NGCalendarDateRange calendarDateRangeWithStartDate:start
299 - (NSCalendarDate *)lastInstanceStartDate {
300 NSCalendarDate *until;
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.
306 if ((until = [self->rrule untilDate]) != nil)
311 @end /* iCalRecurrenceCalculator */
314 @implementation iCalDailyRecurrenceCalculator
316 - (NSArray *)recurrenceRangesWithinCalendarDateRange:(NGCalendarDateRange *)_r {
317 NSMutableArray *ranges;
318 NSCalendarDate *firStart;
319 long i, jnFirst, jnStart, jnEnd, startEndCount;
322 firStart = [self->firstRange startDate];
323 jnFirst = [firStart julianNumber];
324 jnEnd = [[_r endDate] julianNumber];
329 jnStart = [[_r startDate] julianNumber];
330 interval = [self->rrule repeatInterval];
332 /* if rule is bound, check the bounds */
333 if (![self->rrule isInfinite]) {
334 NSCalendarDate *until;
337 until = [self->rrule untilDate];
339 if ([until compare:[_r startDate]] == NSOrderedAscending)
341 jnRuleLast = [until julianNumber];
344 jnRuleLast = (interval * [self->rrule repeatCount])
346 if (jnRuleLast < jnStart)
349 /* jnStart < jnRuleLast < jnEnd ? */
350 if (jnEnd > jnRuleLast)
354 startEndCount = (jnEnd - jnStart) + 1;
355 ranges = [NSMutableArray arrayWithCapacity:startEndCount];
356 for (i = 0 ; i < startEndCount; i++) {
359 jnCurrent = jnStart + i;
360 if (jnCurrent >= jnFirst) {
363 jnTest = jnCurrent - jnFirst;
364 if ((jnTest % interval) == 0) {
365 NSCalendarDate *start, *end;
366 NGCalendarDateRange *r;
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
376 if ([_r containsDateRange:r])
377 [ranges addObject:r];
384 - (NSCalendarDate *)lastInstanceStartDate {
385 if ([self->rrule repeatCount] > 0) {
386 long jnFirst, jnRuleLast;
387 NSCalendarDate *firStart, *until;
389 firStart = [self->firstRange startDate];
390 jnFirst = [firStart julianNumber];
391 jnRuleLast = ([self->rrule repeatInterval] *
392 [self->rrule repeatCount]) +
394 until = [NSCalendarDate dateForJulianNumber:jnRuleLast];
395 until = [until hour: [firStart hourOfDay]
396 minute:[firStart minuteOfHour]
397 second:[firStart secondOfMinute]];
400 return [super lastInstanceStartDate];
403 @end /* iCalDailyRecurrenceCalculator */
407 TODO: If BYDAY is specified, lastInstanceStartDate and recurrences will
408 differ significantly!
410 @implementation iCalWeeklyRecurrenceCalculator
412 - (NSArray *)recurrenceRangesWithinCalendarDateRange:(NGCalendarDateRange *)_r {
413 NSMutableArray *ranges;
414 NSCalendarDate *firStart;
415 long i, jnFirst, jnStart, jnEnd, startEndCount;
416 unsigned interval, byDayMask;
418 firStart = [self->firstRange startDate];
419 jnFirst = [firStart julianNumber];
420 jnEnd = [[_r endDate] julianNumber];
425 jnStart = [[_r startDate] julianNumber];
426 interval = [self->rrule repeatInterval];
428 /* if rule is bound, check the bounds */
429 if (![self->rrule isInfinite]) {
430 NSCalendarDate *until;
433 until = [self->rrule untilDate];
435 if ([until compare:[_r startDate]] == NSOrderedAscending)
437 jnRuleLast = [until julianNumber];
440 jnRuleLast = (interval * [self->rrule repeatCount] * 7)
442 if (jnRuleLast < jnStart)
445 /* jnStart < jnRuleLast < jnEnd ? */
446 if (jnEnd > jnRuleLast)
450 startEndCount = (jnEnd - jnStart) + 1;
451 ranges = [NSMutableArray arrayWithCapacity:startEndCount];
452 byDayMask = [self->rrule byDayMask];
454 for (i = 0 ; i < startEndCount; i++) {
457 jnCurrent = jnStart + i;
458 if (jnCurrent >= jnFirst) {
461 jnDiff = jnCurrent - jnFirst; /* difference in days */
462 if ((jnDiff % (interval * 7)) == 0) {
463 NSCalendarDate *start, *end;
464 NGCalendarDateRange *r;
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
474 if ([_r containsDateRange:r])
475 [ranges addObject:r];
481 long jnFirstWeekStart, weekStartOffset;
483 /* calculate jnFirst's week start - this depends on our setting of week
485 weekStartOffset = [self offsetFromSundayForJulianNumber:jnFirst] -
486 [self offsetFromSundayForCurrentWeekStart];
488 jnFirstWeekStart = jnFirst - weekStartOffset;
490 for (i = 0 ; i < startEndCount; i++) {
493 jnCurrent = jnStart + i;
494 if (jnCurrent >= jnFirst) {
497 /* we need to calculate a difference in weeks */
498 jnDiff = (jnCurrent - jnFirstWeekStart) % 7;
499 if ((jnDiff % interval) == 0) {
500 BOOL isRecurrence = NO;
502 if (jnCurrent == jnFirst) {
508 weekDay = [self weekDayForJulianNumber:jnCurrent];
509 isRecurrence = (weekDay & [self->rrule byDayMask]) ? YES : NO;
512 NSCalendarDate *start, *end;
513 NGCalendarDateRange *r;
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
523 if ([_r containsDateRange:r])
524 [ranges addObject:r];
533 - (NSCalendarDate *)lastInstanceStartDate {
534 if ([self->rrule repeatCount] > 0) {
535 long jnFirst, jnRuleLast;
536 NSCalendarDate *firStart, *until;
538 firStart = [self->firstRange startDate];
539 jnFirst = [firStart julianNumber];
540 jnRuleLast = ([self->rrule repeatInterval] *
541 [self->rrule repeatCount] * 7) +
543 until = [NSCalendarDate dateForJulianNumber:jnRuleLast];
544 until = [until hour: [firStart hourOfDay]
545 minute:[firStart minuteOfHour]
546 second:[firStart secondOfMinute]];
549 return [super lastInstanceStartDate];
552 @end /* iCalWeeklyRecurrenceCalculator */
554 @implementation iCalMonthlyRecurrenceCalculator
556 - (NSArray *)recurrenceRangesWithinCalendarDateRange:(NGCalendarDateRange *)_r {
557 NSMutableArray *ranges;
558 NSCalendarDate *firStart, *rStart, *rEnd, *until;
559 unsigned i, count, interval;
562 firStart = [self->firstRange startDate];
563 rStart = [_r startDate];
565 interval = [self->rrule repeatInterval];
566 until = [self lastInstanceStartDate];
569 if ([until compare:rStart] == NSOrderedAscending)
571 if ([until compare:rEnd] == NSOrderedDescending)
575 diff = [firStart monthsBetweenDate:rStart];
576 if ((diff != 0) && [rStart compare:firStart] == NSOrderedAscending)
579 count = [rStart monthsBetweenDate:rEnd] + 1;
580 ranges = [NSMutableArray arrayWithCapacity:count];
581 for (i = 0 ; i < count; i++) {
585 if ((test >= 0) && (test % interval) == 0) {
586 NSCalendarDate *start, *end;
587 NGCalendarDateRange *r;
589 start = [firStart dateByAddingYears:0
592 [start setTimeZone:[firStart timeZone]];
593 end = [start addTimeInterval:[self->firstRange duration]];
594 r = [NGCalendarDateRange calendarDateRangeWithStartDate:start
596 if ([_r containsDateRange:r])
597 [ranges addObject:r];
603 - (NSCalendarDate *)lastInstanceStartDate {
604 if ([self->rrule repeatCount] > 0) {
605 NSCalendarDate *until;
606 unsigned months, interval;
608 interval = [self->rrule repeatInterval];
609 months = [self->rrule repeatCount] * interval;
610 until = [[self->firstRange startDate] dateByAddingYears:0
615 return [super lastInstanceStartDate];
618 @end /* iCalMonthlyRecurrenceCalculator */
620 @implementation iCalYearlyRecurrenceCalculator
622 - (NSArray *)recurrenceRangesWithinCalendarDateRange:(NGCalendarDateRange *)_r {
623 NSMutableArray *ranges;
624 NSCalendarDate *firStart, *rStart, *rEnd, *until;
625 unsigned i, count, interval;
628 firStart = [self->firstRange startDate];
629 rStart = [_r startDate];
631 interval = [self->rrule repeatInterval];
632 until = [self lastInstanceStartDate];
635 if ([until compare:rStart] == NSOrderedAscending)
637 if ([until compare:rEnd] == NSOrderedDescending)
641 diff = [firStart yearsBetweenDate:rStart];
642 if ((diff != 0) && [rStart compare:firStart] == NSOrderedAscending)
645 count = [rStart yearsBetweenDate:rEnd] + 1;
646 ranges = [NSMutableArray arrayWithCapacity:count];
647 for (i = 0 ; i < count; i++) {
651 if ((test >= 0) && (test % interval) == 0) {
652 NSCalendarDate *start, *end;
653 NGCalendarDateRange *r;
655 start = [firStart dateByAddingYears:diff + i
658 [start setTimeZone:[firStart timeZone]];
659 end = [start addTimeInterval:[self->firstRange duration]];
660 r = [NGCalendarDateRange calendarDateRangeWithStartDate:start
662 if ([_r containsDateRange:r])
663 [ranges addObject:r];
669 - (NSCalendarDate *)lastInstanceStartDate {
670 if ([self->rrule repeatCount] > 0) {
671 NSCalendarDate *until;
672 unsigned years, interval;
674 interval = [self->rrule repeatInterval];
675 years = [self->rrule repeatCount] * interval;
676 until = [[self->firstRange startDate] dateByAddingYears:years
681 return [super lastInstanceStartDate];
684 @end /* iCalYearlyRecurrenceCalculator */