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;
132 - (int)byDayOccurence1 {
133 return self->byDayOccurence1;
137 return (self->repeatCount != 0 || self->untilDate) ? NO : YES;
143 - (iCalWeekDay)weekDayFromICalRepresentation:(NSString *)_day {
144 if ([_day length] > 1) {
148 c0 = [_day characterAtIndex:0];
149 if (c0 == 'm' || c0 == 'M') return iCalWeekDayMonday;
150 if (c0 == 'w' || c0 == 'W') return iCalWeekDayWednesday;
151 if (c0 == 'f' || c0 == 'F') return iCalWeekDayFriday;
153 c1 = [_day characterAtIndex:1];
154 if (c0 == 't' || c0 == 'T') {
155 if (c1 == 'u' || c1 == 'U') return iCalWeekDayTuesday;
156 if (c1 == 'h' || c1 == 'H') return iCalWeekDayThursday;
158 if (c0 == 's' || c0 == 'S') {
159 if (c1 == 'a' || c1 == 'A') return iCalWeekDaySaturday;
160 if (c1 == 'u' || c1 == 'U') return iCalWeekDaySunday;
164 // TODO: do not raise but rather return an error value?
165 [NSException raise:NSGenericException
166 format:@"Incorrect weekDay '%@' specified!", _day];
167 return iCalWeekDayMonday; /* keep compiler happy */
170 - (NSString *)iCalRepresentationForWeekDay:(iCalWeekDay)_weekDay {
172 case iCalWeekDayMonday: return @"MO";
173 case iCalWeekDayTuesday: return @"TU";
174 case iCalWeekDayWednesday: return @"WE";
175 case iCalWeekDayThursday: return @"TH";
176 case iCalWeekDayFriday: return @"FR";
177 case iCalWeekDaySaturday: return @"SA";
178 case iCalWeekDaySunday: return @"SU";
179 default: return @"MO"; // TODO: return error?
184 switch (self->frequency) {
185 case iCalRecurrenceFrequenceWeekly: return @"WEEKLY";
186 case iCalRecurrenceFrequenceMonthly: return @"MONTHLY";
187 case iCalRecurrenceFrequenceDaily: return @"DAILY";
188 case iCalRecurrenceFrequenceYearly: return @"YEARLY";
189 case iCalRecurrenceFrequenceHourly: return @"HOURLY";
190 case iCalRecurrenceFrequenceMinutely: return @"MINUTELY";
191 case iCalRecurrenceFrequenceSecondly: return @"SECONDLY";
193 return @"UNDEFINED?";
198 return [self iCalRepresentationForWeekDay:self->byDay.weekStart];
203 Each BYDAY value can also be preceded by a positive (+n) or negative
204 (-n) integer. If present, this indicates the nth occurrence of the
205 specific day within the MONTHLY or YEARLY RRULE. For example, within
206 a MONTHLY rule, +1MO (or simply 1MO) represents the first Monday
207 within the month, whereas -1MO represents the last Monday of the
208 month. If an integer modifier is not present, it means all days of
209 this type within the specified frequency. For example, within a
210 MONTHLY rule, MO represents all Mondays within the month.
212 - (NSString *)byDayList {
214 unsigned i, mask, day;
217 s = [NSMutableString stringWithCapacity:20];
219 mask = self->byDay.mask;
220 day = iCalWeekDayMonday;
222 for (i = 0; i < 7; i++) {
225 [s appendString:@","];
226 else if (self->byDay.useOccurence)
227 // Note: we only support one occurrence currently
228 [s appendFormat:@"%i", self->byDayOccurence1];
230 [s appendString:[self iCalRepresentationForWeekDay:day]];
240 - (void)setRrule:(NSString *)_rrule {
241 ASSIGNCOPY(self->rrule, _rrule);
242 [self _parseRuleString:self->rrule];
247 - (void)_parseRuleString:(NSString *)_rrule {
248 // TODO: to be exact we would need a timezone to properly process the 'until'
252 NSString *pFrequency = nil;
253 NSString *pUntil = nil;
254 NSString *pCount = nil;
255 NSString *pByday = nil;
256 NSString *pBysetpos = nil;
258 props = [_rrule componentsSeparatedByString:@";"];
259 for (i = 0, count = [props count]; i < count; i++) {
260 NSString *prop, *key, *value;
262 NSString **vHolder = NULL;
264 prop = [props objectAtIndex:i];
265 r = [prop rangeOfString:@"="];
267 key = [prop substringToIndex:r.location];
268 value = [prop substringFromIndex:NSMaxRange(r)];
275 key = [[key stringByTrimmingSpaces] lowercaseString];
276 if (![key isNotEmpty]) {
277 [self errorWithFormat:@"empty component in rrule: %@", _rrule];
282 switch ([key characterAtIndex:0]) {
284 if ([key isEqualToString:@"byday"]) vHolder = &pByday; break;
285 if ([key isEqualToString:@"bysetpos"]) vHolder = &pBysetpos; break;
288 if ([key isEqualToString:@"count"]) vHolder = &pCount; break;
291 if ([key isEqualToString:@"freq"]) vHolder = &pFrequency; break;
294 if ([key isEqualToString:@"until"]) vHolder = &pUntil; break;
300 if (vHolder != NULL) {
301 if ([*vHolder isNotEmpty])
302 [self errorWithFormat:@"more than one '%@' in: %@", key, _rrule];
304 *vHolder = [value copy];
307 // TODO: we should just parse known keys and put remainders into a
308 // separate dictionary
309 //[self logWithFormat:@"TODO: add explicit support for key: %@", key];
310 [self takeValue:value forKey:key];
314 /* parse and fill individual values */
315 // TODO: this method should be a class method and create a new rrule object
317 if ([pFrequency isNotEmpty])
318 [self setFreq:pFrequency];
320 [self errorWithFormat:@"rrule contains no frequency: '%@'", _rrule];
321 [pFrequency release]; pFrequency = nil;
323 // TODO: we should parse byday in here
324 if (pByday != nil) [self setByday:pByday];
325 [pByday release]; pByday = nil;
327 if (pBysetpos != nil)
329 [self errorWithFormat:@"rrule contains bysetpos, unsupported: %@", _rrule];
330 [pBysetpos release]; pBysetpos = nil;
333 NSCalendarDate *pUntilDate;
336 [self errorWithFormat:@"rrule contains 'count' AND 'until': %@", _rrule];
343 "If specified as a date-time value, then it MUST be specified in an
345 TODO: we still need some object representing a 'timeless' date.
347 if (![pUntil hasSuffix:@"Z"]) {
348 [self warnWithFormat:@"'until' date has no explicit UTC marker: '%@'",
352 pUntilDate = [NSCalendarDate calendarDateWithICalRepresentation:pUntil];
353 if (pUntilDate != nil)
354 [self setUntilDate:pUntilDate];
356 [self errorWithFormat:@"could not parse 'until' in rrule: %@",
360 [pUntil release]; pUntil = nil;
363 [self setRepeatCount:[pCount intValue]];
364 [pCount release]; pCount = nil;
370 - (void)setFreq:(NSString *)_freq {
371 // TODO: shouldn't we preserve what the user gives us?
372 // => only used by -_parseRuleString: parser?
373 _freq = [_freq uppercaseString];
374 if ([_freq isEqualToString:@"WEEKLY"])
375 self->frequency = iCalRecurrenceFrequenceWeekly;
376 else if ([_freq isEqualToString:@"MONTHLY"])
377 self->frequency = iCalRecurrenceFrequenceMonthly;
378 else if ([_freq isEqualToString:@"DAILY"])
379 self->frequency = iCalRecurrenceFrequenceDaily;
380 else if ([_freq isEqualToString:@"YEARLY"])
381 self->frequency = iCalRecurrenceFrequenceYearly;
382 else if ([_freq isEqualToString:@"HOURLY"])
383 self->frequency = iCalRecurrenceFrequenceHourly;
384 else if ([_freq isEqualToString:@"MINUTELY"])
385 self->frequency = iCalRecurrenceFrequenceMinutely;
386 else if ([_freq isEqualToString:@"SECONDLY"])
387 self->frequency = iCalRecurrenceFrequenceSecondly;
389 [NSException raise:NSGenericException
390 format:@"Incorrect frequency '%@' specified!", _freq];
394 - (void)setInterval:(NSString *)_interval {
395 self->interval = [_interval intValue];
397 - (void)setCount:(NSString *)_count {
398 self->repeatCount = [_count unsignedIntValue];
400 - (void)setUntil:(NSString *)_until {
401 NSCalendarDate *date;
403 date = [NSCalendarDate calendarDateWithICalRepresentation:_until];
404 ASSIGN(self->untilDate, date);
407 - (void)setWkst:(NSString *)_weekStart {
408 self->byDay.weekStart = [self weekDayFromICalRepresentation:_weekStart];
411 - (void)setByday:(NSString *)_byDayList {
412 // TODO: each day can have an associated occurence, eg:
414 // TODO: this should be moved to the parser
419 self->byDay.mask = 0;
420 self->byDay.useOccurence = 0;
421 self->byDayOccurence1 = 0;
423 days = [_byDayList componentsSeparatedByString:@","];
424 for (i = 0, count = [days count]; i < count; i++) {
431 iCalDay = [days objectAtIndex:i]; // eg: MO or TU
432 if ((len = [iCalDay length]) == 0) {
433 [self errorWithFormat:@"found an empty day in byday list: '%@'",
438 c0 = [iCalDay characterAtIndex:0];
439 if (((c0 == '+' || c0 == '-') && len > 2) || (isdigit(c0) && len > 1)) {
442 occurence = [iCalDay intValue];
445 while (offset < len && isdigit([iCalDay characterAtIndex:offset]))
448 iCalDay = [iCalDay substringFromIndex:offset];
450 if (self->byDay.useOccurence) {
451 [self errorWithFormat:
452 @"we only supported one occurence (occ=%i,day=%@): '%@'",
453 occurence, iCalDay, _byDayList];
457 self->byDay.useOccurence = 1;
458 self->byDayOccurence1 = occurence;
460 else if (self->byDay.useOccurence) {
461 [self errorWithFormat:
462 @"a byday occurence was specified on one day, but not on others"
463 @" (unsupported): '%@'", _byDayList];
466 day = [self weekDayFromICalRepresentation:iCalDay];
467 self->byDay.mask |= day;
471 /* key/value coding */
473 - (void)handleTakeValue:(id)_value forUnboundKey:(NSString *)_key {
474 [self warnWithFormat:@"Cannot handle unbound key: '%@'", _key];
480 - (NSString *)iCalRepresentation {
483 s = [NSMutableString stringWithCapacity:80];
485 [s appendString:@"FREQ="];
486 [s appendString:[self freq]];
488 if ([self repeatInterval] != 1)
489 [s appendFormat:@";INTERVAL=%d", [self repeatInterval]];
491 if (![self isInfinite]) {
492 if ([self repeatCount] > 0) {
493 [s appendFormat:@";COUNT=%d", [self repeatCount]];
496 [s appendString:@";UNTIL="];
497 [s appendString:[[self untilDate] icalString]];
500 if (self->byDay.weekStart != iCalWeekDayMonday) {
501 [s appendString:@";WKST="];
502 [s appendString:[self iCalRepresentationForWeekDay:self->byDay.weekStart]];
504 if (self->byDay.mask != 0) {
505 [s appendString:@";BYDAY="];
506 [s appendString:[self byDayList]];
511 - (NSString *)description {
512 return [self iCalRepresentation];
515 @end /* iCalRecurrenceRule */