/* 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. */ #import #import #import #import #import #import #import "NSCalendarDate+NGCards.h" #import "NSString+NGCards.h" #import "NSCalendarDate+ICal.h" #import "iCalRecurrenceRule.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; */ // TODO: private API in the header file?! @interface iCalRecurrenceRule (PrivateAPI) - (iCalWeekDay) weekDayFromICalRepresentation: (NSString *) _day; - (NSString *) iCalRepresentationForWeekDay: (iCalWeekDay) _waeekDay; - (NSString *) freq; - (NSString *) wkst; - (NSString *) byDayList; // - (void)_parseRuleString:(NSString *)_rrule; /* currently used by parser, should be removed (replace with an -init..) */ - (void)setByday:(NSString *)_byDayList; @end @implementation iCalRecurrenceRule + (id) recurrenceRuleWithICalRepresentation: (NSString *) _iCalRep { iCalRecurrenceRule *rule; rule = [self elementWithTag: @"rrule"]; if ([_iCalRep length] > 0) [rule addValues: [_iCalRep componentsSeparatedByString: @";"]]; return rule; } - (id) init { if ((self = [super init]) != nil) { [self setTag: @"rrule"]; } return self; } - (id) initWithString: (NSString *) _str { if ((self = [self init])) { [self setRrule: _str]; } return self; } - (void) setRrule: (NSString *) _rrule { NSEnumerator *newValues; NSString *newValue; newValues = [[_rrule componentsSeparatedByString: @";"] objectEnumerator]; newValue = [newValues nextObject]; while (newValue) { [self addValue: newValue]; newValue = [newValues nextObject]; } } - (iCalRecurrenceFrequency) valueForFrequency: (NSString *) value { NSString *frequency; iCalRecurrenceFrequency freq; if ([value length] > 0) { frequency = [value uppercaseString]; if ([frequency isEqualToString:@"WEEKLY"]) freq = iCalRecurrenceFrequenceWeekly; else if ([frequency isEqualToString:@"MONTHLY"]) freq = iCalRecurrenceFrequenceMonthly; else if ([frequency isEqualToString:@"DAILY"]) freq = iCalRecurrenceFrequenceDaily; else if ([frequency isEqualToString:@"YEARLY"]) freq = iCalRecurrenceFrequenceYearly; else if ([frequency isEqualToString:@"HOURLY"]) freq = iCalRecurrenceFrequenceHourly; else if ([frequency isEqualToString:@"MINUTELY"]) freq = iCalRecurrenceFrequenceMinutely; else if ([frequency isEqualToString:@"SECONDLY"]) freq = iCalRecurrenceFrequenceSecondly; else freq = NSNotFound; } else freq = NSNotFound; return freq; } - (NSString *) frequencyForValue: (iCalRecurrenceFrequency) freq { NSString *frequency; switch (freq) { case iCalRecurrenceFrequenceWeekly: frequency = @"WEEKLY"; break; case iCalRecurrenceFrequenceMonthly: frequency = @"MONTHLY"; break; case iCalRecurrenceFrequenceDaily: frequency = @"DAILY"; break; case iCalRecurrenceFrequenceYearly: frequency = @"YEARLY"; break; case iCalRecurrenceFrequenceHourly: frequency = @"HOURLY"; break; case iCalRecurrenceFrequenceMinutely: frequency = @"MINUTELY"; break; case iCalRecurrenceFrequenceSecondly: frequency = @"SECONDLY"; break; default: frequency = nil; } return frequency; } /* accessors */ - (void) setFrequency: (iCalRecurrenceFrequency) _frequency { [self setNamedValue: @"freq" to: [self frequencyForValue: _frequency]]; } - (iCalRecurrenceFrequency) frequency { return [self valueForFrequency: [self namedValue: @"freq"]]; } - (void) setRepeatCount: (int) _repeatCount { [self setNamedValue: @"count" to: [NSString stringWithFormat: @"%d", _repeatCount]]; } - (int) repeatCount { return [[self namedValue: @"count"] intValue]; } - (void) setUntilDate: (NSCalendarDate *) _untilDate { [self setNamedValue: @"until" to: [_untilDate icalString]]; } - (NSCalendarDate *) untilDate { #warning handling of default timezone needs to be implemented return [[self namedValue: @"until"] asCalendarDate]; } - (void) setInterval: (NSString *) _interval { [self setNamedValue: @"interval" to: _interval]; } - (void) setCount: (NSString *) _count { [self setNamedValue: @"count" to: _count]; } - (void) setUntil: (NSString *) _until { [self setNamedValue: @"until" to: _until]; } - (void) setRepeatInterval: (int) _repeatInterval { [self setNamedValue: @"interval" to: [NSString stringWithFormat: @"%d", _repeatInterval]]; } - (int) repeatInterval { int interval; interval = [[self namedValue: @"interval"] intValue]; if (interval < 1) interval = 1; return interval; } - (void) setWkst: (NSString *) _weekStart { [self setNamedValue: @"wkst" to: _weekStart]; } #warning we also should handle the user weekstarts - (NSString *) wkst { NSString *start; start = [self namedValue: @"wkst"]; if (![start length]) start = @"MO"; return start; } - (void) setWeekStart: (iCalWeekDay) _weekStart { [self setWkst: [self iCalRepresentationForWeekDay: _weekStart]]; } - (iCalWeekDay) weekStart { return [self weekDayFromICalRepresentation: [self wkst]]; } - (void) setByDayMask: (unsigned) _mask { NSMutableArray *days; unsigned int count; unsigned char maskDays[] = { iCalWeekDaySunday, iCalWeekDayMonday, iCalWeekDayTuesday, iCalWeekDayWednesday, iCalWeekDayThursday, iCalWeekDayFriday, iCalWeekDaySaturday }; days = [NSMutableArray arrayWithCapacity: 7]; if (_mask) { for (count = 0; count < 7; count++) if (_mask & maskDays[count]) [days addObject: [self iCalRepresentationForWeekDay: maskDays[count]]]; } [self setNamedValue: @"byday" to: [days componentsJoinedByString: @","]]; } - (unsigned int) byDayMask { NSArray *days; unsigned int mask, count, max; NSString *day, *value; mask = 0; value = [self namedValue: @"byday"]; if ([value length] > 0) { days = [value componentsSeparatedByString: @","]; max = [days count]; for (count = 0; count < max; count++) { day = [days objectAtIndex: count]; mask |= [self weekDayFromICalRepresentation: day]; } } return mask; } #warning this is bad - (int) byDayOccurence1 { return 0; // return byDayOccurence1; } - (NSArray *) byMonthDay { NSArray *byMonthDay; NSString *byMonthDayStr; byMonthDayStr = [self namedValue: @"bymonthday"]; if ([byMonthDayStr length]) byMonthDay = [byMonthDayStr componentsSeparatedByString: @","]; else byMonthDay = nil; return byMonthDay; } - (BOOL) isInfinite { return !([self repeatCount] || [self untilDate]); } /* private */ - (iCalWeekDay) weekDayFromICalRepresentation: (NSString *) _day { /* be tolerant */ iCalWeekDay foundDay; unichar chars[2]; unsigned int dayLength; foundDay = 0; dayLength = [_day length]; if (dayLength > 1) { [[_day uppercaseString] getCharacters: chars range: NSMakeRange (dayLength - 2, 2)]; switch (chars[0]) { case 'M': foundDay = iCalWeekDayMonday; break; case 'W': foundDay = iCalWeekDayWednesday; break; case 'F': foundDay = iCalWeekDayFriday; break; case 'T': if (chars[1] == 'U') foundDay = iCalWeekDayTuesday; else if (chars[1] == 'H') foundDay = iCalWeekDayThursday; break; case 'S': if (chars[1] == 'A') foundDay = iCalWeekDaySaturday; else if (chars[1] == 'U') foundDay = iCalWeekDaySunday; break; } } if (!foundDay) [self errorWithFormat: @"wrong weekday representation: '%@'", _day]; return foundDay; // // 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"; // TODO: return error? } } // - (iCalWeekDay) weekDayForiCalRepre: (NSString *) _weekDay // { // iCalWeekDay day; // NSString *weekDay; // weekDay = [_weekDay uppercaseString]; // if ([weekDay isEqualToString: @"TU"]) // day = iCalWeekDayTuesday; // else if ([weekDay isEqualToString: @"WE"]) // day = iCalWeekDayWednesday; // else if ([weekDay isEqualToString: @"TH"]) // day = iCalWeekDayThursday; // else if ([weekDay isEqualToString: @"FR"]) // day = iCalWeekDayFriday; // else if ([weekDay isEqualToString: @"SA"]) // day = iCalWeekDaySaturday; // else if ([weekDay isEqualToString: @"SU"]) // day = iCalWeekDaySunday; // else // day = iCalWeekDayMonday; // return day; // } // - (NSString *) wkst // { // return [self iCalRepresentationForWeekDay:byDay.weekStart]; // } /* 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 dow, mask, day; // BOOL needsComma; // s = [NSMutableString stringWithCapacity:20]; // needsComma = NO; // mask = byDay.mask; // day = iCalWeekDayMonday; // for (dow = 0 /* Sun */; dow < 7; dow++) { // if (mask & day) { // if (needsComma) // [s appendString:@","]; // if (byDay.useOccurence) // // Note: we only support one occurrence for all currently // [s appendFormat:@"%i", byDayOccurence1]; // [s appendString:[self iCalRepresentationForWeekDay:day]]; // needsComma = YES; // } // day = (day << 1); // } // return s; // } /* parsing rrule */ // - (void) _parseRuleString: (NSString *) _rrule // { // // TODO: to be exact we would need a timezone to properly process the 'until' // // date // unsigned i, count; // NSString *pFrequency = nil; // NSString *pUntil = nil; // NSString *pCount = nil; // NSString *pByday = nil; // NSString *pBymday = nil; // NSString *pBysetpos = nil; // NSString *pInterval = nil; // for (i = 0, count = [values count]; i < count; i++) { // NSString *prop, *key, *value; // NSRange r; // NSString **vHolder = NULL; // prop = [values objectAtIndex:i]; // r = [prop rangeOfString:@"="]; // if (r.length > 0) { // key = [prop substringToIndex:r.location]; // value = [prop substringFromIndex:NSMaxRange(r)]; // } // else { // key = prop; // value = nil; // } // 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:@"bymonthday"]) { vHolder = &pBymday; 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 'i': // if ([key isEqualToString:@"interval"]) { vHolder = &pInterval; 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 setNamedValue: @"FREQ" to: pFrequency]; // else // [self errorWithFormat:@"rrule contains no frequency: '%@'", _rrule]; // [pFrequency release]; pFrequency = nil; // if (pInterval != nil) // interval = [pInterval intValue]; // [pInterval release]; pInterval = nil; // // TODO: we should parse byday in here // if (pByday != nil) [self setByday:pByday]; // [pByday release]; pByday = nil; // if (pBymday != nil) { // NSArray *t; // t = [pBymday componentsSeparatedByString:@","]; // ASSIGNCOPY(byMonthDay, t); // } // [pBymday release]; pBymday = 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"] && [pUntil length] > 8) { // [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) 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; // NSString *iCalDay; // iCalWeekDay day; // unsigned len; // unichar c0; // int occurence; // int offset; // /* reset mask */ // byDay.mask = 0; // byDay.useOccurence = 0; // byDayOccurence1 = 0; // days = [_byDayList componentsSeparatedByString:@","]; // for (i = 0, count = [days count]; i < count; i++) // { // 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)) { // occurence = [iCalDay intValue]; // offset = 1; /* skip occurence */ // while (offset < len && isdigit([iCalDay characterAtIndex:offset])) // offset++; // iCalDay = [iCalDay substringFromIndex:offset]; // if (byDay.useOccurence && (occurence != byDayOccurence1)) // { // [self errorWithFormat: // @"we only supported one occurence (occ=%i,day=%@): '%@'", // occurence, iCalDay, _byDayList]; // continue; // } // byDay.useOccurence = 1; // byDayOccurence1 = occurence; // } // else if (byDay.useOccurence) // [self errorWithFormat: // @"a byday occurence was specified on one day, but not on others" // @" (unsupported): '%@'", _byDayList]; // day = [self weekDayFromICalRepresentation:iCalDay]; // byDay.mask |= day; // } // } /* key/value coding */ - (void) handleTakeValue: (id) _value forUnboundKey: (NSString *)_key { [self warnWithFormat:@"Cannot handle unbound key: '%@'", _key]; } - (BOOL) isEqual: (id) rrule { BOOL isEqual = YES; if ([rrule isKindOfClass: [iCalRecurrenceRule class]]) { /* NSLog(@"*** iCalRecurrenceRule comparison ***"); NSLog(@"Event 1 : repeat %i, interval %i, frequency %i, until %@", [self repeatCount], [self repeatInterval], [self frequency], [self untilDate]); NSLog(@"Event 2 : repeat %i, interval %i, frequency %i, until %@", [rrule repeatCount], [rrule repeatInterval], [rrule frequency], [rrule untilDate]); */ if ([self untilDate] && [rrule untilDate]) isEqual = [[self untilDate] isEqual: [rrule untilDate]]; else if ([self untilDate] || [self untilDate]) isEqual = NO; isEqual = isEqual && [self repeatCount] == [rrule repeatCount] && [self repeatInterval] == [rrule repeatInterval] && [self frequency] == [rrule frequency]; } else isEqual = NO; return isEqual; } @end /* iCalRecurrenceRule */