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 unsigned i, count, rCount;
108 ranges = [NSMutableArray array];
109 count = [_rRules count];
110 for (i = 0; i < count; i++) {
113 rule = [_rRules objectAtIndex:i];
114 if (![rule isKindOfClass:iCalRecurrenceRuleClass])
115 rule = [iCalRecurrenceRule recurrenceRuleWithICalRepresentation:rule];
117 calc = [self recurrenceCalculatorForRecurrenceRule:rule
118 withFirstInstanceCalendarDateRange:_fir];
119 rs = [calc recurrenceRangesWithinCalendarDateRange:_r];
120 [ranges addObjectsFromArray:rs];
126 /* test if any exceptions do match */
127 count = [_exRules count];
128 for (i = 0; i < count; i++) {
131 rule = [_exRules objectAtIndex:i];
132 if (![rule isKindOfClass:iCalRecurrenceRuleClass])
133 rule = [iCalRecurrenceRule recurrenceRuleWithICalRepresentation:rule];
135 calc = [self recurrenceCalculatorForRecurrenceRule:rule
136 withFirstInstanceCalendarDateRange:_fir];
137 rs = [calc recurrenceRangesWithinCalendarDateRange:_r];
138 [ranges removeObjectsInArray:rs];
144 /* exception dates are also possible */
145 rCount = [ranges count];
146 count = [_exDates count];
147 for (i = 0; i < count; i++) {
149 NGCalendarDateRange *r;
152 exDate = [_exDates objectAtIndex:i];
153 if (![exDate isKindOfClass:NSCalendarDateClass]) {
154 exDate = [NSCalendarDate calendarDateWithICalRepresentation:exDate];
156 for (k = 0; k < rCount; k++) {
157 r = [ranges objectAtIndex:(rCount - k) - 1];
158 if ([r containsDate:exDate]) {
159 [ranges removeObjectAtIndex:k];
169 - (id)initWithRecurrenceRule:(iCalRecurrenceRule *)_rrule
170 firstInstanceCalendarDateRange:(NGCalendarDateRange *)_range
172 iCalRecurrenceFrequency freq;
173 Class calcClass = Nil;
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;
186 if (calcClass == Nil)
189 self = [[calcClass alloc] init];
190 ASSIGN(self->rrule, _rrule);
191 ASSIGN(self->firstRange, _range);
197 - (unsigned)offsetFromSundayForJulianNumber:(long)_jn {
198 return (unsigned)((int)(_jn + 1.5)) % 7;
201 - (unsigned)offsetFromSundayForWeekDay:(iCalWeekDay)_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;
217 - (unsigned)offsetFromSundayForCurrentWeekStart {
218 return [self offsetFromSundayForWeekDay:[self->rrule weekStart]];
221 - (iCalWeekDay)weekDayForJulianNumber:(long)_jn {
225 day = [self offsetFromSundayForJulianNumber:_jn];
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 */
241 - (NSArray *)recurrenceRangesWithinCalendarDateRange:(NGCalendarDateRange *)_r {
242 return nil; /* subclass responsibility */
244 - (BOOL)doesRecurrWithinCalendarDateRange:(NGCalendarDateRange *)_range {
247 ranges = [self recurrenceRangesWithinCalendarDateRange:_range];
248 return (ranges == nil || [ranges count] == 0) ? NO : YES;
251 - (NGCalendarDateRange *)firstInstanceCalendarDateRange {
252 return self->firstRange;
255 - (NGCalendarDateRange *)lastInstanceCalendarDateRange {
256 NSCalendarDate *start, *end;
258 start = [self lastInstanceStartDate];
261 end = [start addTimeInterval:[self->firstRange duration]];
262 return [NGCalendarDateRange calendarDateRangeWithStartDate:start
266 - (NSCalendarDate *)lastInstanceStartDate {
267 NSCalendarDate *until;
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.
273 if ((until = [self->rrule untilDate]) != nil)
278 @end /* iCalRecurrenceCalculator */
281 @implementation iCalDailyRecurrenceCalculator
283 - (NSArray *)recurrenceRangesWithinCalendarDateRange:(NGCalendarDateRange *)_r {
284 NSMutableArray *ranges;
285 NSCalendarDate *firStart;
286 long i, jnFirst, jnStart, jnEnd, startEndCount;
289 firStart = [self->firstRange startDate];
290 jnFirst = [firStart julianNumber];
291 jnEnd = [[_r endDate] julianNumber];
296 jnStart = [[_r startDate] julianNumber];
297 interval = [self->rrule repeatInterval];
299 /* if rule is bound, check the bounds */
300 if (![self->rrule isInfinite]) {
301 NSCalendarDate *until;
304 until = [self->rrule untilDate];
306 if ([until compare:[_r startDate]] == NSOrderedAscending)
308 jnRuleLast = [until julianNumber];
311 jnRuleLast = (interval * [self->rrule repeatCount])
313 if (jnRuleLast < jnStart)
316 /* jnStart < jnRuleLast < jnEnd ? */
317 if (jnEnd > jnRuleLast)
321 startEndCount = (jnEnd - jnStart) + 1;
322 ranges = [NSMutableArray arrayWithCapacity:startEndCount];
323 for (i = 0 ; i < startEndCount; i++) {
326 jnCurrent = jnStart + i;
327 if (jnCurrent >= jnFirst) {
330 jnTest = jnCurrent - jnFirst;
331 if ((jnTest % interval) == 0) {
332 NSCalendarDate *start, *end;
333 NGCalendarDateRange *r;
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
343 [ranges addObject:r];
350 - (NSCalendarDate *)lastInstanceStartDate {
351 if ([self->rrule repeatCount] > 0) {
352 long jnFirst, jnRuleLast;
353 NSCalendarDate *firStart, *until;
355 firStart = [self->firstRange startDate];
356 jnFirst = [firStart julianNumber];
357 jnRuleLast = ([self->rrule repeatInterval] *
358 [self->rrule repeatCount]) +
360 until = [NSCalendarDate dateForJulianNumber:jnRuleLast];
361 until = [until hour: [firStart hourOfDay]
362 minute:[firStart minuteOfHour]
363 second:[firStart secondOfMinute]];
366 return [super lastInstanceStartDate];
369 @end /* iCalDailyRecurrenceCalculator */
373 TODO: If BYDAY is specified, lastInstanceStartDate and recurrences will
374 differ significantly!
376 @implementation iCalWeeklyRecurrenceCalculator
378 - (NSArray *)recurrenceRangesWithinCalendarDateRange:(NGCalendarDateRange *)_r {
379 NSMutableArray *ranges;
380 NSCalendarDate *firStart;
381 long i, jnFirst, jnStart, jnEnd, startEndCount;
382 unsigned interval, byDayMask;
384 firStart = [self->firstRange startDate];
385 jnFirst = [firStart julianNumber];
386 jnEnd = [[_r endDate] julianNumber];
391 jnStart = [[_r startDate] julianNumber];
392 interval = [self->rrule repeatInterval];
394 /* if rule is bound, check the bounds */
395 if (![self->rrule isInfinite]) {
396 NSCalendarDate *until;
399 until = [self->rrule untilDate];
401 if ([until compare:[_r startDate]] == NSOrderedAscending)
403 jnRuleLast = [until julianNumber];
406 jnRuleLast = (interval * [self->rrule repeatCount] * 7)
408 if (jnRuleLast < jnStart)
411 /* jnStart < jnRuleLast < jnEnd ? */
412 if (jnEnd > jnRuleLast)
416 startEndCount = (jnEnd - jnStart) + 1;
417 ranges = [NSMutableArray arrayWithCapacity:startEndCount];
418 byDayMask = [self->rrule byDayMask];
420 for (i = 0 ; i < startEndCount; i++) {
423 jnCurrent = jnStart + i;
424 if (jnCurrent >= jnFirst) {
427 jnDiff = jnCurrent - jnFirst; /* difference in days */
428 if ((jnDiff % (interval * 7)) == 0) {
429 NSCalendarDate *start, *end;
430 NGCalendarDateRange *r;
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
440 [ranges addObject:r];
446 long jnFirstWeekStart, weekStartOffset;
448 /* calculate jnFirst's week start - this depends on our setting of week
450 weekStartOffset = [self offsetFromSundayForJulianNumber:jnFirst] -
451 [self offsetFromSundayForCurrentWeekStart];
453 jnFirstWeekStart = jnFirst - weekStartOffset;
455 for (i = 0 ; i < startEndCount; i++) {
458 jnCurrent = jnStart + i;
459 if (jnCurrent >= jnFirst) {
462 /* we need to calculate a difference in weeks */
463 jnDiff = (jnCurrent - jnFirstWeekStart) % 7;
464 if ((jnDiff % interval) == 0) {
465 BOOL isRecurrence = NO;
467 if (jnCurrent == jnFirst) {
473 weekDay = [self weekDayForJulianNumber:jnCurrent];
474 isRecurrence = (weekDay & [self->rrule byDayMask]) ? YES : NO;
477 NSCalendarDate *start, *end;
478 NGCalendarDateRange *r;
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
488 [ranges addObject:r];
497 - (NSCalendarDate *)lastInstanceStartDate {
498 if ([self->rrule repeatCount] > 0) {
499 long jnFirst, jnRuleLast;
500 NSCalendarDate *firStart, *until;
502 firStart = [self->firstRange startDate];
503 jnFirst = [firStart julianNumber];
504 jnRuleLast = ([self->rrule repeatInterval] *
505 [self->rrule repeatCount] * 7) +
507 until = [NSCalendarDate dateForJulianNumber:jnRuleLast];
508 until = [until hour: [firStart hourOfDay]
509 minute:[firStart minuteOfHour]
510 second:[firStart secondOfMinute]];
513 return [super lastInstanceStartDate];
516 @end /* iCalWeeklyRecurrenceCalculator */
518 @implementation iCalMonthlyRecurrenceCalculator
520 - (NSArray *)recurrenceRangesWithinCalendarDateRange:(NGCalendarDateRange *)_r {
521 NSMutableArray *ranges;
522 NSCalendarDate *firStart, *rStart, *rEnd, *until;
523 unsigned i, count, interval, diff;
525 firStart = [self->firstRange startDate];
526 rStart = [_r startDate];
528 interval = [self->rrule repeatInterval];
529 until = [self lastInstanceStartDate];
532 if ([until compare:rStart] == NSOrderedAscending)
534 if ([until compare:rEnd] == NSOrderedDescending)
538 diff = [firStart monthsBetweenDate:rStart];
539 count = [rStart monthsBetweenDate:rEnd] + 1;
540 ranges = [NSMutableArray arrayWithCapacity:count];
541 for (i = 0 ; i < count; i++) {
545 if ((test % interval) == 0) {
546 NSCalendarDate *start, *end;
547 NGCalendarDateRange *r;
549 start = [firStart dateByAddingYears:0
552 [start setTimeZone:[firStart timeZone]];
553 end = [start addTimeInterval:[self->firstRange duration]];
554 r = [NGCalendarDateRange calendarDateRangeWithStartDate:start
556 [ranges addObject:r];
562 - (NSCalendarDate *)lastInstanceStartDate {
563 if ([self->rrule repeatCount] > 0) {
564 NSCalendarDate *until;
565 unsigned months, interval;
567 interval = [self->rrule repeatInterval];
568 months = [self->rrule repeatCount] * interval;
569 until = [[self->firstRange startDate] dateByAddingYears:0
574 return [super lastInstanceStartDate];
577 @end /* iCalMonthlyRecurrenceCalculator */
579 @implementation iCalYearlyRecurrenceCalculator
581 - (NSArray *)recurrenceRangesWithinCalendarDateRange:(NGCalendarDateRange *)_r {
582 NSMutableArray *ranges;
583 NSCalendarDate *firStart, *rStart, *rEnd, *until;
584 unsigned i, count, interval, diff;
586 firStart = [self->firstRange startDate];
587 rStart = [_r startDate];
589 interval = [self->rrule repeatInterval];
590 until = [self lastInstanceStartDate];
593 if ([until compare:rStart] == NSOrderedAscending)
595 if ([until compare:rEnd] == NSOrderedDescending)
599 diff = [firStart yearsBetweenDate:rStart];
600 count = [rStart yearsBetweenDate:rEnd] + 1;
601 ranges = [NSMutableArray arrayWithCapacity:count];
602 for (i = 0 ; i < count; i++) {
606 if ((test % interval) == 0) {
607 NSCalendarDate *start, *end;
608 NGCalendarDateRange *r;
610 start = [firStart dateByAddingYears:diff + i
613 [start setTimeZone:[firStart timeZone]];
614 end = [start addTimeInterval:[self->firstRange duration]];
615 r = [NGCalendarDateRange calendarDateRangeWithStartDate:start
617 [ranges addObject:r];
623 - (NSCalendarDate *)lastInstanceStartDate {
624 if ([self->rrule repeatCount] > 0) {
625 NSCalendarDate *until;
626 unsigned years, interval;
628 interval = [self->rrule repeatInterval];
629 years = [self->rrule repeatCount] * interval;
630 until = [[self->firstRange startDate] dateByAddingYears:years
635 return [super lastInstanceStartDate];
638 @end /* iCalYearlyRecurrenceCalculator */