2 Copyright (C) 2004-2005 SKYRIX Software AG
4 This file is part of SOPE.
6 SOPE is free software; you can redistribute it and/or modify it under
7 the terms of the GNU Lesser General Public License as published by the
8 Free Software Foundation; either version 2, or (at your option) any
11 SOPE is distributed in the hope that it will be useful, but WITHOUT ANY
12 WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
14 License for more details.
16 You should have received a copy of the GNU Lesser General Public
17 License along with SOPE; see the file COPYING. If not, write to the
18 Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
22 #include "iCalRecurrenceRule.h"
23 #include "NSCalendarDate+ICal.h"
30 interval = rrInterval;
31 bysecond = rrBySecondList;
32 byminute = rrByMinuteList;
33 byhour = rrByHourList;
35 bymonthday = rrByMonthDayList;
36 byyearday = rrByYearDayList;
37 byweekno = rrByWeekNumberList;
38 bymonth = rrByMonthList;
39 bysetpos = rrBySetPosList;
43 // TODO: private API in the header file?!
44 @interface iCalRecurrenceRule (PrivateAPI)
46 - (iCalWeekDay)weekDayFromICalRepresentation:(NSString *)_day;
47 - (NSString *)iCalRepresentationForWeekDay:(iCalWeekDay)_weekDay;
50 - (NSString *)byDayList;
52 - (void)_parseRuleString:(NSString *)_rrule;
53 - (void)setRrule:(NSString *)_rrule; // TODO: weird name?
55 /* currently used by parser, should be removed (replace with an -init..) */
56 - (void)setByday:(NSString *)_byDayList;
57 - (void)setFreq:(NSString *)_freq;
61 @implementation iCalRecurrenceRule
63 + (id)recurrenceRuleWithICalRepresentation:(NSString *)_iCalRep {
64 return [[[self alloc] initWithString:_iCalRep] autorelease];
67 - (id)init { /* designated initializer */
68 if ((self = [super init]) != nil) {
69 self->byDay.weekStart = iCalWeekDayMonday;
75 - (id)initWithString:(NSString *)_str {
76 if ((self = [self init]) != nil) {
83 [self->untilDate release];
84 [self->rrule release];
91 - (void)setFrequency:(iCalRecurrenceFrequency)_frequency {
92 self->frequency = _frequency;
94 - (iCalRecurrenceFrequency)frequency {
95 return self->frequency;
98 - (void)setRepeatCount:(unsigned)_repeatCount {
99 self->repeatCount = _repeatCount;
101 - (unsigned)repeatCount {
102 return self->repeatCount;
105 - (void)setUntilDate:(NSCalendarDate *)_untilDate {
106 ASSIGN(self->untilDate, _untilDate);
108 - (NSCalendarDate *)untilDate {
109 return self->untilDate;
112 - (void)setRepeatInterval:(int)_repeatInterval {
113 self->interval = _repeatInterval;
115 - (int)repeatInterval {
116 return self->interval;
119 - (void)setWeekStart:(iCalWeekDay)_weekStart {
120 self->byDay.weekStart = _weekStart;
122 - (iCalWeekDay)weekStart {
123 return self->byDay.weekStart;
126 - (void)setByDayMask:(unsigned)_mask {
127 self->byDay.mask = _mask;
129 - (unsigned)byDayMask {
130 return self->byDay.mask;
134 return (self->repeatCount != 0 || self->untilDate) ? NO : YES;
140 - (iCalWeekDay)weekDayFromICalRepresentation:(NSString *)_day {
141 if ([_day length] > 1) {
145 c0 = [_day characterAtIndex:0];
146 if (c0 == 'm' || c0 == 'M') return iCalWeekDayMonday;
147 if (c0 == 'w' || c0 == 'W') return iCalWeekDayWednesday;
148 if (c0 == 'f' || c0 == 'F') return iCalWeekDayFriday;
150 c1 = [_day characterAtIndex:1];
151 if (c0 == 't' || c0 == 'T') {
152 if (c1 == 'u' || c1 == 'U') return iCalWeekDayTuesday;
153 if (c1 == 'h' || c1 == 'H') return iCalWeekDayThursday;
155 if (c0 == 's' || c0 == 'S') {
156 if (c1 == 'a' || c1 == 'A') return iCalWeekDaySaturday;
157 if (c1 == 'u' || c1 == 'U') return iCalWeekDaySunday;
161 // TODO: do not raise but rather return an error value?
162 [NSException raise:NSGenericException
163 format:@"Incorrect weekDay '%@' specified!", _day];
164 return iCalWeekDayMonday; /* keep compiler happy */
167 - (NSString *)iCalRepresentationForWeekDay:(iCalWeekDay)_weekDay {
169 case iCalWeekDayMonday: return @"MO";
170 case iCalWeekDayTuesday: return @"TU";
171 case iCalWeekDayWednesday: return @"WE";
172 case iCalWeekDayThursday: return @"TH";
173 case iCalWeekDayFriday: return @"FR";
174 case iCalWeekDaySaturday: return @"SA";
175 case iCalWeekDaySunday: return @"SU";
176 default: return @"MO"; // TODO: return error?
181 switch (self->frequency) {
182 case iCalRecurrenceFrequenceWeekly: return @"WEEKLY";
183 case iCalRecurrenceFrequenceMonthly: return @"MONTHLY";
184 case iCalRecurrenceFrequenceDaily: return @"DAILY";
185 case iCalRecurrenceFrequenceYearly: return @"YEARLY";
186 case iCalRecurrenceFrequenceHourly: return @"HOURLY";
187 case iCalRecurrenceFrequenceMinutely: return @"MINUTELY";
188 case iCalRecurrenceFrequenceSecondly: return @"SECONDLY";
190 return @"UNDEFINED?";
195 return [self iCalRepresentationForWeekDay:self->byDay.weekStart];
200 Each BYDAY value can also be preceded by a positive (+n) or negative
201 (-n) integer. If present, this indicates the nth occurrence of the
202 specific day within the MONTHLY or YEARLY RRULE. For example, within
203 a MONTHLY rule, +1MO (or simply 1MO) represents the first Monday
204 within the month, whereas -1MO represents the last Monday of the
205 month. If an integer modifier is not present, it means all days of
206 this type within the specified frequency. For example, within a
207 MONTHLY rule, MO represents all Mondays within the month.
209 - (NSString *)byDayList {
211 unsigned i, mask, day;
214 s = [NSMutableString stringWithCapacity:20];
216 mask = self->byDay.mask;
217 day = iCalWeekDayMonday;
219 for (i = 0; i < 7; i++) {
222 [s appendString:@","];
223 else if (self->byDay.useOccurence)
224 // Note: we only support one occurrence currently
225 [s appendFormat:@"%i", self->byDayOccurence1];
227 [s appendString:[self iCalRepresentationForWeekDay:day]];
237 - (void)setRrule:(NSString *)_rrule {
238 ASSIGNCOPY(self->rrule, _rrule);
239 [self _parseRuleString:self->rrule];
244 - (void)_parseRuleString:(NSString *)_rrule {
245 // TODO: to be exact we would need a timezone to properly process the 'until'
249 NSString *pFrequency = nil;
250 NSString *pUntil = nil;
251 NSString *pCount = nil;
252 NSString *pByday = nil;
253 NSString *pBysetpos = nil;
255 props = [_rrule componentsSeparatedByString:@";"];
256 for (i = 0, count = [props count]; i < count; i++) {
257 NSString *prop, *key, *value;
259 NSString **vHolder = NULL;
261 prop = [props objectAtIndex:i];
262 r = [prop rangeOfString:@"="];
264 key = [prop substringToIndex:r.location];
265 value = [prop substringFromIndex:NSMaxRange(r)];
272 key = [[key stringByTrimmingSpaces] lowercaseString];
273 if (![key isNotEmpty]) {
274 [self errorWithFormat:@"empty component in rrule: %@", _rrule];
279 switch ([key characterAtIndex:0]) {
281 if ([key isEqualToString:@"byday"]) vHolder = &pByday; break;
282 if ([key isEqualToString:@"bysetpos"]) vHolder = &pBysetpos; break;
285 if ([key isEqualToString:@"count"]) vHolder = &pCount; break;
288 if ([key isEqualToString:@"freq"]) vHolder = &pFrequency; break;
291 if ([key isEqualToString:@"until"]) vHolder = &pUntil; break;
297 if (vHolder != NULL) {
298 if ([*vHolder isNotEmpty])
299 [self errorWithFormat:@"more than one '%@' in: %@", key, _rrule];
301 *vHolder = [value copy];
304 // TODO: we should just parse known keys and put remainders into a
305 // separate dictionary
306 //[self logWithFormat:@"TODO: add explicit support for key: %@", key];
307 [self takeValue:value forKey:key];
311 /* parse and fill individual values */
312 // TODO: this method should be a class method and create a new rrule object
314 if ([pFrequency isNotEmpty])
315 [self setFreq:pFrequency];
317 [self errorWithFormat:@"rrule contains no frequency: '%@'", _rrule];
318 [pFrequency release]; pFrequency = nil;
320 // TODO: we should parse byday in here
321 if (pByday != nil) [self setByday:pByday];
322 [pByday release]; pByday = nil;
324 if (pBysetpos != nil)
326 [self errorWithFormat:@"rrule contains bysetpos, unsupported: %@", _rrule];
327 [pBysetpos release]; pBysetpos = nil;
330 NSCalendarDate *pUntilDate;
333 [self errorWithFormat:@"rrule contains 'count' AND 'until': %@", _rrule];
340 "If specified as a date-time value, then it MUST be specified in an
342 TODO: we still need some object representing a 'timeless' date.
344 if (![pUntil hasSuffix:@"Z"]) {
345 [self warnWithFormat:@"'until' date has no explicit UTC marker: '%@'",
349 pUntilDate = [NSCalendarDate calendarDateWithICalRepresentation:pUntil];
350 if (pUntilDate != nil)
351 [self setUntilDate:pUntilDate];
353 [self errorWithFormat:@"could not parse 'until' in rrule: %@",
357 [pUntil release]; pUntil = nil;
360 [self setRepeatCount:[pCount intValue]];
361 [pCount release]; pCount = nil;
367 - (void)setFreq:(NSString *)_freq {
368 // TODO: shouldn't we preserve what the user gives us?
369 // => only used by -_parseRuleString: parser?
370 _freq = [_freq uppercaseString];
371 if ([_freq isEqualToString:@"WEEKLY"])
372 self->frequency = iCalRecurrenceFrequenceWeekly;
373 else if ([_freq isEqualToString:@"MONTHLY"])
374 self->frequency = iCalRecurrenceFrequenceMonthly;
375 else if ([_freq isEqualToString:@"DAILY"])
376 self->frequency = iCalRecurrenceFrequenceDaily;
377 else if ([_freq isEqualToString:@"YEARLY"])
378 self->frequency = iCalRecurrenceFrequenceYearly;
379 else if ([_freq isEqualToString:@"HOURLY"])
380 self->frequency = iCalRecurrenceFrequenceHourly;
381 else if ([_freq isEqualToString:@"MINUTELY"])
382 self->frequency = iCalRecurrenceFrequenceMinutely;
383 else if ([_freq isEqualToString:@"SECONDLY"])
384 self->frequency = iCalRecurrenceFrequenceSecondly;
386 [NSException raise:NSGenericException
387 format:@"Incorrect frequency '%@' specified!", _freq];
391 - (void)setInterval:(NSString *)_interval {
392 self->interval = [_interval intValue];
394 - (void)setCount:(NSString *)_count {
395 self->repeatCount = [_count unsignedIntValue];
397 - (void)setUntil:(NSString *)_until {
398 NSCalendarDate *date;
400 date = [NSCalendarDate calendarDateWithICalRepresentation:_until];
401 ASSIGN(self->untilDate, date);
404 - (void)setWkst:(NSString *)_weekStart {
405 self->byDay.weekStart = [self weekDayFromICalRepresentation:_weekStart];
408 - (void)setByday:(NSString *)_byDayList {
409 // TODO: each day can have an associated occurence, eg:
411 // TODO: this should be moved to the parser
416 self->byDay.mask = 0;
417 self->byDay.useOccurence = 0;
418 self->byDayOccurence1 = 0;
420 days = [_byDayList componentsSeparatedByString:@","];
421 for (i = 0, count = [days count]; i < count; i++) {
428 iCalDay = [days objectAtIndex:i]; // eg: MO or TU
429 if ((len = [iCalDay length]) == 0) {
430 [self errorWithFormat:@"found an empty day in byday list: '%@'",
435 c0 = [iCalDay characterAtIndex:0];
436 if (((c0 == '+' || c0 == '-') && len > 2) || (isdigit(c0) && len > 1)) {
439 occurence = [iCalDay intValue];
442 while (offset < len && isdigit([iCalDay characterAtIndex:offset]))
445 iCalDay = [iCalDay substringFromIndex:offset];
447 if (self->byDay.useOccurence) {
448 [self errorWithFormat:
449 @"we only supported one occurence (occ=%i,day=%@): '%@'",
450 occurence, iCalDay, _byDayList];
454 self->byDay.useOccurence = 1;
455 self->byDayOccurence1 = occurence;
457 else if (self->byDay.useOccurence) {
458 [self errorWithFormat:
459 @"a byday occurence was specified on one day, but not on others"
460 @" (unsupported): '%@'", _byDayList];
463 day = [self weekDayFromICalRepresentation:iCalDay];
464 self->byDay.mask |= day;
468 /* key/value coding */
470 - (void)handleTakeValue:(id)_value forUnboundKey:(NSString *)_key {
471 [self warnWithFormat:@"Cannot handle unbound key: '%@'", _key];
477 - (NSString *)iCalRepresentation {
480 s = [NSMutableString stringWithCapacity:80];
482 [s appendString:@"FREQ="];
483 [s appendString:[self freq]];
485 if ([self repeatInterval] != 1)
486 [s appendFormat:@";INTERVAL=%d", [self repeatInterval]];
488 if (![self isInfinite]) {
489 if ([self repeatCount] > 0) {
490 [s appendFormat:@";COUNT=%d", [self repeatCount]];
493 [s appendString:@";UNTIL="];
494 [s appendString:[[self untilDate] icalString]];
497 if (self->byDay.weekStart != iCalWeekDayMonday) {
498 [s appendString:@";WKST="];
499 [s appendString:[self iCalRepresentationForWeekDay:self->byDay.weekStart]];
501 if (self->byDay.mask != 0) {
502 [s appendString:@";BYDAY="];
503 [s appendString:[self byDayList]];
508 - (NSString *)description {
509 return [self iCalRepresentation];
512 @end /* iCalRecurrenceRule */