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"
24 #include <NGExtensions/NSString+Ext.h>
31 interval = rrInterval;
32 bysecond = rrBySecondList;
33 byminute = rrByMinuteList;
34 byhour = rrByHourList;
36 bymonthday = rrByMonthDayList;
37 byyearday = rrByYearDayList;
38 byweekno = rrByWeekNumberList;
39 bymonth = rrByMonthList;
40 bysetpos = rrBySetPosList;
44 // TODO: private API in the header file?!
45 @interface iCalRecurrenceRule (PrivateAPI)
47 - (iCalWeekDay)weekDayFromICalRepresentation:(NSString *)_day;
48 - (NSString *)iCalRepresentationForWeekDay:(iCalWeekDay)_weekDay;
51 - (NSString *)byDayList;
53 - (void)_parseRuleString:(NSString *)_rrule;
54 - (void)setRrule:(NSString *)_rrule; // TODO: weird name?
56 /* currently used by parser, should be removed (replace with an -init..) */
57 - (void)setByday:(NSString *)_byDayList;
58 - (void)setFreq:(NSString *)_freq;
62 @implementation iCalRecurrenceRule
64 + (id)recurrenceRuleWithICalRepresentation:(NSString *)_iCalRep {
65 return [[[self alloc] initWithString:_iCalRep] autorelease];
68 - (id)init { /* designated initializer */
69 if ((self = [super init]) != nil) {
70 self->byDay.weekStart = iCalWeekDayMonday;
76 - (id)initWithString:(NSString *)_str {
77 if ((self = [self init]) != nil) {
84 [self->byMonthDay release];
85 [self->untilDate release];
86 [self->rrule release];
93 - (void)setFrequency:(iCalRecurrenceFrequency)_frequency {
94 self->frequency = _frequency;
96 - (iCalRecurrenceFrequency)frequency {
97 return self->frequency;
100 - (void)setRepeatCount:(unsigned)_repeatCount {
101 self->repeatCount = _repeatCount;
103 - (unsigned)repeatCount {
104 return self->repeatCount;
107 - (void)setUntilDate:(NSCalendarDate *)_untilDate {
108 ASSIGNCOPY(self->untilDate, _untilDate);
110 - (NSCalendarDate *)untilDate {
111 return self->untilDate;
114 - (void)setRepeatInterval:(int)_repeatInterval {
115 self->interval = _repeatInterval;
117 - (int)repeatInterval {
118 return self->interval;
121 - (void)setWeekStart:(iCalWeekDay)_weekStart {
122 self->byDay.weekStart = _weekStart;
124 - (iCalWeekDay)weekStart {
125 return self->byDay.weekStart;
128 - (void)setByDayMask:(unsigned)_mask {
129 self->byDay.mask = _mask;
131 - (unsigned)byDayMask {
132 return self->byDay.mask;
134 - (int)byDayOccurence1 {
135 return self->byDayOccurence1;
138 - (NSArray *)byMonthDay {
139 return self->byMonthDay;
143 return (self->repeatCount != 0 || self->untilDate) ? NO : YES;
149 - (iCalWeekDay)weekDayFromICalRepresentation:(NSString *)_day {
150 if ([_day length] > 1) {
154 c0 = [_day characterAtIndex:0];
155 if (c0 == 'm' || c0 == 'M') return iCalWeekDayMonday;
156 if (c0 == 'w' || c0 == 'W') return iCalWeekDayWednesday;
157 if (c0 == 'f' || c0 == 'F') return iCalWeekDayFriday;
159 c1 = [_day characterAtIndex:1];
160 if (c0 == 't' || c0 == 'T') {
161 if (c1 == 'u' || c1 == 'U') return iCalWeekDayTuesday;
162 if (c1 == 'h' || c1 == 'H') return iCalWeekDayThursday;
164 if (c0 == 's' || c0 == 'S') {
165 if (c1 == 'a' || c1 == 'A') return iCalWeekDaySaturday;
166 if (c1 == 'u' || c1 == 'U') return iCalWeekDaySunday;
170 // TODO: do not raise but rather return an error value?
171 [NSException raise:NSGenericException
172 format:@"Incorrect weekDay '%@' specified!", _day];
173 return iCalWeekDayMonday; /* keep compiler happy */
176 - (NSString *)iCalRepresentationForWeekDay:(iCalWeekDay)_weekDay {
178 case iCalWeekDayMonday: return @"MO";
179 case iCalWeekDayTuesday: return @"TU";
180 case iCalWeekDayWednesday: return @"WE";
181 case iCalWeekDayThursday: return @"TH";
182 case iCalWeekDayFriday: return @"FR";
183 case iCalWeekDaySaturday: return @"SA";
184 case iCalWeekDaySunday: return @"SU";
185 default: return @"MO"; // TODO: return error?
190 switch (self->frequency) {
191 case iCalRecurrenceFrequenceWeekly: return @"WEEKLY";
192 case iCalRecurrenceFrequenceMonthly: return @"MONTHLY";
193 case iCalRecurrenceFrequenceDaily: return @"DAILY";
194 case iCalRecurrenceFrequenceYearly: return @"YEARLY";
195 case iCalRecurrenceFrequenceHourly: return @"HOURLY";
196 case iCalRecurrenceFrequenceMinutely: return @"MINUTELY";
197 case iCalRecurrenceFrequenceSecondly: return @"SECONDLY";
199 return @"UNDEFINED?";
204 return [self iCalRepresentationForWeekDay:self->byDay.weekStart];
209 Each BYDAY value can also be preceded by a positive (+n) or negative
210 (-n) integer. If present, this indicates the nth occurrence of the
211 specific day within the MONTHLY or YEARLY RRULE. For example, within
212 a MONTHLY rule, +1MO (or simply 1MO) represents the first Monday
213 within the month, whereas -1MO represents the last Monday of the
214 month. If an integer modifier is not present, it means all days of
215 this type within the specified frequency. For example, within a
216 MONTHLY rule, MO represents all Mondays within the month.
218 - (NSString *)byDayList {
220 unsigned dow, mask, day;
223 s = [NSMutableString stringWithCapacity:20];
225 mask = self->byDay.mask;
226 day = iCalWeekDayMonday;
228 for (dow = 0 /* Sun */; dow < 7; dow++) {
231 [s appendString:@","];
233 if (self->byDay.useOccurence)
234 // Note: we only support one occurrence for all currently
235 [s appendFormat:@"%i", self->byDayOccurence1];
237 [s appendString:[self iCalRepresentationForWeekDay:day]];
247 - (void)setRrule:(NSString *)_rrule {
248 ASSIGNCOPY(self->rrule, _rrule);
249 [self _parseRuleString:self->rrule];
254 - (void)_parseRuleString:(NSString *)_rrule {
255 // TODO: to be exact we would need a timezone to properly process the 'until'
259 NSString *pFrequency = nil;
260 NSString *pUntil = nil;
261 NSString *pCount = nil;
262 NSString *pByday = nil;
263 NSString *pBymday = nil;
264 NSString *pBysetpos = nil;
265 NSString *pInterval = nil;
267 props = [_rrule componentsSeparatedByString:@";"];
268 for (i = 0, count = [props count]; i < count; i++) {
269 NSString *prop, *key, *value;
271 NSString **vHolder = NULL;
273 prop = [props objectAtIndex:i];
274 r = [prop rangeOfString:@"="];
276 key = [prop substringToIndex:r.location];
277 value = [prop substringFromIndex:NSMaxRange(r)];
284 key = [[key stringByTrimmingSpaces] lowercaseString];
285 if (![key isNotEmpty]) {
286 [self errorWithFormat:@"empty component in rrule: %@", _rrule];
291 switch ([key characterAtIndex:0]) {
293 if ([key isEqualToString:@"byday"]) { vHolder = &pByday; break; }
294 if ([key isEqualToString:@"bymonthday"]) { vHolder = &pBymday; break; }
295 if ([key isEqualToString:@"bysetpos"]) { vHolder = &pBysetpos; break; }
298 if ([key isEqualToString:@"count"]) { vHolder = &pCount; break; }
301 if ([key isEqualToString:@"freq"]) { vHolder = &pFrequency; break; }
304 if ([key isEqualToString:@"interval"]) { vHolder = &pInterval; break; }
307 if ([key isEqualToString:@"until"]) { vHolder = &pUntil; break; }
313 if (vHolder != NULL) {
314 if ([*vHolder isNotEmpty])
315 [self errorWithFormat:@"more than one '%@' in: %@", key, _rrule];
317 *vHolder = [value copy];
320 // TODO: we should just parse known keys and put remainders into a
321 // separate dictionary
322 [self logWithFormat:@"TODO: add explicit support for key: %@", key];
323 [self takeValue:value forKey:key];
327 /* parse and fill individual values */
328 // TODO: this method should be a class method and create a new rrule object
330 if ([pFrequency isNotEmpty])
331 [self setFreq:pFrequency];
333 [self errorWithFormat:@"rrule contains no frequency: '%@'", _rrule];
334 [pFrequency release]; pFrequency = nil;
336 if (pInterval != nil)
337 self->interval = [pInterval intValue];
338 [pInterval release]; pInterval = nil;
340 // TODO: we should parse byday in here
341 if (pByday != nil) [self setByday:pByday];
342 [pByday release]; pByday = nil;
344 if (pBymday != nil) {
347 t = [pBymday componentsSeparatedByString:@","];
348 ASSIGNCOPY(self->byMonthDay, t);
350 [pBymday release]; pBymday = nil;
352 if (pBysetpos != nil)
354 [self errorWithFormat:@"rrule contains bysetpos, unsupported: %@", _rrule];
355 [pBysetpos release]; pBysetpos = nil;
358 NSCalendarDate *pUntilDate;
361 [self errorWithFormat:@"rrule contains 'count' AND 'until': %@", _rrule];
368 "If specified as a date-time value, then it MUST be specified in an
370 TODO: we still need some object representing a 'timeless' date.
372 if (![pUntil hasSuffix:@"Z"] && [pUntil length] > 8) {
373 [self warnWithFormat:@"'until' date has no explicit UTC marker: '%@'",
377 pUntilDate = [NSCalendarDate calendarDateWithICalRepresentation:pUntil];
378 if (pUntilDate != nil)
379 [self setUntilDate:pUntilDate];
381 [self errorWithFormat:@"could not parse 'until' in rrule: %@",
385 [pUntil release]; pUntil = nil;
388 [self setRepeatCount:[pCount intValue]];
389 [pCount release]; pCount = nil;
395 - (void)setFreq:(NSString *)_freq {
396 // TODO: shouldn't we preserve what the user gives us?
397 // => only used by -_parseRuleString: parser?
398 _freq = [_freq uppercaseString];
399 if ([_freq isEqualToString:@"WEEKLY"])
400 self->frequency = iCalRecurrenceFrequenceWeekly;
401 else if ([_freq isEqualToString:@"MONTHLY"])
402 self->frequency = iCalRecurrenceFrequenceMonthly;
403 else if ([_freq isEqualToString:@"DAILY"])
404 self->frequency = iCalRecurrenceFrequenceDaily;
405 else if ([_freq isEqualToString:@"YEARLY"])
406 self->frequency = iCalRecurrenceFrequenceYearly;
407 else if ([_freq isEqualToString:@"HOURLY"])
408 self->frequency = iCalRecurrenceFrequenceHourly;
409 else if ([_freq isEqualToString:@"MINUTELY"])
410 self->frequency = iCalRecurrenceFrequenceMinutely;
411 else if ([_freq isEqualToString:@"SECONDLY"])
412 self->frequency = iCalRecurrenceFrequenceSecondly;
414 [NSException raise:NSGenericException
415 format:@"Incorrect frequency '%@' specified!", _freq];
419 - (void)setInterval:(NSString *)_interval {
420 self->interval = [_interval intValue];
422 - (void)setCount:(NSString *)_count {
423 self->repeatCount = [_count unsignedIntValue];
425 - (void)setUntil:(NSString *)_until {
426 NSCalendarDate *date;
428 date = [NSCalendarDate calendarDateWithICalRepresentation:_until];
429 ASSIGN(self->untilDate, date);
432 - (void)setWkst:(NSString *)_weekStart {
433 self->byDay.weekStart = [self weekDayFromICalRepresentation:_weekStart];
436 - (void)setByday:(NSString *)_byDayList {
437 // TODO: each day can have an associated occurence, eg:
439 // TODO: this should be moved to the parser
444 self->byDay.mask = 0;
445 self->byDay.useOccurence = 0;
446 self->byDayOccurence1 = 0;
448 days = [_byDayList componentsSeparatedByString:@","];
449 for (i = 0, count = [days count]; i < count; i++) {
456 iCalDay = [days objectAtIndex:i]; // eg: MO or TU
457 if ((len = [iCalDay length]) == 0) {
458 [self errorWithFormat:@"found an empty day in byday list: '%@'",
463 c0 = [iCalDay characterAtIndex:0];
464 if (((c0 == '+' || c0 == '-') && len > 2) || (isdigit(c0) && len > 1)) {
467 occurence = [iCalDay intValue];
469 offset = 1; /* skip occurence */
470 while (offset < len && isdigit([iCalDay characterAtIndex:offset]))
473 iCalDay = [iCalDay substringFromIndex:offset];
475 if (self->byDay.useOccurence && (occurence != self->byDayOccurence1)) {
476 [self errorWithFormat:
477 @"we only supported one occurence (occ=%i,day=%@): '%@'",
478 occurence, iCalDay, _byDayList];
482 self->byDay.useOccurence = 1;
483 self->byDayOccurence1 = occurence;
485 else if (self->byDay.useOccurence) {
486 [self errorWithFormat:
487 @"a byday occurence was specified on one day, but not on others"
488 @" (unsupported): '%@'", _byDayList];
491 day = [self weekDayFromICalRepresentation:iCalDay];
492 self->byDay.mask |= day;
496 /* key/value coding */
498 - (void)handleTakeValue:(id)_value forUnboundKey:(NSString *)_key {
499 [self warnWithFormat:@"Cannot handle unbound key: '%@'", _key];
505 - (NSString *)iCalRepresentation {
508 s = [NSMutableString stringWithCapacity:80];
510 [s appendString:@"FREQ="];
511 [s appendString:[self freq]];
513 if ([self repeatInterval] != 1)
514 [s appendFormat:@";INTERVAL=%d", [self repeatInterval]];
516 if (![self isInfinite]) {
517 if ([self repeatCount] > 0) {
518 [s appendFormat:@";COUNT=%d", [self repeatCount]];
521 [s appendString:@";UNTIL="];
522 [s appendString:[[self untilDate] icalString]];
525 if (self->byDay.weekStart != iCalWeekDayMonday) {
526 [s appendString:@";WKST="];
527 [s appendString:[self iCalRepresentationForWeekDay:self->byDay.weekStart]];
529 if (self->byDay.mask != 0) {
530 [s appendString:@";BYDAY="];
531 [s appendString:[self byDayList]];
536 - (NSString *)description {
537 return [self iCalRepresentation];
540 @end /* iCalRecurrenceRule */