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);
222 - (unsigned)offsetFromSundayForJulianNumber:(long)_jn {
223 return (unsigned)((int)(_jn + 1.5)) % 7;
226 - (unsigned)offsetFromSundayForWeekDay:(iCalWeekDay)_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;
242 - (unsigned)offsetFromSundayForCurrentWeekStart {
243 return [self offsetFromSundayForWeekDay:[self->rrule weekStart]];
246 - (iCalWeekDay)weekDayForJulianNumber:(long)_jn {
250 day = [self offsetFromSundayForJulianNumber:_jn];
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 */
266 - (NSArray *)recurrenceRangesWithinCalendarDateRange:(NGCalendarDateRange *)_r {
267 return nil; /* subclass responsibility */
269 - (BOOL)doesRecurrWithinCalendarDateRange:(NGCalendarDateRange *)_range {
272 ranges = [self recurrenceRangesWithinCalendarDateRange:_range];
273 return (ranges == nil || [ranges count] == 0) ? NO : YES;
276 - (NGCalendarDateRange *)firstInstanceCalendarDateRange {
277 return self->firstRange;
280 - (NGCalendarDateRange *)lastInstanceCalendarDateRange {
281 NSCalendarDate *start, *end;
283 start = [self lastInstanceStartDate];
286 end = [start addTimeInterval:[self->firstRange duration]];
287 return [NGCalendarDateRange calendarDateRangeWithStartDate:start
291 - (NSCalendarDate *)lastInstanceStartDate {
292 NSCalendarDate *until;
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.
298 if ((until = [self->rrule untilDate]) != nil)
303 @end /* iCalRecurrenceCalculator */
306 @implementation iCalDailyRecurrenceCalculator
308 - (NSArray *)recurrenceRangesWithinCalendarDateRange:(NGCalendarDateRange *)_r {
309 NSMutableArray *ranges;
310 NSCalendarDate *firStart;
311 long i, jnFirst, jnStart, jnEnd, startEndCount;
314 firStart = [self->firstRange startDate];
315 jnFirst = [firStart julianNumber];
316 jnEnd = [[_r endDate] julianNumber];
321 jnStart = [[_r startDate] julianNumber];
322 interval = [self->rrule repeatInterval];
324 /* if rule is bound, check the bounds */
325 if (![self->rrule isInfinite]) {
326 NSCalendarDate *until;
329 until = [self->rrule untilDate];
331 if ([until compare:[_r startDate]] == NSOrderedAscending)
333 jnRuleLast = [until julianNumber];
336 jnRuleLast = (interval * [self->rrule repeatCount])
338 if (jnRuleLast < jnStart)
341 /* jnStart < jnRuleLast < jnEnd ? */
342 if (jnEnd > jnRuleLast)
346 startEndCount = (jnEnd - jnStart) + 1;
347 ranges = [NSMutableArray arrayWithCapacity:startEndCount];
348 for (i = 0 ; i < startEndCount; i++) {
351 jnCurrent = jnStart + i;
352 if (jnCurrent >= jnFirst) {
355 jnTest = jnCurrent - jnFirst;
356 if ((jnTest % interval) == 0) {
357 NSCalendarDate *start, *end;
358 NGCalendarDateRange *r;
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
368 if ([_r containsDateRange:r])
369 [ranges addObject:r];
376 - (NSCalendarDate *)lastInstanceStartDate {
377 if ([self->rrule repeatCount] > 0) {
378 long jnFirst, jnRuleLast;
379 NSCalendarDate *firStart, *until;
381 firStart = [self->firstRange startDate];
382 jnFirst = [firStart julianNumber];
383 jnRuleLast = ([self->rrule repeatInterval] *
384 [self->rrule repeatCount]) +
386 until = [NSCalendarDate dateForJulianNumber:jnRuleLast];
387 until = [until hour: [firStart hourOfDay]
388 minute:[firStart minuteOfHour]
389 second:[firStart secondOfMinute]];
392 return [super lastInstanceStartDate];
395 @end /* iCalDailyRecurrenceCalculator */
399 TODO: If BYDAY is specified, lastInstanceStartDate and recurrences will
400 differ significantly!
402 @implementation iCalWeeklyRecurrenceCalculator
404 - (NSArray *)recurrenceRangesWithinCalendarDateRange:(NGCalendarDateRange *)_r {
405 NSMutableArray *ranges;
406 NSCalendarDate *firStart;
407 long i, jnFirst, jnStart, jnEnd, startEndCount;
408 unsigned interval, byDayMask;
410 firStart = [self->firstRange startDate];
411 jnFirst = [firStart julianNumber];
412 jnEnd = [[_r endDate] julianNumber];
417 jnStart = [[_r startDate] julianNumber];
418 interval = [self->rrule repeatInterval];
420 /* if rule is bound, check the bounds */
421 if (![self->rrule isInfinite]) {
422 NSCalendarDate *until;
425 until = [self->rrule untilDate];
427 if ([until compare:[_r startDate]] == NSOrderedAscending)
429 jnRuleLast = [until julianNumber];
432 jnRuleLast = (interval * [self->rrule repeatCount] * 7)
434 if (jnRuleLast < jnStart)
437 /* jnStart < jnRuleLast < jnEnd ? */
438 if (jnEnd > jnRuleLast)
442 startEndCount = (jnEnd - jnStart) + 1;
443 ranges = [NSMutableArray arrayWithCapacity:startEndCount];
444 byDayMask = [self->rrule byDayMask];
446 for (i = 0 ; i < startEndCount; i++) {
449 jnCurrent = jnStart + i;
450 if (jnCurrent >= jnFirst) {
453 jnDiff = jnCurrent - jnFirst; /* difference in days */
454 if ((jnDiff % (interval * 7)) == 0) {
455 NSCalendarDate *start, *end;
456 NGCalendarDateRange *r;
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
466 if ([_r containsDateRange:r])
467 [ranges addObject:r];
473 long jnFirstWeekStart, weekStartOffset;
475 /* calculate jnFirst's week start - this depends on our setting of week
477 weekStartOffset = [self offsetFromSundayForJulianNumber:jnFirst] -
478 [self offsetFromSundayForCurrentWeekStart];
480 jnFirstWeekStart = jnFirst - weekStartOffset;
482 for (i = 0 ; i < startEndCount; i++) {
485 jnCurrent = jnStart + i;
486 if (jnCurrent >= jnFirst) {
489 /* we need to calculate a difference in weeks */
490 jnDiff = (jnCurrent - jnFirstWeekStart) % 7;
491 if ((jnDiff % interval) == 0) {
492 BOOL isRecurrence = NO;
494 if (jnCurrent == jnFirst) {
500 weekDay = [self weekDayForJulianNumber:jnCurrent];
501 isRecurrence = (weekDay & [self->rrule byDayMask]) ? YES : NO;
504 NSCalendarDate *start, *end;
505 NGCalendarDateRange *r;
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
515 if ([_r containsDateRange:r])
516 [ranges addObject:r];
525 - (NSCalendarDate *)lastInstanceStartDate {
526 if ([self->rrule repeatCount] > 0) {
527 long jnFirst, jnRuleLast;
528 NSCalendarDate *firStart, *until;
530 firStart = [self->firstRange startDate];
531 jnFirst = [firStart julianNumber];
532 jnRuleLast = ([self->rrule repeatInterval] *
533 [self->rrule repeatCount] * 7) +
535 until = [NSCalendarDate dateForJulianNumber:jnRuleLast];
536 until = [until hour: [firStart hourOfDay]
537 minute:[firStart minuteOfHour]
538 second:[firStart secondOfMinute]];
541 return [super lastInstanceStartDate];
544 @end /* iCalWeeklyRecurrenceCalculator */
546 @implementation iCalMonthlyRecurrenceCalculator
548 - (NSArray *)recurrenceRangesWithinCalendarDateRange:(NGCalendarDateRange *)_r {
549 NSMutableArray *ranges;
550 NSCalendarDate *firStart, *rStart, *rEnd, *until;
551 unsigned i, count, interval, diff;
553 firStart = [self->firstRange startDate];
554 rStart = [_r startDate];
556 interval = [self->rrule repeatInterval];
557 until = [self lastInstanceStartDate];
560 if ([until compare:rStart] == NSOrderedAscending)
562 if ([until compare:rEnd] == NSOrderedDescending)
566 diff = [firStart monthsBetweenDate:rStart];
567 count = [rStart monthsBetweenDate:rEnd] + 1;
568 ranges = [NSMutableArray arrayWithCapacity:count];
569 for (i = 0 ; i < count; i++) {
573 if ((test % interval) == 0) {
574 NSCalendarDate *start, *end;
575 NGCalendarDateRange *r;
577 start = [firStart dateByAddingYears:0
580 [start setTimeZone:[firStart timeZone]];
581 end = [start addTimeInterval:[self->firstRange duration]];
582 r = [NGCalendarDateRange calendarDateRangeWithStartDate:start
584 if ([_r containsDateRange:r])
585 [ranges addObject:r];
591 - (NSCalendarDate *)lastInstanceStartDate {
592 if ([self->rrule repeatCount] > 0) {
593 NSCalendarDate *until;
594 unsigned months, interval;
596 interval = [self->rrule repeatInterval];
597 months = [self->rrule repeatCount] * interval;
598 until = [[self->firstRange startDate] dateByAddingYears:0
603 return [super lastInstanceStartDate];
606 @end /* iCalMonthlyRecurrenceCalculator */
608 @implementation iCalYearlyRecurrenceCalculator
610 - (NSArray *)recurrenceRangesWithinCalendarDateRange:(NGCalendarDateRange *)_r {
611 NSMutableArray *ranges;
612 NSCalendarDate *firStart, *rStart, *rEnd, *until;
613 unsigned i, count, interval, diff;
615 firStart = [self->firstRange startDate];
616 rStart = [_r startDate];
618 interval = [self->rrule repeatInterval];
619 until = [self lastInstanceStartDate];
622 if ([until compare:rStart] == NSOrderedAscending)
624 if ([until compare:rEnd] == NSOrderedDescending)
628 diff = [firStart yearsBetweenDate:rStart];
629 count = [rStart yearsBetweenDate:rEnd] + 1;
630 ranges = [NSMutableArray arrayWithCapacity:count];
631 for (i = 0 ; i < count; i++) {
635 if ((test % interval) == 0) {
636 NSCalendarDate *start, *end;
637 NGCalendarDateRange *r;
639 start = [firStart dateByAddingYears:diff + i
642 [start setTimeZone:[firStart timeZone]];
643 end = [start addTimeInterval:[self->firstRange duration]];
644 r = [NGCalendarDateRange calendarDateRangeWithStartDate:start
646 if ([_r containsDateRange:r])
647 [ranges addObject:r];
653 - (NSCalendarDate *)lastInstanceStartDate {
654 if ([self->rrule repeatCount] > 0) {
655 NSCalendarDate *until;
656 unsigned years, interval;
658 interval = [self->rrule repeatInterval];
659 years = [self->rrule repeatCount] * interval;
660 until = [[self->firstRange startDate] dateByAddingYears:years
665 return [super lastInstanceStartDate];
668 @end /* iCalYearlyRecurrenceCalculator */