+2005-09-21 Helge Hess <helge.hess@skyrix.com>
+
+ * 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 <helge.hess@opengroupware.org>
* iCalMonthlyRecurrenceCalculator.m: stop calculation if a byday part
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
@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: %@",
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;
}
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];
Provides an API for performing common calculations performed in conjunction
with iCalRecurrenceRule objects.
+
+ TODO: rather move this functionality to iCalRecurrenceRule?
*/
@class NSArray;
/* factory */
+ (id)recurrenceCalculatorForRecurrenceRule:(iCalRecurrenceRule *)_rrule
- withFirstInstanceCalendarDateRange:(NGCalendarDateRange *)_range
+ withFirstInstanceCalendarDateRange:(NGCalendarDateRange *)_range
{
return [[[self alloc] initWithRecurrenceRule:_rrule
firstInstanceCalendarDateRange:_range] autorelease];
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];
[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];
[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];
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;
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];
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 {
- (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
- (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:@"="];
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;
}
- (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;
- (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;
/* 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];
}