X-Git-Url: https://err.no/cgi-bin/gitweb.cgi?a=blobdiff_plain;f=sope-ical%2FNGiCal%2FiCalRecurrenceRule.m;h=f437738a57e707ab7083a3aa6ac7474aaba576f9;hb=c9c941b66b24b243f36bd2a3d537f584274cd969;hp=7c09525fdb2c23aff6bae64b52323d39cae033bf;hpb=6744b1e0ea75bdeed62e4c940ab1cae24b33efe9;p=sope diff --git a/sope-ical/NGiCal/iCalRecurrenceRule.m b/sope-ical/NGiCal/iCalRecurrenceRule.m index 7c09525f..f437738a 100644 --- a/sope-ical/NGiCal/iCalRecurrenceRule.m +++ b/sope-ical/NGiCal/iCalRecurrenceRule.m @@ -20,69 +20,65 @@ */ #include "iCalRecurrenceRule.h" -#include "iCalDateHolder.h" #include "NSCalendarDate+ICal.h" #include "common.h" /* - freq = rrFreq; - until = rrUntil; - count = rrCount; - interval = rrInterval; - bysecond = rrBySecondList; - byminute = rrByMinuteList; - byhour = rrByHourList; - byday = rrByDayList; - bymonthday = rrByMonthDayList; - byyearday = rrByYearDayList; - byweekno = rrByWeekNumberList; - bymonth = rrByMonthList; - bysetpos = rrBySetPosList; - wkst = rrWeekStart; - */ - -@interface iCalDateHolder (PrivateAPI) -- (void)setString:(NSString *)_value; -- (id)awakeAfterUsingSaxDecoder:(id)_decoder; -@end + freq = rrFreq; + until = rrUntil; + count = rrCount; + interval = rrInterval; + bysecond = rrBySecondList; + byminute = rrByMinuteList; + byhour = rrByHourList; + byday = rrByDayList; + bymonthday = rrByMonthDayList; + byyearday = rrByYearDayList; + byweekno = rrByWeekNumberList; + bymonth = rrByMonthList; + bysetpos = rrBySetPosList; + wkst = rrWeekStart; +*/ +// TODO: private API in the header file?! @interface iCalRecurrenceRule (PrivateAPI) + - (iCalWeekDay)weekDayFromICalRepresentation:(NSString *)_day; - (NSString *)iCalRepresentationForWeekDay:(iCalWeekDay)_weekDay; - (NSString *)freq; - (NSString *)wkst; - (NSString *)byDayList; -- (void)_processRule; -- (void)setRrule:(NSString *)_rrule; +- (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)initialize { - static BOOL didInit = NO; - - if (didInit) return; - didInit = YES; -} - + (id)recurrenceRuleWithICalRepresentation:(NSString *)_iCalRep { - iCalRecurrenceRule *r; - - r = [[[self alloc] init] autorelease]; - [r setRrule:_iCalRep]; - return r; + return [[[self alloc] initWithString:_iCalRep] autorelease]; } -- (id)init { - self = [super init]; - if (self) { +- (id)init { /* designated initializer */ + if ((self = [super init]) != nil) { self->byDay.weekStart = iCalWeekDayMonday; self->interval = 1; } return self; } +- (id)initWithString:(NSString *)_str { + if ((self = [self init]) != nil) { + [self setRrule:_str]; + } + return self; +} + - (void)dealloc { [self->untilDate release]; [self->rrule release]; @@ -90,7 +86,7 @@ } -/* Accessors */ +/* accessors */ - (void)setFrequency:(iCalRecurrenceFrequency)_frequency { self->frequency = _frequency; @@ -133,73 +129,66 @@ - (unsigned)byDayMask { return self->byDay.mask; } +- (int)byDayOccurence1 { + return self->byDayOccurence1; +} - (BOOL)isInfinite { return (self->repeatCount != 0 || self->untilDate) ? NO : YES; } -/* Private */ +/* private */ - (iCalWeekDay)weekDayFromICalRepresentation:(NSString *)_day { - _day = [_day uppercaseString]; - if ([_day isEqualToString:@"MO"]) - return iCalWeekDayMonday; - else if ([_day isEqualToString:@"TU"]) - return iCalWeekDayTuesday; - else if ([_day isEqualToString:@"WE"]) - return iCalWeekDayWednesday; - else if ([_day isEqualToString:@"TH"]) - return iCalWeekDayThursday; - else if ([_day isEqualToString:@"FR"]) - return iCalWeekDayFriday; - else if ([_day isEqualToString:@"SA"]) - return iCalWeekDaySaturday; - else if ([_day isEqualToString:@"SU"]) - return iCalWeekDaySunday; - else - [NSException raise:NSGenericException - format:@"Incorrect weekDay '%@' specified!", _day]; + if ([_day length] > 1) { + /* be tolerant */ + unichar c0, c1; + + c0 = [_day characterAtIndex:0]; + if (c0 == 'm' || c0 == 'M') return iCalWeekDayMonday; + if (c0 == 'w' || c0 == 'W') return iCalWeekDayWednesday; + if (c0 == 'f' || c0 == 'F') return iCalWeekDayFriday; + + c1 = [_day characterAtIndex:1]; + if (c0 == 't' || c0 == 'T') { + if (c1 == 'u' || c1 == 'U') return iCalWeekDayTuesday; + if (c1 == 'h' || c1 == 'H') return iCalWeekDayThursday; + } + if (c0 == 's' || c0 == 'S') { + if (c1 == 'a' || c1 == 'A') return iCalWeekDaySaturday; + if (c1 == 'u' || c1 == 'U') return iCalWeekDaySunday; + } + } + + // TODO: do not raise but rather return an error value? + [NSException raise:NSGenericException + format:@"Incorrect weekDay '%@' specified!", _day]; return iCalWeekDayMonday; /* keep compiler happy */ } - (NSString *)iCalRepresentationForWeekDay:(iCalWeekDay)_weekDay { switch (_weekDay) { - case iCalWeekDayMonday: - return @"MO"; - case iCalWeekDayTuesday: - return @"TU"; - case iCalWeekDayWednesday: - return @"WE"; - case iCalWeekDayThursday: - return @"TH"; - case iCalWeekDayFriday: - return @"FR"; - case iCalWeekDaySaturday: - return @"SA"; - case iCalWeekDaySunday: - return @"SU"; - default: - return @"MO"; + case iCalWeekDayMonday: return @"MO"; + case iCalWeekDayTuesday: return @"TU"; + case iCalWeekDayWednesday: return @"WE"; + case iCalWeekDayThursday: return @"TH"; + case iCalWeekDayFriday: return @"FR"; + case iCalWeekDaySaturday: return @"SA"; + case iCalWeekDaySunday: return @"SU"; + default: return @"MO"; // TODO: return error? } } - (NSString *)freq { switch (self->frequency) { - case iCalRecurrenceFrequenceWeekly: - return @"WEEKLY"; - case iCalRecurrenceFrequenceMonthly: - return @"MONTHLY"; - case iCalRecurrenceFrequenceDaily: - return @"DAILY"; - case iCalRecurrenceFrequenceYearly: - return @"YEARLY"; - case iCalRecurrenceFrequenceHourly: - return @"HOURLY"; - case iCalRecurrenceFrequenceMinutely: - return @"MINUTELY"; - case iCalRecurrenceFrequenceSecondly: - return @"SECONDLY"; + case iCalRecurrenceFrequenceWeekly: return @"WEEKLY"; + case iCalRecurrenceFrequenceMonthly: return @"MONTHLY"; + case iCalRecurrenceFrequenceDaily: return @"DAILY"; + case iCalRecurrenceFrequenceYearly: return @"YEARLY"; + case iCalRecurrenceFrequenceHourly: return @"HOURLY"; + case iCalRecurrenceFrequenceMinutely: return @"MINUTELY"; + case iCalRecurrenceFrequenceSecondly: return @"SECONDLY"; default: return @"UNDEFINED?"; } @@ -210,29 +199,34 @@ } /* - TODO: - Each BYDAY value can also be preceded by a positive (+n) or negative - (-n) integer. If present, this indicates the nth occurrence of the - specific day within the MONTHLY or YEARLY RRULE. For example, within - a MONTHLY rule, +1MO (or simply 1MO) represents the first Monday - within the month, whereas -1MO represents the last Monday of the - month. If an integer modifier is not present, it means all days of - this type within the specified frequency. For example, within a - MONTHLY rule, MO represents all Mondays within the month. + TODO: + Each BYDAY value can also be preceded by a positive (+n) or negative + (-n) integer. If present, this indicates the nth occurrence of the + specific day within the MONTHLY or YEARLY RRULE. For example, within + a MONTHLY rule, +1MO (or simply 1MO) represents the first Monday + within the month, whereas -1MO represents the last Monday of the + month. If an integer modifier is not present, it means all days of + this type within the specified frequency. For example, within a + MONTHLY rule, MO represents all Mondays within the month. */ - (NSString *)byDayList { NSMutableString *s; unsigned i, mask, day; BOOL needsComma; - + s = [NSMutableString stringWithCapacity:20]; needsComma = NO; mask = self->byDay.mask; day = iCalWeekDayMonday; + for (i = 0; i < 7; i++) { if (mask & day) { if (needsComma) [s appendString:@","]; + else if (self->byDay.useOccurence) + // Note: we only support one occurrence currently + [s appendFormat:@"%i", self->byDayOccurence1]; + [s appendString:[self iCalRepresentationForWeekDay:day]]; needsComma = YES; } @@ -244,25 +238,32 @@ /* Rule */ - (void)setRrule:(NSString *)_rrule { - ASSIGN(self->rrule, _rrule); - [self _processRule]; + ASSIGNCOPY(self->rrule, _rrule); + [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:@";"]; - count = [props count]; - for (i = 0; i < count; i++) { + 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:@"="]; - if (r.length) { + if (r.length > 0) { key = [prop substringToIndex:r.location]; value = [prop substringFromIndex:NSMaxRange(r)]; } @@ -270,14 +271,105 @@ 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; } /* properties */ - (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; @@ -293,9 +385,10 @@ self->frequency = iCalRecurrenceFrequenceMinutely; else if ([_freq isEqualToString:@"SECONDLY"]) self->frequency = iCalRecurrenceFrequenceSecondly; - else + else { [NSException raise:NSGenericException format:@"Incorrect frequency '%@' specified!", _freq]; + } } - (void)setInterval:(NSString *)_interval { @@ -305,14 +398,10 @@ self->repeatCount = [_count unsignedIntValue]; } - (void)setUntil:(NSString *)_until { - iCalDateHolder *dh; NSCalendarDate *date; - dh = [[iCalDateHolder alloc] init]; - [dh setString:_until]; - date = [dh awakeAfterUsingSaxDecoder:nil]; + date = [NSCalendarDate calendarDateWithICalRepresentation:_until]; ASSIGN(self->untilDate, date); - [dh release]; } - (void)setWkst:(NSString *)_weekStart { @@ -320,18 +409,61 @@ } - (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; - + + /* reset mask */ self->byDay.mask = 0; + self->byDay.useOccurence = 0; + self->byDayOccurence1 = 0; + days = [_byDayList componentsSeparatedByString:@","]; - count = [days count]; - for (i = 0; i < count; i++) { + for (i = 0, count = [days count]; i < count; i++) { NSString *iCalDay; iCalWeekDay day; + unsigned len; + unichar c0; + int occurence; + + iCalDay = [days objectAtIndex:i]; // eg: MO or TU + if ((len = [iCalDay length]) == 0) { + [self errorWithFormat:@"found an empty day in byday list: '%@'", + _byDayList]; + continue; + } + + c0 = [iCalDay characterAtIndex:0]; + if (((c0 == '+' || c0 == '-') && len > 2) || (isdigit(c0) && len > 1)) { + int offset; + + occurence = [iCalDay intValue]; + + offset = 1; + while (offset < len && isdigit([iCalDay characterAtIndex:offset])) + offset++; + + iCalDay = [iCalDay substringFromIndex:offset]; + + if (self->byDay.useOccurence) { + [self errorWithFormat: + @"we only supported one occurence (occ=%i,day=%@): '%@'", + occurence, iCalDay, _byDayList]; + continue; + } + + self->byDay.useOccurence = 1; + self->byDayOccurence1 = occurence; + } + else if (self->byDay.useOccurence) { + [self errorWithFormat: + @"a byday occurence was specified on one day, but not on others" + @" (unsupported): '%@'", _byDayList]; + } - iCalDay = [days objectAtIndex:i]; - day = [self weekDayFromICalRepresentation:iCalDay]; + day = [self weekDayFromICalRepresentation:iCalDay]; self->byDay.mask |= day; } } @@ -339,21 +471,23 @@ /* 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]; } -/* Description */ +/* description */ - (NSString *)iCalRepresentation { NSMutableString *s; s = [NSMutableString stringWithCapacity:80]; + [s appendString:@"FREQ="]; [s appendString:[self freq]]; - if ([self repeatInterval] != 1) { + + if ([self repeatInterval] != 1) [s appendFormat:@";INTERVAL=%d", [self repeatInterval]]; - } + if (![self isInfinite]) { if ([self repeatCount] > 0) { [s appendFormat:@";COUNT=%d", [self repeatCount]]; @@ -374,5 +508,8 @@ return s; } +- (NSString *)description { + return [self iCalRepresentation]; +} -@end +@end /* iCalRecurrenceRule */