From 12afa2859aa4930f5d557417e58efc697d32cbca Mon Sep 17 00:00:00 2001 From: znek Date: Sat, 12 Feb 2005 22:03:18 +0000 Subject: [PATCH] recurrence work in progress git-svn-id: http://svn.opengroupware.org/SOPE/trunk@554 e4a50df8-12e2-0310-a44c-efbce7f8a7e3 --- sope-ical/NGiCal/ChangeLog | 20 ++ sope-ical/NGiCal/NGiCal.xcode/project.pbxproj | 4 +- sope-ical/NGiCal/NSCalendarDate+ICal.h | 6 + sope-ical/NGiCal/NSCalendarDate+ICal.m | 45 +++ sope-ical/NGiCal/Version | 3 +- sope-ical/NGiCal/iCalEvent.h | 1 + sope-ical/NGiCal/iCalEvent.m | 11 + sope-ical/NGiCal/iCalRecurrenceCalculator.h | 3 + sope-ical/NGiCal/iCalRecurrenceCalculator.m | 257 ++++++++++++++---- sope-ical/NGiCal/iCalRepeatableEntityObject.h | 3 + sope-ical/NGiCal/iCalRepeatableEntityObject.m | 28 ++ .../tests/iCalRecurrenceCalculatorTests.m | 32 ++- 12 files changed, 354 insertions(+), 59 deletions(-) diff --git a/sope-ical/NGiCal/ChangeLog b/sope-ical/NGiCal/ChangeLog index f79d637b..7799a49c 100644 --- a/sope-ical/NGiCal/ChangeLog +++ b/sope-ical/NGiCal/ChangeLog @@ -1,3 +1,23 @@ +2005-02-12 Marcus Mueller + + * v4.5.40 + + * iCalRecurrenceCalculator.[hm]: implemented all required (and simple) + calculations. Added some convenience API to query some of the more + obvious ranges suitable as limits for fetching/comparison. + + * iCalRepeatableEntityObject.[hm]: new method for calculating the + last possible recurrence start date. This can be used for fetches + as well. + + * iCalEvent.[hm]: more convenient wrapper for the new method found in + iCalRepeatableEntityObject. + + * NSCalendarDate+ICal.[hm]: convenience methods for calculating + "distances" between dates. + + * tests/*: updated + 2005-02-11 Marcus Mueller * v4.5.39 diff --git a/sope-ical/NGiCal/NGiCal.xcode/project.pbxproj b/sope-ical/NGiCal/NGiCal.xcode/project.pbxproj index ec2b5a23..33a59f72 100644 --- a/sope-ical/NGiCal/NGiCal.xcode/project.pbxproj +++ b/sope-ical/NGiCal/NGiCal.xcode/project.pbxproj @@ -164,7 +164,7 @@ ); buildSettings = { DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 4.5.39; + DYLIB_CURRENT_VERSION = 4.5.40; FRAMEWORK_SEARCH_PATHS = "\"$(USER_LIBRARY_DIR)/EmbeddedFrameworks\""; FRAMEWORK_VERSION = A; GCC_PRECOMPILE_PREFIX_HEADER = NO; @@ -578,7 +578,7 @@ ); buildSettings = { DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 4.5.39; + DYLIB_CURRENT_VERSION = 4.5.40; FRAMEWORK_SEARCH_PATHS = "$(LOCAL_LIBRARY_DIR)/Frameworks"; FRAMEWORK_VERSION = A; GCC_PRECOMPILE_PREFIX_HEADER = YES; diff --git a/sope-ical/NGiCal/NSCalendarDate+ICal.h b/sope-ical/NGiCal/NSCalendarDate+ICal.h index ea594978..2af61b3f 100644 --- a/sope-ical/NGiCal/NSCalendarDate+ICal.h +++ b/sope-ical/NGiCal/NSCalendarDate+ICal.h @@ -35,4 +35,10 @@ @end +@interface NSCalendarDate (iCalRecurrenceCalculatorExtensions) +- (unsigned)yearsBetweenDate:(NSCalendarDate *)_date; +- (unsigned)monthsBetweenDate:(NSCalendarDate *)_date; +- (unsigned)daysBetweenDate:(NSCalendarDate *)_date; +@end + #endif /* __ICal2_NSCalendarDate_ICal_H__ */ diff --git a/sope-ical/NGiCal/NSCalendarDate+ICal.m b/sope-ical/NGiCal/NSCalendarDate+ICal.m index 777dbb6d..7a51986c 100644 --- a/sope-ical/NGiCal/NSCalendarDate+ICal.m +++ b/sope-ical/NGiCal/NSCalendarDate+ICal.m @@ -82,3 +82,48 @@ static NSString *gmtcalfmt = @"%Y%m%dT%H%M00Z"; } @end /* NSDate(ICalValue) */ + + +#ifndef ABS +#define ABS(a) ((a) < 0 ? -(a) : (a)) +#endif + +@implementation NSCalendarDate (iCalRecurrenceCalculatorExtensions) + +- (unsigned)yearsBetweenDate:(NSCalendarDate *)_date { + return ABS([self yearOfCommonEra] - [_date yearOfCommonEra]); +} + +- (unsigned)monthsBetweenDate:(NSCalendarDate *)_date { + NSCalendarDate *start, *end; + NSComparisonResult order; + int yDiff; + + order = [self compare:_date]; + if (order == NSOrderedSame) + return 0; + else if (order == NSOrderedAscending) { + start = self; + end = _date; + } + else { + start = _date; + end = self; + } + yDiff = [end yearOfCommonEra] - [start yearOfCommonEra]; + if (yDiff > 0) { + unsigned monthsRemaining, monthsToGo; + + monthsRemaining = 12 - [start monthOfYear]; + monthsToGo = [end monthOfYear]; + yDiff -= 1; + return monthsRemaining + monthsToGo + (12 * yDiff); + } + /* start and end in same year, calculate plain diff */ + return [end monthOfYear] - [start monthOfYear]; +} + +- (unsigned)daysBetweenDate:(NSCalendarDate *)_date { + return ABS([self julianNumber] - [_date julianNumber]); +} +@end /* NSCalendarDate (iCalRecurrenceCalculatorExtensions) */ diff --git a/sope-ical/NGiCal/Version b/sope-ical/NGiCal/Version index c2c1f65e..99d8f0db 100644 --- a/sope-ical/NGiCal/Version +++ b/sope-ical/NGiCal/Version @@ -2,6 +2,7 @@ MAJOR_VERSION=4 MINOR_VERSION=5 -SUBMINOR_VERSION:=39 +SUBMINOR_VERSION:=40 +# v4.5.40 requires NGExtensions v4.5.145 # v4.5.37 requires NGExtensions v4.5.140 diff --git a/sope-ical/NGiCal/iCalEvent.h b/sope-ical/NGiCal/iCalEvent.h index 2c7489a2..59c2602a 100644 --- a/sope-ical/NGiCal/iCalEvent.h +++ b/sope-ical/NGiCal/iCalEvent.h @@ -62,6 +62,7 @@ - (BOOL)isAllDay; - (BOOL)isWithinCalendarDateRange:(NGCalendarDateRange *)_range; +- (NSCalendarDate *)lastPossibleRecurrenceStartDate; /* calculating changes */ diff --git a/sope-ical/NGiCal/iCalEvent.m b/sope-ical/NGiCal/iCalEvent.m index bb60b10b..34217f02 100644 --- a/sope-ical/NGiCal/iCalEvent.m +++ b/sope-ical/NGiCal/iCalEvent.m @@ -170,6 +170,17 @@ return NO; } +- (NSCalendarDate *)lastPossibleRecurrenceStartDate { + NGCalendarDateRange *fir; + + if (![self isRecurrent]) + return nil; + + fir = [NGCalendarDateRange calendarDateRangeWithStartDate:self->startDate + endDate:self->endDate]; + return [self lastPossibleRecurrenceStartDateUsingFirstInstanceCalendarDateRange:fir]; +} + /* ical typing */ - (NSString *)entityName { diff --git a/sope-ical/NGiCal/iCalRecurrenceCalculator.h b/sope-ical/NGiCal/iCalRecurrenceCalculator.h index 49ca6b7f..60cf5289 100644 --- a/sope-ical/NGiCal/iCalRecurrenceCalculator.h +++ b/sope-ical/NGiCal/iCalRecurrenceCalculator.h @@ -48,6 +48,9 @@ - (NSArray *)recurrenceRangesWithinCalendarDateRange:(NGCalendarDateRange *)_r; - (BOOL)doesRecurrWithinCalendarDateRange:(NGCalendarDateRange *)_range; +- (NGCalendarDateRange *)firstInstanceCalendarDateRange; +- (NGCalendarDateRange *)lastInstanceCalendarDateRange; /* might be nil */ + @end #endif /* __NGiCal_iCalRecurrenceCalculator_H_ */ diff --git a/sope-ical/NGiCal/iCalRecurrenceCalculator.m b/sope-ical/NGiCal/iCalRecurrenceCalculator.m index 63e7b91b..83b57d8d 100644 --- a/sope-ical/NGiCal/iCalRecurrenceCalculator.m +++ b/sope-ical/NGiCal/iCalRecurrenceCalculator.m @@ -22,9 +22,9 @@ #include "iCalRecurrenceCalculator.h" #include #include "iCalRecurrenceRule.h" +#include "NSCalendarDate+ICal.h" #include "common.h" - /* class cluster */ @interface iCalDailyRecurrenceCalculator : iCalRecurrenceCalculator @@ -48,7 +48,11 @@ } @end -/* superclass */ +/* Private */ + +@interface iCalRecurrenceCalculator (PrivateAPI) +- (NSCalendarDate *)lastInstanceStartDate; +@end @implementation iCalRecurrenceCalculator @@ -109,76 +113,136 @@ static Class yearlyCalcClass = Nil; return nil; /* subclass responsibility */ } - (BOOL)doesRecurrWithinCalendarDateRange:(NGCalendarDateRange *)_range { - return NO; /* subclass responsibility */ + NSArray *ranges; + + ranges = [self recurrenceRangesWithinCalendarDateRange:_range]; + return (ranges == nil || [ranges count] == 0) ? NO : YES; } -@end /* iCalRecurrenceCalculator */ +- (NGCalendarDateRange *)firstInstanceCalendarDateRange { + return self->firstRange; +} +- (NGCalendarDateRange *)lastInstanceCalendarDateRange { + NSCalendarDate *start, *end; -/* ZNeK: NOTE -taeglich: -alle events -fuer all diese jeweils -differenz der julian numbers durch interval teilen, wenn ok, ... -*/ -@implementation iCalDailyRecurrenceCalculator + start = [self lastInstanceStartDate]; + if (!start) + return nil; + end = [start addTimeInterval:[self->firstRange duration]]; + return [NGCalendarDateRange calendarDateRangeWithStartDate:start + endDate:end]; +} -- (unsigned)factor { - return 1; +- (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; } -- (BOOL)doesRecurrWithinCalendarDateRange:(NGCalendarDateRange *)_range { - long i, jnFirst, jnStart, jnEnd, startEndCount; - unsigned interval; +@end /* iCalRecurrenceCalculator */ - jnFirst = [[self->firstRange startDate] julianNumber]; - jnEnd = [[_range endDate] julianNumber]; - if (jnFirst > jnEnd) - return NO; +@implementation iCalDailyRecurrenceCalculator - jnStart = [[_range startDate] julianNumber]; +- (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:[_range startDate]] == NSOrderedAscending) - return NO; + if ([until compare:[_r startDate]] == NSOrderedAscending) + return nil; jnRuleLast = [until julianNumber]; } else { jnRuleLast = (interval * [self->rrule repeatCount] * [self factor]) - + jnFirst; + + jnFirst; if (jnRuleLast < jnStart) - return NO; + return nil; } /* jnStart < jnRuleLast < jnEnd ? */ if (jnEnd > jnRuleLast) jnEnd = jnRuleLast; } + ranges = [NSMutableArray arrayWithCapacity:5]; startEndCount = (jnEnd - jnStart) + 1; for (i = 0 ; i < startEndCount; i++) { long jnTest; jnTest = (jnStart + i) - jnFirst; - if ((jnTest % interval) == 0) - return YES; + if ((jnTest % interval) == 0) { + NSCalendarDate *start, *end; + NGCalendarDateRange *r; + + start = [NSCalendarDate dateForJulianNumber:jnStart + i]; + start = [start hour: [firStart hourOfDay] + minute:[firStart minuteOfHour] + second:[firStart secondOfMinute]]; + end = [start addTimeInterval:[self->firstRange duration]]; + r = [NGCalendarDateRange calendarDateRangeWithStartDate:start + endDate:end]; + [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] * + [self factor]) + + jnFirst; + until = [NSCalendarDate dateForJulianNumber:jnRuleLast]; + until = [until hour: [firStart hourOfDay] + minute:[firStart minuteOfHour] + second:[firStart secondOfMinute]]; + return until; } - return NO; + return [super lastInstanceStartDate]; +} + +- (unsigned)factor { + return 1; } @end /* iCalDailyRecurrenceCalculator */ + /* - ZNeK: NOTE - woechentlich: - wie taeglich, aber teilen durch interval*7 + TODO: If BYDAY is specified, lastInstanceStartDate and recurrences will + differ significantly! */ @implementation iCalWeeklyRecurrenceCalculator @@ -188,33 +252,122 @@ differenz der julian numbers durch interval teilen, wenn ok, ... @end /* iCalWeeklyRecurrenceCalculator */ -/* - ZNeK: NOTE - monatlich: - monatlich: differenz der monate / interval - alle events mit korrektem tag. - dann differenz der monate durch interval teilen, wenn ok, ... - (diffrenz der monate = 12 * 'volle' jahre plus monate im start jahr und monate im ziel jahr - */ @implementation iCalMonthlyRecurrenceCalculator -- (BOOL)doesRecurrWithinCalendarDateRange:(NGCalendarDateRange *)_range { - return YES; +- (NSArray *)recurrenceRangesWithinCalendarDateRange:(NGCalendarDateRange *)_r { + NSMutableArray *ranges; + NSCalendarDate *firStart, *rStart, *rEnd, *until; + unsigned i, count, interval, 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]; + count = [rStart monthsBetweenDate:rEnd] + 1; + ranges = [NSMutableArray arrayWithCapacity:count]; + for (i = 0 ; i < count; i++) { + unsigned test; + + test = diff + i; + if ((test % interval) == 0) { + NSCalendarDate *start, *end; + NGCalendarDateRange *r; + + start = [firStart dateByAddingYears:0 + months:diff + i + days:0]; + end = [start addTimeInterval:[self->firstRange duration]]; + r = [NGCalendarDateRange calendarDateRangeWithStartDate:start + endDate:end]; + [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 */ -/* - ZNeK: NOTE - jaehrlich: - alle events mit dem korrekten tag und monat. - fuer all diese jeweils - differenz in jahren durch interval teilen, wenn ok, dann event fuer den tag. -*/ @implementation iCalYearlyRecurrenceCalculator -- (BOOL)doesRecurrWithinCalendarDateRange:(NGCalendarDateRange *)_range { - return YES; +- (NSArray *)recurrenceRangesWithinCalendarDateRange:(NGCalendarDateRange *)_r { + NSMutableArray *ranges; + NSCalendarDate *firStart, *rStart, *rEnd, *until; + unsigned i, count, interval, 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]; + count = [rStart yearsBetweenDate:rEnd] + 1; + ranges = [NSMutableArray arrayWithCapacity:count]; + for (i = 0 ; i < count; i++) { + unsigned test; + + test = diff + i; + if ((test % interval) == 0) { + NSCalendarDate *start, *end; + NGCalendarDateRange *r; + + start = [firStart dateByAddingYears:diff + i + months:0 + days:0]; + end = [start addTimeInterval:[self->firstRange duration]]; + r = [NGCalendarDateRange calendarDateRangeWithStartDate:start + endDate:end]; + [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 */ diff --git a/sope-ical/NGiCal/iCalRepeatableEntityObject.h b/sope-ical/NGiCal/iCalRepeatableEntityObject.h index 33509a07..7fb90d6d 100644 --- a/sope-ical/NGiCal/iCalRepeatableEntityObject.h +++ b/sope-ical/NGiCal/iCalRepeatableEntityObject.h @@ -59,6 +59,9 @@ - (BOOL)isWithinCalendarDateRange:(NGCalendarDateRange *)_range firstInstanceCalendarDateRange:(NGCalendarDateRange *)_fir; +/* this is the outmost bound possible, not necessarily the real last date */ +- (NSCalendarDate *)lastPossibleRecurrenceStartDateUsingFirstInstanceCalendarDateRange:(NGCalendarDateRange *)_r; + @end #endif /* __NGiCal_iCalRepeatableEntityObject_H_ */ diff --git a/sope-ical/NGiCal/iCalRepeatableEntityObject.m b/sope-ical/NGiCal/iCalRepeatableEntityObject.m index 3b4345e3..01242e3f 100644 --- a/sope-ical/NGiCal/iCalRepeatableEntityObject.m +++ b/sope-ical/NGiCal/iCalRepeatableEntityObject.m @@ -159,4 +159,32 @@ return YES; } +/* this is the outmost bound possible, not necessarily the real last date */ +- (NSCalendarDate *)lastPossibleRecurrenceStartDateUsingFirstInstanceCalendarDateRange:(NGCalendarDateRange *)_r +{ + NSCalendarDate *date; + unsigned i, count; + + count = [self->rRules count]; + if (!count) + return nil; + + date = nil; + for (i = 0; i < count; i++) { + iCalRecurrenceRule *rule; + iCalRecurrenceCalculator *calc; + NSCalendarDate *rdate; + + rule = [self->rRules objectAtIndex:i]; + if ([rule isInfinite]) + return nil; /* rule is not bound, hence no limit */ + calc = [iCalRecurrenceCalculator recurrenceCalculatorForRecurrenceRule:rule + withFirstInstanceCalendarDateRange:_r]; + rdate = [[calc lastInstanceCalendarDateRange] startDate]; + if (date == nil || [date compare:rdate] == NSOrderedAscending) + date = rdate; + } + return date; +} + @end diff --git a/sope-ical/NGiCal/tests/iCalRecurrenceCalculatorTests.m b/sope-ical/NGiCal/tests/iCalRecurrenceCalculatorTests.m index a6905032..eb6719b4 100644 --- a/sope-ical/NGiCal/tests/iCalRecurrenceCalculatorTests.m +++ b/sope-ical/NGiCal/tests/iCalRecurrenceCalculatorTests.m @@ -29,6 +29,7 @@ NSTimeZone *gmt; NGCalendarDateRange *fir; NGCalendarDateRange *tr1; + NGCalendarDateRange *dr1; } - (iCalRecurrenceRule *)ruleWithICalString:(NSString *)_rule; @@ -83,12 +84,31 @@ self->tr1 = [[NGCalendarDateRange calendarDateRangeWithStartDate:sd endDate:ed] retain]; + + sd = [NSCalendarDate dateWithYear:2005 + month:2 + day:6 + hour:0 + minute:0 + second:0 + timeZone:self->gmt]; + ed = [NSCalendarDate dateWithYear:2005 + month:2 + day:13 + hour:23 + minute:59 + second:59 + timeZone:self->gmt]; + + self->dr1 = [[NGCalendarDateRange calendarDateRangeWithStartDate:sd + endDate:ed] retain]; } - (void)tearDown { [self->gmt release]; [self->fir release]; [self->tr1 release]; + [self->dr1 release]; } /* Private Helper */ @@ -126,21 +146,25 @@ iCalRecurrenceRule *rule; iCalRecurrenceCalculator *calc; BOOL result; - + NSArray *ranges; + /* recurrence outside of range */ rule = [self ruleWithICalString:@"FREQ=DAILY;INTERVAL=2;COUNT=2"]; calc = [iCalRecurrenceCalculator recurrenceCalculatorForRecurrenceRule:rule - withFirstInstanceCalendarDateRange:fir]; + withFirstInstanceCalendarDateRange:fir]; result = [calc doesRecurrWithinCalendarDateRange:self->tr1]; STAssertFalse(result, @"recurrence!"); /* recurrence within range */ rule = [self ruleWithICalString:@"FREQ=DAILY;INTERVAL=2;UNTIL=20050212T120000Z"]; calc = [iCalRecurrenceCalculator recurrenceCalculatorForRecurrenceRule:rule - withFirstInstanceCalendarDateRange:fir]; + withFirstInstanceCalendarDateRange:fir]; result = [calc doesRecurrWithinCalendarDateRange:self->tr1]; STAssertTrue(result, @"didn't spot expected recurrence!"); - + ranges = [calc recurrenceRangesWithinCalendarDateRange:self->tr1]; + STAssertTrue([ranges count] == 1, @"didn't spot expected recurrence!"); + ranges = [calc recurrenceRangesWithinCalendarDateRange:self->dr1]; + STAssertTrue([ranges count] == 4, @"didn't spot expected recurrence!"); } -- 2.39.5