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;
264 NSString *pInterval = nil;
266 props = [_rrule componentsSeparatedByString:@";"];
267 for (i = 0, count = [props count]; i < count; i++) {
268 NSString *prop, *key, *value;
270 NSString **vHolder = NULL;
272 prop = [props objectAtIndex:i];
273 r = [prop rangeOfString:@"="];
275 key = [prop substringToIndex:r.location];
276 value = [prop substringFromIndex:NSMaxRange(r)];
283 key = [[key stringByTrimmingSpaces] lowercaseString];
284 if (![key isNotEmpty]) {
285 [self errorWithFormat:@"empty component in rrule: %@", _rrule];
290 switch ([key characterAtIndex:0]) {
292 if ([key isEqualToString:@"byday"]) { vHolder = &pByday; break; }
293 if ([key isEqualToString:@"bymonthday"]) { vHolder = &pBymday; break; }
294 if ([key isEqualToString:@"bysetpos"]) { vHolder = &pBysetpos; break; }
297 if ([key isEqualToString:@"count"]) { vHolder = &pCount; break; }
300 if ([key isEqualToString:@"freq"]) { vHolder = &pFrequency; break; }
303 if ([key isEqualToString:@"interval"]) { vHolder = &pInterval; break; }
306 if ([key isEqualToString:@"until"]) { vHolder = &pUntil; break; }
312 if (vHolder != NULL) {
313 if ([*vHolder isNotEmpty])
314 [self errorWithFormat:@"more than one '%@' in: %@", key, _rrule];
316 *vHolder = [value copy];
319 // TODO: we should just parse known keys and put remainders into a
320 // separate dictionary
321 [self logWithFormat:@"TODO: add explicit support for key: %@", key];
322 [self takeValue:value forKey:key];
326 /* parse and fill individual values */
327 // TODO: this method should be a class method and create a new rrule object
329 if ([pFrequency isNotEmpty])
330 [self setFreq:pFrequency];
332 [self errorWithFormat:@"rrule contains no frequency: '%@'", _rrule];
333 [pFrequency release]; pFrequency = nil;
335 if (pInterval != nil)
336 self->interval = [pInterval intValue];
337 [pInterval release]; pInterval = nil;
339 // TODO: we should parse byday in here
340 if (pByday != nil) [self setByday:pByday];
341 [pByday release]; pByday = nil;
343 if (pBymday != nil) {
346 t = [pBymday componentsSeparatedByString:@","];
347 ASSIGNCOPY(self->byMonthDay, t);
349 [pBymday release]; pBymday = nil;
351 if (pBysetpos != nil)
353 [self errorWithFormat:@"rrule contains bysetpos, unsupported: %@", _rrule];
354 [pBysetpos release]; pBysetpos = nil;
357 NSCalendarDate *pUntilDate;
360 [self errorWithFormat:@"rrule contains 'count' AND 'until': %@", _rrule];
367 "If specified as a date-time value, then it MUST be specified in an
369 TODO: we still need some object representing a 'timeless' date.
371 if (![pUntil hasSuffix:@"Z"] && [pUntil length] > 8) {
372 [self warnWithFormat:@"'until' date has no explicit UTC marker: '%@'",
376 pUntilDate = [NSCalendarDate calendarDateWithICalRepresentation:pUntil];
377 if (pUntilDate != nil)
378 [self setUntilDate:pUntilDate];
380 [self errorWithFormat:@"could not parse 'until' in rrule: %@",
384 [pUntil release]; pUntil = nil;
387 [self setRepeatCount:[pCount intValue]];
388 [pCount release]; pCount = nil;
394 - (void)setFreq:(NSString *)_freq {
395 // TODO: shouldn't we preserve what the user gives us?
396 // => only used by -_parseRuleString: parser?
397 _freq = [_freq uppercaseString];
398 if ([_freq isEqualToString:@"WEEKLY"])
399 self->frequency = iCalRecurrenceFrequenceWeekly;
400 else if ([_freq isEqualToString:@"MONTHLY"])
401 self->frequency = iCalRecurrenceFrequenceMonthly;
402 else if ([_freq isEqualToString:@"DAILY"])
403 self->frequency = iCalRecurrenceFrequenceDaily;
404 else if ([_freq isEqualToString:@"YEARLY"])
405 self->frequency = iCalRecurrenceFrequenceYearly;
406 else if ([_freq isEqualToString:@"HOURLY"])
407 self->frequency = iCalRecurrenceFrequenceHourly;
408 else if ([_freq isEqualToString:@"MINUTELY"])
409 self->frequency = iCalRecurrenceFrequenceMinutely;
410 else if ([_freq isEqualToString:@"SECONDLY"])
411 self->frequency = iCalRecurrenceFrequenceSecondly;
413 [NSException raise:NSGenericException
414 format:@"Incorrect frequency '%@' specified!", _freq];
418 - (void)setInterval:(NSString *)_interval {
419 self->interval = [_interval intValue];
421 - (void)setCount:(NSString *)_count {
422 self->repeatCount = [_count unsignedIntValue];
424 - (void)setUntil:(NSString *)_until {
425 NSCalendarDate *date;
427 date = [NSCalendarDate calendarDateWithICalRepresentation:_until];
428 ASSIGN(self->untilDate, date);
431 - (void)setWkst:(NSString *)_weekStart {
432 self->byDay.weekStart = [self weekDayFromICalRepresentation:_weekStart];
435 - (void)setByday:(NSString *)_byDayList {
436 // TODO: each day can have an associated occurence, eg:
438 // TODO: this should be moved to the parser
443 self->byDay.mask = 0;
444 self->byDay.useOccurence = 0;
445 self->byDayOccurence1 = 0;
447 days = [_byDayList componentsSeparatedByString:@","];
448 for (i = 0, count = [days count]; i < count; i++) {
455 iCalDay = [days objectAtIndex:i]; // eg: MO or TU
456 if ((len = [iCalDay length]) == 0) {
457 [self errorWithFormat:@"found an empty day in byday list: '%@'",
462 c0 = [iCalDay characterAtIndex:0];
463 if (((c0 == '+' || c0 == '-') && len > 2) || (isdigit(c0) && len > 1)) {
466 occurence = [iCalDay intValue];
468 offset = 1; /* skip occurence */
469 while (offset < len && isdigit([iCalDay characterAtIndex:offset]))
472 iCalDay = [iCalDay substringFromIndex:offset];
474 if (self->byDay.useOccurence && (occurence != self->byDayOccurence1)) {
475 [self errorWithFormat:
476 @"we only supported one occurence (occ=%i,day=%@): '%@'",
477 occurence, iCalDay, _byDayList];
481 self->byDay.useOccurence = 1;
482 self->byDayOccurence1 = occurence;
484 else if (self->byDay.useOccurence) {
485 [self errorWithFormat:
486 @"a byday occurence was specified on one day, but not on others"
487 @" (unsupported): '%@'", _byDayList];
490 day = [self weekDayFromICalRepresentation:iCalDay];
491 self->byDay.mask |= day;
495 /* key/value coding */
497 - (void)handleTakeValue:(id)_value forUnboundKey:(NSString *)_key {
498 [self warnWithFormat:@"Cannot handle unbound key: '%@'", _key];
504 - (NSString *)iCalRepresentation {
507 s = [NSMutableString stringWithCapacity:80];
509 [s appendString:@"FREQ="];
510 [s appendString:[self freq]];
512 if ([self repeatInterval] != 1)
513 [s appendFormat:@";INTERVAL=%d", [self repeatInterval]];
515 if (![self isInfinite]) {
516 if ([self repeatCount] > 0) {
517 [s appendFormat:@";COUNT=%d", [self repeatCount]];
520 [s appendString:@";UNTIL="];
521 [s appendString:[[self untilDate] icalString]];
524 if (self->byDay.weekStart != iCalWeekDayMonday) {
525 [s appendString:@";WKST="];
526 [s appendString:[self iCalRepresentationForWeekDay:self->byDay.weekStart]];
528 if (self->byDay.mask != 0) {
529 [s appendString:@";BYDAY="];
530 [s appendString:[self byDayList]];
535 - (NSString *)description {
536 return [self iCalRepresentation];
539 @end /* iCalRecurrenceRule */