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->byMonthDay release];
84 [self->untilDate release];
85 [self->rrule release];
92 - (void)setFrequency:(iCalRecurrenceFrequency)_frequency {
93 self->frequency = _frequency;
95 - (iCalRecurrenceFrequency)frequency {
96 return self->frequency;
99 - (void)setRepeatCount:(unsigned)_repeatCount {
100 self->repeatCount = _repeatCount;
102 - (unsigned)repeatCount {
103 return self->repeatCount;
106 - (void)setUntilDate:(NSCalendarDate *)_untilDate {
107 ASSIGNCOPY(self->untilDate, _untilDate);
109 - (NSCalendarDate *)untilDate {
110 return self->untilDate;
113 - (void)setRepeatInterval:(int)_repeatInterval {
114 self->interval = _repeatInterval;
116 - (int)repeatInterval {
117 return self->interval;
120 - (void)setWeekStart:(iCalWeekDay)_weekStart {
121 self->byDay.weekStart = _weekStart;
123 - (iCalWeekDay)weekStart {
124 return self->byDay.weekStart;
127 - (void)setByDayMask:(unsigned)_mask {
128 self->byDay.mask = _mask;
130 - (unsigned)byDayMask {
131 return self->byDay.mask;
133 - (int)byDayOccurence1 {
134 return self->byDayOccurence1;
137 - (NSArray *)byMonthDay {
138 return self->byMonthDay;
142 return (self->repeatCount != 0 || self->untilDate) ? NO : YES;
148 - (iCalWeekDay)weekDayFromICalRepresentation:(NSString *)_day {
149 if ([_day length] > 1) {
153 c0 = [_day characterAtIndex:0];
154 if (c0 == 'm' || c0 == 'M') return iCalWeekDayMonday;
155 if (c0 == 'w' || c0 == 'W') return iCalWeekDayWednesday;
156 if (c0 == 'f' || c0 == 'F') return iCalWeekDayFriday;
158 c1 = [_day characterAtIndex:1];
159 if (c0 == 't' || c0 == 'T') {
160 if (c1 == 'u' || c1 == 'U') return iCalWeekDayTuesday;
161 if (c1 == 'h' || c1 == 'H') return iCalWeekDayThursday;
163 if (c0 == 's' || c0 == 'S') {
164 if (c1 == 'a' || c1 == 'A') return iCalWeekDaySaturday;
165 if (c1 == 'u' || c1 == 'U') return iCalWeekDaySunday;
169 // TODO: do not raise but rather return an error value?
170 [NSException raise:NSGenericException
171 format:@"Incorrect weekDay '%@' specified!", _day];
172 return iCalWeekDayMonday; /* keep compiler happy */
175 - (NSString *)iCalRepresentationForWeekDay:(iCalWeekDay)_weekDay {
177 case iCalWeekDayMonday: return @"MO";
178 case iCalWeekDayTuesday: return @"TU";
179 case iCalWeekDayWednesday: return @"WE";
180 case iCalWeekDayThursday: return @"TH";
181 case iCalWeekDayFriday: return @"FR";
182 case iCalWeekDaySaturday: return @"SA";
183 case iCalWeekDaySunday: return @"SU";
184 default: return @"MO"; // TODO: return error?
189 switch (self->frequency) {
190 case iCalRecurrenceFrequenceWeekly: return @"WEEKLY";
191 case iCalRecurrenceFrequenceMonthly: return @"MONTHLY";
192 case iCalRecurrenceFrequenceDaily: return @"DAILY";
193 case iCalRecurrenceFrequenceYearly: return @"YEARLY";
194 case iCalRecurrenceFrequenceHourly: return @"HOURLY";
195 case iCalRecurrenceFrequenceMinutely: return @"MINUTELY";
196 case iCalRecurrenceFrequenceSecondly: return @"SECONDLY";
198 return @"UNDEFINED?";
203 return [self iCalRepresentationForWeekDay:self->byDay.weekStart];
208 Each BYDAY value can also be preceded by a positive (+n) or negative
209 (-n) integer. If present, this indicates the nth occurrence of the
210 specific day within the MONTHLY or YEARLY RRULE. For example, within
211 a MONTHLY rule, +1MO (or simply 1MO) represents the first Monday
212 within the month, whereas -1MO represents the last Monday of the
213 month. If an integer modifier is not present, it means all days of
214 this type within the specified frequency. For example, within a
215 MONTHLY rule, MO represents all Mondays within the month.
217 - (NSString *)byDayList {
219 unsigned dow, mask, day;
222 s = [NSMutableString stringWithCapacity:20];
224 mask = self->byDay.mask;
225 day = iCalWeekDayMonday;
227 for (dow = 0 /* Sun */; dow < 7; dow++) {
230 [s appendString:@","];
232 if (self->byDay.useOccurence)
233 // Note: we only support one occurrence for all currently
234 [s appendFormat:@"%i", self->byDayOccurence1];
236 [s appendString:[self iCalRepresentationForWeekDay:day]];
246 - (void)setRrule:(NSString *)_rrule {
247 ASSIGNCOPY(self->rrule, _rrule);
248 [self _parseRuleString:self->rrule];
253 - (void)_parseRuleString:(NSString *)_rrule {
254 // TODO: to be exact we would need a timezone to properly process the 'until'
258 NSString *pFrequency = nil;
259 NSString *pUntil = nil;
260 NSString *pCount = nil;
261 NSString *pByday = nil;
262 NSString *pBymday = nil;
263 NSString *pBysetpos = nil;
265 props = [_rrule componentsSeparatedByString:@";"];
266 for (i = 0, count = [props count]; i < count; i++) {
267 NSString *prop, *key, *value;
269 NSString **vHolder = NULL;
271 prop = [props objectAtIndex:i];
272 r = [prop rangeOfString:@"="];
274 key = [prop substringToIndex:r.location];
275 value = [prop substringFromIndex:NSMaxRange(r)];
282 key = [[key stringByTrimmingSpaces] lowercaseString];
283 if (![key isNotEmpty]) {
284 [self errorWithFormat:@"empty component in rrule: %@", _rrule];
289 switch ([key characterAtIndex:0]) {
291 if ([key isEqualToString:@"byday"]) { vHolder = &pByday; break; }
292 if ([key isEqualToString:@"bymonthday"]) { vHolder = &pBymday; break; }
293 if ([key isEqualToString:@"bysetpos"]) { vHolder = &pBysetpos; break; }
296 if ([key isEqualToString:@"count"]) { vHolder = &pCount; break; }
299 if ([key isEqualToString:@"freq"]) { vHolder = &pFrequency; break; }
302 if ([key isEqualToString:@"until"]) { vHolder = &pUntil; break; }
308 if (vHolder != NULL) {
309 if ([*vHolder isNotEmpty])
310 [self errorWithFormat:@"more than one '%@' in: %@", key, _rrule];
312 *vHolder = [value copy];
315 // TODO: we should just parse known keys and put remainders into a
316 // separate dictionary
317 [self logWithFormat:@"TODO: add explicit support for key: %@", key];
318 [self takeValue:value forKey:key];
322 /* parse and fill individual values */
323 // TODO: this method should be a class method and create a new rrule object
325 if ([pFrequency isNotEmpty])
326 [self setFreq:pFrequency];
328 [self errorWithFormat:@"rrule contains no frequency: '%@'", _rrule];
329 [pFrequency release]; pFrequency = nil;
331 // TODO: we should parse byday in here
332 if (pByday != nil) [self setByday:pByday];
333 [pByday release]; pByday = nil;
335 if (pBymday != nil) {
338 t = [pBymday componentsSeparatedByString:@","];
339 ASSIGNCOPY(self->byMonthDay, t);
341 [pBymday release]; pBymday = nil;
343 if (pBysetpos != nil)
345 [self errorWithFormat:@"rrule contains bysetpos, unsupported: %@", _rrule];
346 [pBysetpos release]; pBysetpos = nil;
349 NSCalendarDate *pUntilDate;
352 [self errorWithFormat:@"rrule contains 'count' AND 'until': %@", _rrule];
359 "If specified as a date-time value, then it MUST be specified in an
361 TODO: we still need some object representing a 'timeless' date.
363 if (![pUntil hasSuffix:@"Z"]) {
364 [self warnWithFormat:@"'until' date has no explicit UTC marker: '%@'",
368 pUntilDate = [NSCalendarDate calendarDateWithICalRepresentation:pUntil];
369 if (pUntilDate != nil)
370 [self setUntilDate:pUntilDate];
372 [self errorWithFormat:@"could not parse 'until' in rrule: %@",
376 [pUntil release]; pUntil = nil;
379 [self setRepeatCount:[pCount intValue]];
380 [pCount release]; pCount = nil;
386 - (void)setFreq:(NSString *)_freq {
387 // TODO: shouldn't we preserve what the user gives us?
388 // => only used by -_parseRuleString: parser?
389 _freq = [_freq uppercaseString];
390 if ([_freq isEqualToString:@"WEEKLY"])
391 self->frequency = iCalRecurrenceFrequenceWeekly;
392 else if ([_freq isEqualToString:@"MONTHLY"])
393 self->frequency = iCalRecurrenceFrequenceMonthly;
394 else if ([_freq isEqualToString:@"DAILY"])
395 self->frequency = iCalRecurrenceFrequenceDaily;
396 else if ([_freq isEqualToString:@"YEARLY"])
397 self->frequency = iCalRecurrenceFrequenceYearly;
398 else if ([_freq isEqualToString:@"HOURLY"])
399 self->frequency = iCalRecurrenceFrequenceHourly;
400 else if ([_freq isEqualToString:@"MINUTELY"])
401 self->frequency = iCalRecurrenceFrequenceMinutely;
402 else if ([_freq isEqualToString:@"SECONDLY"])
403 self->frequency = iCalRecurrenceFrequenceSecondly;
405 [NSException raise:NSGenericException
406 format:@"Incorrect frequency '%@' specified!", _freq];
410 - (void)setInterval:(NSString *)_interval {
411 self->interval = [_interval intValue];
413 - (void)setCount:(NSString *)_count {
414 self->repeatCount = [_count unsignedIntValue];
416 - (void)setUntil:(NSString *)_until {
417 NSCalendarDate *date;
419 date = [NSCalendarDate calendarDateWithICalRepresentation:_until];
420 ASSIGN(self->untilDate, date);
423 - (void)setWkst:(NSString *)_weekStart {
424 self->byDay.weekStart = [self weekDayFromICalRepresentation:_weekStart];
427 - (void)setByday:(NSString *)_byDayList {
428 // TODO: each day can have an associated occurence, eg:
430 // TODO: this should be moved to the parser
435 self->byDay.mask = 0;
436 self->byDay.useOccurence = 0;
437 self->byDayOccurence1 = 0;
439 days = [_byDayList componentsSeparatedByString:@","];
440 for (i = 0, count = [days count]; i < count; i++) {
447 iCalDay = [days objectAtIndex:i]; // eg: MO or TU
448 if ((len = [iCalDay length]) == 0) {
449 [self errorWithFormat:@"found an empty day in byday list: '%@'",
454 c0 = [iCalDay characterAtIndex:0];
455 if (((c0 == '+' || c0 == '-') && len > 2) || (isdigit(c0) && len > 1)) {
458 occurence = [iCalDay intValue];
460 offset = 1; /* skip occurence */
461 while (offset < len && isdigit([iCalDay characterAtIndex:offset]))
464 iCalDay = [iCalDay substringFromIndex:offset];
466 if (self->byDay.useOccurence && (occurence != self->byDayOccurence1)) {
467 [self errorWithFormat:
468 @"we only supported one occurence (occ=%i,day=%@): '%@'",
469 occurence, iCalDay, _byDayList];
473 self->byDay.useOccurence = 1;
474 self->byDayOccurence1 = occurence;
476 else if (self->byDay.useOccurence) {
477 [self errorWithFormat:
478 @"a byday occurence was specified on one day, but not on others"
479 @" (unsupported): '%@'", _byDayList];
482 day = [self weekDayFromICalRepresentation:iCalDay];
483 self->byDay.mask |= day;
487 /* key/value coding */
489 - (void)handleTakeValue:(id)_value forUnboundKey:(NSString *)_key {
490 [self warnWithFormat:@"Cannot handle unbound key: '%@'", _key];
496 - (NSString *)iCalRepresentation {
499 s = [NSMutableString stringWithCapacity:80];
501 [s appendString:@"FREQ="];
502 [s appendString:[self freq]];
504 if ([self repeatInterval] != 1)
505 [s appendFormat:@";INTERVAL=%d", [self repeatInterval]];
507 if (![self isInfinite]) {
508 if ([self repeatCount] > 0) {
509 [s appendFormat:@";COUNT=%d", [self repeatCount]];
512 [s appendString:@";UNTIL="];
513 [s appendString:[[self untilDate] icalString]];
516 if (self->byDay.weekStart != iCalWeekDayMonday) {
517 [s appendString:@";WKST="];
518 [s appendString:[self iCalRepresentationForWeekDay:self->byDay.weekStart]];
520 if (self->byDay.mask != 0) {
521 [s appendString:@";BYDAY="];
522 [s appendString:[self byDayList]];
527 - (NSString *)description {
528 return [self iCalRepresentation];
531 @end /* iCalRecurrenceRule */