From 2d9d8e82856cfc6c5c61b121890d66fa96aa1479 Mon Sep 17 00:00:00 2001 From: helge Date: Wed, 21 Sep 2005 12:09:26 +0000 Subject: [PATCH] improved rrule parser fixed some month rrule calculation git-svn-id: http://svn.opengroupware.org/SOPE/trunk@1125 e4a50df8-12e2-0310-a44c-efbce7f8a7e3 --- sope-ical/NGiCal/ChangeLog | 10 ++ sope-ical/NGiCal/Version | 2 +- .../NGiCal/iCalMonthlyRecurrenceCalculator.m | 72 +++++++---- sope-ical/NGiCal/iCalRecurrenceCalculator.h | 2 + sope-ical/NGiCal/iCalRecurrenceCalculator.m | 63 +++++----- sope-ical/NGiCal/iCalRecurrenceRule.m | 117 ++++++++++++++++-- 6 files changed, 208 insertions(+), 58 deletions(-) diff --git a/sope-ical/NGiCal/ChangeLog b/sope-ical/NGiCal/ChangeLog index 84247db3..95123ffc 100644 --- a/sope-ical/NGiCal/ChangeLog +++ b/sope-ical/NGiCal/ChangeLog @@ -1,3 +1,13 @@ +2005-09-21 Helge Hess + + * v4.5.65 + + * iCalMonthlyRecurrenceCalculator.m: fixed calculation of 'count' field + + * iCalRecurrenceCalculator.m: minor code cleanups + + * iCalRecurrenceRule.m: improved rrule parser + 2005-09-20 Helge Hess * iCalMonthlyRecurrenceCalculator.m: stop calculation if a byday part diff --git a/sope-ical/NGiCal/Version b/sope-ical/NGiCal/Version index 0e2ffe99..3e9a0986 100644 --- a/sope-ical/NGiCal/Version +++ b/sope-ical/NGiCal/Version @@ -2,7 +2,7 @@ MAJOR_VERSION=4 MINOR_VERSION=5 -SUBMINOR_VERSION:=64 +SUBMINOR_VERSION:=65 # v4.5.40 requires NGExtensions v4.5.145 # v4.5.37 requires NGExtensions v4.5.140 diff --git a/sope-ical/NGiCal/iCalMonthlyRecurrenceCalculator.m b/sope-ical/NGiCal/iCalMonthlyRecurrenceCalculator.m index be6e6f8a..7f04abb0 100644 --- a/sope-ical/NGiCal/iCalMonthlyRecurrenceCalculator.m +++ b/sope-ical/NGiCal/iCalMonthlyRecurrenceCalculator.m @@ -36,16 +36,19 @@ @implementation iCalMonthlyRecurrenceCalculator - (NSArray *)recurrenceRangesWithinCalendarDateRange:(NGCalendarDateRange *)_r{ + /* main entry */ NSMutableArray *ranges; + NSTimeZone *timeZone; NSCalendarDate *firStart, *rStart, *rEnd, *until; unsigned i, count, interval; int diff; firStart = [self->firstRange startDate]; + timeZone = [firStart timeZone]; rStart = [_r startDate]; rEnd = [_r endDate]; interval = [self->rrule repeatInterval]; - until = [self lastInstanceStartDate]; + until = [self lastInstanceStartDate]; // TODO: maybe replace if ([self->rrule byDayMask] != 0) { [self errorWithFormat:@"cannot process byday part of rrule: %@", @@ -53,37 +56,62 @@ return nil; } + /* check whether the range to be processed is beyond the 'until' date */ + if (until != nil) { - if ([until compare:rStart] == NSOrderedAscending) + if ([until compare:rStart] == NSOrderedAscending) /* until before start */ return nil; - if ([until compare:rEnd] == NSOrderedDescending) - rEnd = until; + if ([until compare:rEnd] == NSOrderedDescending) /* end before until */ + rEnd = until; // TODO: why is that? end is _before_ until? } - - diff = [firStart monthsBetweenDate:rStart]; + + // TODO: I think the diff is to skip recurrence which are before the + // requested range. Not sure whether this is actually possible, eg + // the repeatCount must be processed from the start. + 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++) { + NSCalendarDate *start, *end; + NGCalendarDateRange *r; 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]; + + if (test < 0) + continue; + + if ((test % interval) != 0) + continue; + + start = [firStart dateByAddingYears:0 months:(diff + i) days:0]; + [start setTimeZone:timeZone]; + + /* check whether we are still in the limits */ + + // TODO: I think we should check in here whether we succeeded the + // repeatCount. Currently we precalculate that info in the + // -lastInstanceStartDate method. + if (until != nil) { + /* Note: the 'until' in the rrule is inclusive as per spec */ + if ([until compare:start] == NSOrderedAscending) /* start after until */ + break; /* Note: we assume that the algorithm is sequential */ } + + /* create end date */ + + end = [start addTimeInterval:[self->firstRange duration]]; + [end setTimeZone:timeZone]; + + /* create range and check whether its in the requested range */ + + r = [[NGCalendarDateRange alloc] initWithStartDate:start endDate:end]; + if ([_r containsDateRange:r]) + [ranges addObject:r]; + [r release]; r = nil; } return ranges; } @@ -92,9 +120,9 @@ if ([self->rrule repeatCount] > 0) { NSCalendarDate *until; unsigned months, interval; - + interval = [self->rrule repeatInterval]; - months = [self->rrule repeatCount] * interval; + months = ([self->rrule repeatCount] - 1) * interval; until = [[self->firstRange startDate] dateByAddingYears:0 months:months days:0]; diff --git a/sope-ical/NGiCal/iCalRecurrenceCalculator.h b/sope-ical/NGiCal/iCalRecurrenceCalculator.h index 007a2ed8..7f191c61 100644 --- a/sope-ical/NGiCal/iCalRecurrenceCalculator.h +++ b/sope-ical/NGiCal/iCalRecurrenceCalculator.h @@ -29,6 +29,8 @@ Provides an API for performing common calculations performed in conjunction with iCalRecurrenceRule objects. + + TODO: rather move this functionality to iCalRecurrenceRule? */ @class NSArray; diff --git a/sope-ical/NGiCal/iCalRecurrenceCalculator.m b/sope-ical/NGiCal/iCalRecurrenceCalculator.m index 6e048f7e..d144efea 100644 --- a/sope-ical/NGiCal/iCalRecurrenceCalculator.m +++ b/sope-ical/NGiCal/iCalRecurrenceCalculator.m @@ -67,7 +67,7 @@ static Class yearlyCalcClass = Nil; /* factory */ + (id)recurrenceCalculatorForRecurrenceRule:(iCalRecurrenceRule *)_rrule - withFirstInstanceCalendarDateRange:(NGCalendarDateRange *)_range + withFirstInstanceCalendarDateRange:(NGCalendarDateRange *)_range { return [[[self alloc] initWithRecurrenceRule:_rrule firstInstanceCalendarDateRange:_range] autorelease]; @@ -87,9 +87,9 @@ static Class yearlyCalcClass = Nil; NSMutableArray *exDates; unsigned i, count, rCount; - ranges = [NSMutableArray array]; - count = [_rRules count]; - for (i = 0; i < count; i++) { + ranges = [NSMutableArray arrayWithCapacity:64]; + + for (i = 0, count = [_rRules count]; i < count; i++) { NSArray *rs; rule = [_rRules objectAtIndex:i]; @@ -102,12 +102,12 @@ static Class yearlyCalcClass = Nil; [ranges addObjectsFromArray:rs]; } - if (![ranges count]) + if ([ranges count] == 0) return nil; /* test if any exceptions do match */ - count = [_exRules count]; - for (i = 0; i < count; i++) { + + for (i = 0, count = [_exRules count]; i < count; i++) { NSArray *rs; rule = [_exRules objectAtIndex:i]; @@ -120,14 +120,14 @@ static Class yearlyCalcClass = Nil; [ranges removeObjectsInArray:rs]; } - if (![ranges count]) + if (![ranges isNotEmpty]) return nil; /* exception dates */ - count = [_exDates count]; - if (!count) return ranges; - + if ((count = [_exDates count]) == 0) + return ranges; + /* sort out exDates not within range */ exDates = [NSMutableArray arrayWithCapacity:count]; @@ -135,20 +135,19 @@ static Class yearlyCalcClass = Nil; id exDate; exDate = [_exDates objectAtIndex:i]; - if (![exDate isKindOfClass:NSCalendarDateClass]) { + 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++) { + if ((count = [exDates count]) == 0) + return ranges; + + for (i = 0, rCount = [ranges count]; i < count; i++) { NSCalendarDate *exDate; NGCalendarDateRange *r; unsigned k; @@ -187,19 +186,24 @@ static Class yearlyCalcClass = Nil; calcClass = monthlyCalcClass; else if (freq == iCalRecurrenceFrequenceYearly) calcClass = yearlyCalcClass; - - [self autorelease]; + else { + [self errorWithFormat:@"unsupported rrule frequency: %@", _rrule]; + calcClass = Nil; + [self release]; + return nil; + } + + [self autorelease]; // TODO: why autorelease? if (calcClass == Nil) return nil; - - self = [[calcClass alloc] init]; - ASSIGN(self->rrule, _rrule); - ASSIGN(self->firstRange, _range); + + if ((self = [[calcClass alloc] init]) != nil) { + self->rrule = [_rrule retain]; + self->firstRange = [_range retain]; + } return self; } -/* dealloc */ - - (void)dealloc { [self->firstRange release]; [self->rrule release]; @@ -245,14 +249,17 @@ static Class yearlyCalcClass = Nil; case 4: weekDay = iCalWeekDayThursday; break; case 5: weekDay = iCalWeekDayFriday; break; case 6: weekDay = iCalWeekDaySaturday; break; - default: weekDay = iCalWeekDaySunday; break; /* keep compiler happy */ + default: + [self errorWithFormat:@"got unexpected weekday: %d", day]; + weekDay = iCalWeekDaySunday; + break; /* keep compiler happy */ } return weekDay; } /* calculation */ -- (NSArray *)recurrenceRangesWithinCalendarDateRange:(NGCalendarDateRange *)_r { +- (NSArray *)recurrenceRangesWithinCalendarDateRange:(NGCalendarDateRange *)_r{ return nil; /* subclass responsibility */ } - (BOOL)doesRecurrWithinCalendarDateRange:(NGCalendarDateRange *)_range { diff --git a/sope-ical/NGiCal/iCalRecurrenceRule.m b/sope-ical/NGiCal/iCalRecurrenceRule.m index 7a03c467..3a32075c 100644 --- a/sope-ical/NGiCal/iCalRecurrenceRule.m +++ b/sope-ical/NGiCal/iCalRecurrenceRule.m @@ -49,9 +49,13 @@ - (NSString *)wkst; - (NSString *)byDayList; -- (void)_processRule; +- (void)_parseRuleString:(NSString *)_rrule; - (void)setRrule:(NSString *)_rrule; // TODO: weird name? +/* currently used by parser, should be removed (replace with an -init..) */ +- (void)setByday:(NSString *)_byDayList; +- (void)setFreq:(NSString *)_freq; + @end @implementation iCalRecurrenceRule @@ -232,19 +236,27 @@ - (void)setRrule:(NSString *)_rrule { ASSIGNCOPY(self->rrule, _rrule); - [self _processRule]; + [self _parseRuleString:self->rrule]; } -/* Processing existing rrule */ +/* parsing rrule */ -- (void)_processRule { +- (void)_parseRuleString:(NSString *)_rrule { + // TODO: to be exact we would need a timezone to properly process the 'until' + // date NSArray *props; unsigned i, count; + NSString *pFrequency = nil; + NSString *pUntil = nil; + NSString *pCount = nil; + NSString *pByday = nil; + NSString *pBysetpos = nil; - props = [self->rrule componentsSeparatedByString:@";"]; + props = [_rrule componentsSeparatedByString:@";"]; for (i = 0, count = [props count]; i < count; i++) { NSString *prop, *key, *value; NSRange r; + NSString **vHolder = NULL; prop = [props objectAtIndex:i]; r = [prop rangeOfString:@"="]; @@ -256,8 +268,97 @@ key = prop; value = nil; } - [self takeValue:value forKey:[key lowercaseString]]; + + key = [[key stringByTrimmingSpaces] lowercaseString]; + if (![key isNotEmpty]) { + [self errorWithFormat:@"empty component in rrule: %@", _rrule]; + continue; + } + + vHolder = NULL; + switch ([key characterAtIndex:0]) { + case 'b': + if ([key isEqualToString:@"byday"]) vHolder = &pByday; break; + if ([key isEqualToString:@"bysetpos"]) vHolder = &pBysetpos; break; + break; + case 'c': + if ([key isEqualToString:@"count"]) vHolder = &pCount; break; + break; + case 'f': + if ([key isEqualToString:@"freq"]) vHolder = &pFrequency; break; + break; + case 'u': + if ([key isEqualToString:@"until"]) vHolder = &pUntil; break; + break; + default: + break; + } + + if (vHolder != NULL) { + if ([*vHolder isNotEmpty]) + [self errorWithFormat:@"more than one '%@' in: %@", key, _rrule]; + else + *vHolder = [value copy]; + } + else { + // TODO: we should just parse known keys and put remainders into a + // separate dictionary + //[self logWithFormat:@"TODO: add explicit support for key: %@", key]; + [self takeValue:value forKey:key]; + } } + + /* parse and fill individual values */ + // TODO: this method should be a class method and create a new rrule object + + if ([pFrequency isNotEmpty]) + [self setFreq:pFrequency]; + else + [self errorWithFormat:@"rrule contains no frequency: '%@'", _rrule]; + [pFrequency release]; pFrequency = nil; + + // TODO: we should parse byday in here + if (pByday != nil) [self setByday:pByday]; + [pByday release]; pByday = nil; + + if (pBysetpos != nil) + // TODO: implement + [self errorWithFormat:@"rrule contains bysetpos, unsupported: %@", _rrule]; + [pBysetpos release]; pBysetpos = nil; + + if (pUntil != nil) { + NSCalendarDate *pUntilDate; + + if (pCount != nil) { + [self errorWithFormat:@"rrule contains 'count' AND 'until': %@", _rrule]; + [pCount release]; + pCount = nil; + } + + /* + The spec says: + "If specified as a date-time value, then it MUST be specified in an + UTC time format." + TODO: we still need some object representing a 'timeless' date. + */ + if (![pUntil hasSuffix:@"Z"]) { + [self warnWithFormat:@"'until' date has no explicit UTC marker: '%@'", + _rrule]; + } + + pUntilDate = [NSCalendarDate calendarDateWithICalRepresentation:pUntil]; + if (pUntilDate != nil) + [self setUntilDate:pUntilDate]; + else { + [self errorWithFormat:@"could not parse 'until' in rrule: %@", + _rrule]; + } + } + [pUntil release]; pUntil = nil; + + if (pCount != nil) + [self setRepeatCount:[pCount intValue]]; + [pCount release]; pCount = nil; } @@ -265,6 +366,7 @@ - (void)setFreq:(NSString *)_freq { // TODO: shouldn't we preserve what the user gives us? + // => only used by -_parseRuleString: parser? _freq = [_freq uppercaseString]; if ([_freq isEqualToString:@"WEEKLY"]) self->frequency = iCalRecurrenceFrequenceWeekly; @@ -306,6 +408,7 @@ - (void)setByday:(NSString *)_byDayList { // TODO: each day can have an associated occurence, eg: // +1MO,+2TU,-9WE + // TODO: this should be moved to the parser NSArray *days; unsigned i, count; @@ -365,7 +468,7 @@ /* key/value coding */ - (void)handleTakeValue:(id)_value forUnboundKey:(NSString *)_key { - [self warnWithFormat:@"Don't know how to process '%@'!", _key]; + [self warnWithFormat:@"Cannot handle unbound key: '%@'", _key]; } -- 2.39.2