/* Copyright (C) 2004-2005 SKYRIX Software AG This file is part of SOPE. SOPE is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2, or (at your option) any later version. SOPE is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with SOPE; see the file COPYING. If not, write to the Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ #include "iCalRecurrenceCalculator.h" #include #include "iCalRecurrenceRule.h" #include "NSCalendarDate+ICal.h" #include "common.h" /* class cluster */ @interface iCalDailyRecurrenceCalculator : iCalRecurrenceCalculator { } @end @interface iCalWeeklyRecurrenceCalculator : iCalRecurrenceCalculator { } @end @interface iCalMonthlyRecurrenceCalculator : iCalRecurrenceCalculator { } @end @interface iCalYearlyRecurrenceCalculator : iCalRecurrenceCalculator { } @end /* Private */ @interface iCalRecurrenceCalculator (PrivateAPI) - (NSCalendarDate *)lastInstanceStartDate; - (unsigned)offsetFromSundayForJulianNumber:(long)_jn; - (unsigned)offsetFromSundayForWeekDay:(iCalWeekDay)_weekDay; - (unsigned)offsetFromSundayForCurrentWeekStart; - (iCalWeekDay)weekDayForJulianNumber:(long)_jn; @end @implementation iCalRecurrenceCalculator static Class NSCalendarDateClass = Nil; static Class iCalRecurrenceRuleClass = Nil; static Class dailyCalcClass = Nil; static Class weeklyCalcClass = Nil; static Class monthlyCalcClass = Nil; static Class yearlyCalcClass = Nil; + (void)initialize { static BOOL didInit = NO; if (didInit) return; didInit = YES; NSCalendarDateClass = [NSCalendarDate class]; iCalRecurrenceRuleClass = [iCalRecurrenceRule class]; dailyCalcClass = [iCalDailyRecurrenceCalculator class]; weeklyCalcClass = [iCalWeeklyRecurrenceCalculator class]; monthlyCalcClass = [iCalMonthlyRecurrenceCalculator class]; yearlyCalcClass = [iCalYearlyRecurrenceCalculator class]; } /* factory */ + (id)recurrenceCalculatorForRecurrenceRule:(iCalRecurrenceRule *)_rrule withFirstInstanceCalendarDateRange:(NGCalendarDateRange *)_range { return [[[self alloc] initWithRecurrenceRule:_rrule firstInstanceCalendarDateRange:_range] autorelease]; } /* complex calculation convenience */ + (NSArray *)recurrenceRangesWithinCalendarDateRange:(NGCalendarDateRange *)_r firstInstanceCalendarDateRange:(NGCalendarDateRange *)_fir recurrenceRules:(NSArray *)_rRules exceptionRules:(NSArray *)_exRules exceptionDates:(NSArray *)_exDates { id rule; iCalRecurrenceCalculator *calc; NSMutableArray *ranges; NSMutableArray *exDates; unsigned i, count, rCount; ranges = [NSMutableArray array]; count = [_rRules count]; for (i = 0; i < count; i++) { NSArray *rs; rule = [_rRules objectAtIndex:i]; if (![rule isKindOfClass:iCalRecurrenceRuleClass]) rule = [iCalRecurrenceRule recurrenceRuleWithICalRepresentation:rule]; calc = [self recurrenceCalculatorForRecurrenceRule:rule withFirstInstanceCalendarDateRange:_fir]; rs = [calc recurrenceRangesWithinCalendarDateRange:_r]; [ranges addObjectsFromArray:rs]; } if (![ranges count]) return nil; /* test if any exceptions do match */ count = [_exRules count]; for (i = 0; i < count; i++) { NSArray *rs; rule = [_exRules objectAtIndex:i]; if (![rule isKindOfClass:iCalRecurrenceRuleClass]) rule = [iCalRecurrenceRule recurrenceRuleWithICalRepresentation:rule]; calc = [self recurrenceCalculatorForRecurrenceRule:rule withFirstInstanceCalendarDateRange:_fir]; rs = [calc recurrenceRangesWithinCalendarDateRange:_r]; [ranges removeObjectsInArray:rs]; } if (![ranges count]) return nil; /* exception dates */ count = [_exDates count]; if (!count) return ranges; /* sort out exDates not within range */ exDates = [NSMutableArray arrayWithCapacity:count]; for (i = 0; i < count; i++) { id exDate; exDate = [_exDates objectAtIndex:i]; if (![exDate isKindOfClass:NSCalendarDateClass]) { exDate = [NSCalendarDate calendarDateWithICalRepresentation:exDate]; } if ([_r containsDate:exDate]) [exDates addObject:exDate]; } /* remove matching exDates from ranges */ count = [exDates count]; if (!count) return ranges; rCount = [ranges count]; for (i = 0; i < count; i++) { NSCalendarDate *exDate; NGCalendarDateRange *r; unsigned k; exDate = [exDates objectAtIndex:i]; for (k = 0; k < rCount; k++) { unsigned rIdx; rIdx = (rCount - k) - 1; r = [ranges objectAtIndex:rIdx]; if ([r containsDate:exDate]) { [ranges removeObjectAtIndex:rIdx]; rCount--; break; /* this is safe because we know that ranges don't overlap */ } } } return ranges; } /* init */ - (id)initWithRecurrenceRule:(iCalRecurrenceRule *)_rrule firstInstanceCalendarDateRange:(NGCalendarDateRange *)_range { iCalRecurrenceFrequency freq; Class calcClass = Nil; freq = [_rrule frequency]; if (freq == iCalRecurrenceFrequenceDaily) calcClass = dailyCalcClass; else if (freq == iCalRecurrenceFrequenceWeekly) calcClass = weeklyCalcClass; else if (freq == iCalRecurrenceFrequenceMonthly) calcClass = monthlyCalcClass; else if (freq == iCalRecurrenceFrequenceYearly) calcClass = yearlyCalcClass; [self autorelease]; if (calcClass == Nil) return nil; self = [[calcClass alloc] init]; ASSIGN(self->rrule, _rrule); ASSIGN(self->firstRange, _range); return self; } /* dealloc */ - (void)dealloc { [self->firstRange release]; [self->rrule release]; [super dealloc]; } /* helpers */ - (unsigned)offsetFromSundayForJulianNumber:(long)_jn { return (unsigned)((int)(_jn + 1.5)) % 7; } - (unsigned)offsetFromSundayForWeekDay:(iCalWeekDay)_weekDay { unsigned offset; switch (_weekDay) { case iCalWeekDaySunday: offset = 0; break; case iCalWeekDayMonday: offset = 1; break; case iCalWeekDayTuesday: offset = 2; break; case iCalWeekDayWednesday: offset = 3; break; case iCalWeekDayThursday: offset = 4; break; case iCalWeekDayFriday: offset = 5; break; case iCalWeekDaySaturday: offset = 6; break; default: offset = 0; break; } return offset; } - (unsigned)offsetFromSundayForCurrentWeekStart { return [self offsetFromSundayForWeekDay:[self->rrule weekStart]]; } - (iCalWeekDay)weekDayForJulianNumber:(long)_jn { unsigned day; iCalWeekDay weekDay; day = [self offsetFromSundayForJulianNumber:_jn]; switch (day) { case 0: weekDay = iCalWeekDaySunday; break; case 1: weekDay = iCalWeekDayMonday; break; case 2: weekDay = iCalWeekDayTuesday; break; case 3: weekDay = iCalWeekDayWednesday; break; case 4: weekDay = iCalWeekDayThursday; break; case 5: weekDay = iCalWeekDayFriday; break; case 6: weekDay = iCalWeekDaySaturday; break; default: weekDay = iCalWeekDaySunday; break; /* keep compiler happy */ } return weekDay; } /* calculation */ - (NSArray *)recurrenceRangesWithinCalendarDateRange:(NGCalendarDateRange *)_r { return nil; /* subclass responsibility */ } - (BOOL)doesRecurrWithinCalendarDateRange:(NGCalendarDateRange *)_range { NSArray *ranges; ranges = [self recurrenceRangesWithinCalendarDateRange:_range]; return (ranges == nil || [ranges count] == 0) ? NO : YES; } - (NGCalendarDateRange *)firstInstanceCalendarDateRange { return self->firstRange; } - (NGCalendarDateRange *)lastInstanceCalendarDateRange { NSCalendarDate *start, *end; start = [self lastInstanceStartDate]; if (!start) return nil; end = [start addTimeInterval:[self->firstRange duration]]; return [NGCalendarDateRange calendarDateRangeWithStartDate:start endDate:end]; } - (NSCalendarDate *)lastInstanceStartDate { NSCalendarDate *until; /* NOTE: this is horribly inaccurate and doesn't even consider the use of repeatCount. It MUST be implemented by subclasses properly! However, it does the trick for SOGO 1.0 - that's why it's left here. */ if ((until = [self->rrule untilDate]) != nil) return until; return nil; } @end /* iCalRecurrenceCalculator */ @implementation iCalDailyRecurrenceCalculator - (NSArray *)recurrenceRangesWithinCalendarDateRange:(NGCalendarDateRange *)_r { NSMutableArray *ranges; NSCalendarDate *firStart; long i, jnFirst, jnStart, jnEnd, startEndCount; unsigned interval; firStart = [self->firstRange startDate]; jnFirst = [firStart julianNumber]; jnEnd = [[_r endDate] julianNumber]; if (jnFirst > jnEnd) return nil; jnStart = [[_r startDate] julianNumber]; interval = [self->rrule repeatInterval]; /* if rule is bound, check the bounds */ if (![self->rrule isInfinite]) { NSCalendarDate *until; long jnRuleLast; until = [self->rrule untilDate]; if (until) { if ([until compare:[_r startDate]] == NSOrderedAscending) return nil; jnRuleLast = [until julianNumber]; } else { jnRuleLast = (interval * [self->rrule repeatCount]) + jnFirst; if (jnRuleLast < jnStart) return nil; } /* jnStart < jnRuleLast < jnEnd ? */ if (jnEnd > jnRuleLast) jnEnd = jnRuleLast; } startEndCount = (jnEnd - jnStart) + 1; ranges = [NSMutableArray arrayWithCapacity:startEndCount]; for (i = 0 ; i < startEndCount; i++) { long jnCurrent; jnCurrent = jnStart + i; if (jnCurrent >= jnFirst) { long jnTest; jnTest = jnCurrent - jnFirst; if ((jnTest % interval) == 0) { NSCalendarDate *start, *end; NGCalendarDateRange *r; start = [NSCalendarDate dateForJulianNumber:jnCurrent]; [start setTimeZone:[firStart timeZone]]; start = [start hour: [firStart hourOfDay] minute:[firStart minuteOfHour] second:[firStart secondOfMinute]]; end = [start addTimeInterval:[self->firstRange duration]]; r = [NGCalendarDateRange calendarDateRangeWithStartDate:start endDate:end]; if ([_r containsDateRange:r]) [ranges addObject:r]; } } } return ranges; } - (NSCalendarDate *)lastInstanceStartDate { if ([self->rrule repeatCount] > 0) { long jnFirst, jnRuleLast; NSCalendarDate *firStart, *until; firStart = [self->firstRange startDate]; jnFirst = [firStart julianNumber]; jnRuleLast = ([self->rrule repeatInterval] * [self->rrule repeatCount]) + jnFirst; until = [NSCalendarDate dateForJulianNumber:jnRuleLast]; until = [until hour: [firStart hourOfDay] minute:[firStart minuteOfHour] second:[firStart secondOfMinute]]; return until; } return [super lastInstanceStartDate]; } @end /* iCalDailyRecurrenceCalculator */ /* TODO: If BYDAY is specified, lastInstanceStartDate and recurrences will differ significantly! */ @implementation iCalWeeklyRecurrenceCalculator - (NSArray *)recurrenceRangesWithinCalendarDateRange:(NGCalendarDateRange *)_r { NSMutableArray *ranges; NSCalendarDate *firStart; long i, jnFirst, jnStart, jnEnd, startEndCount; unsigned interval, byDayMask; firStart = [self->firstRange startDate]; jnFirst = [firStart julianNumber]; jnEnd = [[_r endDate] julianNumber]; if (jnFirst > jnEnd) return nil; jnStart = [[_r startDate] julianNumber]; interval = [self->rrule repeatInterval]; /* if rule is bound, check the bounds */ if (![self->rrule isInfinite]) { NSCalendarDate *until; long jnRuleLast; until = [self->rrule untilDate]; if (until) { if ([until compare:[_r startDate]] == NSOrderedAscending) return nil; jnRuleLast = [until julianNumber]; } else { jnRuleLast = (interval * [self->rrule repeatCount] * 7) + jnFirst; if (jnRuleLast < jnStart) return nil; } /* jnStart < jnRuleLast < jnEnd ? */ if (jnEnd > jnRuleLast) jnEnd = jnRuleLast; } startEndCount = (jnEnd - jnStart) + 1; ranges = [NSMutableArray arrayWithCapacity:startEndCount]; byDayMask = [self->rrule byDayMask]; if (!byDayMask) { for (i = 0 ; i < startEndCount; i++) { long jnCurrent; jnCurrent = jnStart + i; if (jnCurrent >= jnFirst) { long jnDiff; jnDiff = jnCurrent - jnFirst; /* difference in days */ if ((jnDiff % (interval * 7)) == 0) { NSCalendarDate *start, *end; NGCalendarDateRange *r; start = [NSCalendarDate dateForJulianNumber:jnCurrent]; [start setTimeZone:[firStart timeZone]]; start = [start hour: [firStart hourOfDay] minute:[firStart minuteOfHour] second:[firStart secondOfMinute]]; end = [start addTimeInterval:[self->firstRange duration]]; r = [NGCalendarDateRange calendarDateRangeWithStartDate:start endDate:end]; if ([_r containsDateRange:r]) [ranges addObject:r]; } } } } else { long jnFirstWeekStart, weekStartOffset; /* calculate jnFirst's week start - this depends on our setting of week start */ weekStartOffset = [self offsetFromSundayForJulianNumber:jnFirst] - [self offsetFromSundayForCurrentWeekStart]; jnFirstWeekStart = jnFirst - weekStartOffset; for (i = 0 ; i < startEndCount; i++) { long jnCurrent; jnCurrent = jnStart + i; if (jnCurrent >= jnFirst) { long jnDiff; /* we need to calculate a difference in weeks */ jnDiff = (jnCurrent - jnFirstWeekStart) % 7; if ((jnDiff % interval) == 0) { BOOL isRecurrence = NO; if (jnCurrent == jnFirst) { isRecurrence = YES; } else { iCalWeekDay weekDay; weekDay = [self weekDayForJulianNumber:jnCurrent]; isRecurrence = (weekDay & [self->rrule byDayMask]) ? YES : NO; } if (isRecurrence) { NSCalendarDate *start, *end; NGCalendarDateRange *r; start = [NSCalendarDate dateForJulianNumber:jnCurrent]; [start setTimeZone:[firStart timeZone]]; start = [start hour: [firStart hourOfDay] minute:[firStart minuteOfHour] second:[firStart secondOfMinute]]; end = [start addTimeInterval:[self->firstRange duration]]; r = [NGCalendarDateRange calendarDateRangeWithStartDate:start endDate:end]; if ([_r containsDateRange:r]) [ranges addObject:r]; } } } } } return ranges; } - (NSCalendarDate *)lastInstanceStartDate { if ([self->rrule repeatCount] > 0) { long jnFirst, jnRuleLast; NSCalendarDate *firStart, *until; firStart = [self->firstRange startDate]; jnFirst = [firStart julianNumber]; jnRuleLast = ([self->rrule repeatInterval] * [self->rrule repeatCount] * 7) + jnFirst; until = [NSCalendarDate dateForJulianNumber:jnRuleLast]; until = [until hour: [firStart hourOfDay] minute:[firStart minuteOfHour] second:[firStart secondOfMinute]]; return until; } return [super lastInstanceStartDate]; } @end /* iCalWeeklyRecurrenceCalculator */ @implementation iCalMonthlyRecurrenceCalculator - (NSArray *)recurrenceRangesWithinCalendarDateRange:(NGCalendarDateRange *)_r { NSMutableArray *ranges; NSCalendarDate *firStart, *rStart, *rEnd, *until; unsigned i, count, interval; int diff; firStart = [self->firstRange startDate]; rStart = [_r startDate]; rEnd = [_r endDate]; interval = [self->rrule repeatInterval]; until = [self lastInstanceStartDate]; if (until) { if ([until compare:rStart] == NSOrderedAscending) return nil; if ([until compare:rEnd] == NSOrderedDescending) rEnd = until; } diff = [firStart monthsBetweenDate:rStart]; if ((diff != 0) && [rStart compare:firStart] == NSOrderedAscending) diff = -diff; count = [rStart monthsBetweenDate:rEnd] + 1; ranges = [NSMutableArray arrayWithCapacity:count]; for (i = 0 ; i < count; i++) { int test; test = diff + i; if ((test >= 0) && (test % interval) == 0) { NSCalendarDate *start, *end; NGCalendarDateRange *r; start = [firStart dateByAddingYears:0 months:diff + i days:0]; [start setTimeZone:[firStart timeZone]]; end = [start addTimeInterval:[self->firstRange duration]]; r = [NGCalendarDateRange calendarDateRangeWithStartDate:start endDate:end]; if ([_r containsDateRange:r]) [ranges addObject:r]; } } return ranges; } - (NSCalendarDate *)lastInstanceStartDate { if ([self->rrule repeatCount] > 0) { NSCalendarDate *until; unsigned months, interval; interval = [self->rrule repeatInterval]; months = [self->rrule repeatCount] * interval; until = [[self->firstRange startDate] dateByAddingYears:0 months:months days:0]; return until; } return [super lastInstanceStartDate]; } @end /* iCalMonthlyRecurrenceCalculator */ @implementation iCalYearlyRecurrenceCalculator - (NSArray *)recurrenceRangesWithinCalendarDateRange:(NGCalendarDateRange *)_r { NSMutableArray *ranges; NSCalendarDate *firStart, *rStart, *rEnd, *until; unsigned i, count, interval; int diff; firStart = [self->firstRange startDate]; rStart = [_r startDate]; rEnd = [_r endDate]; interval = [self->rrule repeatInterval]; until = [self lastInstanceStartDate]; if (until) { if ([until compare:rStart] == NSOrderedAscending) return nil; if ([until compare:rEnd] == NSOrderedDescending) rEnd = until; } diff = [firStart yearsBetweenDate:rStart]; if ((diff != 0) && [rStart compare:firStart] == NSOrderedAscending) diff = -diff; count = [rStart yearsBetweenDate:rEnd] + 1; ranges = [NSMutableArray arrayWithCapacity:count]; for (i = 0 ; i < count; i++) { int test; test = diff + i; if ((test >= 0) && (test % interval) == 0) { NSCalendarDate *start, *end; NGCalendarDateRange *r; start = [firStart dateByAddingYears:diff + i months:0 days:0]; [start setTimeZone:[firStart timeZone]]; end = [start addTimeInterval:[self->firstRange duration]]; r = [NGCalendarDateRange calendarDateRangeWithStartDate:start endDate:end]; if ([_r containsDateRange:r]) [ranges addObject:r]; } } return ranges; } - (NSCalendarDate *)lastInstanceStartDate { if ([self->rrule repeatCount] > 0) { NSCalendarDate *until; unsigned years, interval; interval = [self->rrule repeatInterval]; years = [self->rrule repeatCount] * interval; until = [[self->firstRange startDate] dateByAddingYears:years months:0 days:0]; return until; } return [super lastInstanceStartDate]; } @end /* iCalYearlyRecurrenceCalculator */