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 #import <Foundation/NSArray.h>
23 #import <Foundation/NSEnumerator.h>
24 #import <Foundation/NSException.h>
25 #import <NGExtensions/NSString+Ext.h>
26 #import <NGExtensions/NSObject+Logs.h>
30 #import "NSCalendarDate+NGCards.h"
31 #import "NSString+NGCards.h"
33 #import "NSCalendarDate+ICal.h"
35 #import "iCalRecurrenceRule.h"
41 interval = rrInterval;
42 bysecond = rrBySecondList;
43 byminute = rrByMinuteList;
44 byhour = rrByHourList;
46 bymonthday = rrByMonthDayList;
47 byyearday = rrByYearDayList;
48 byweekno = rrByWeekNumberList;
49 bymonth = rrByMonthList;
50 bysetpos = rrBySetPosList;
54 // TODO: private API in the header file?!
55 @interface iCalRecurrenceRule (PrivateAPI)
57 - (iCalWeekDay) weekDayFromICalRepresentation: (NSString *) _day;
58 - (NSString *) iCalRepresentationForWeekDay: (iCalWeekDay) _waeekDay;
61 - (NSString *) byDayList;
63 // - (void)_parseRuleString:(NSString *)_rrule;
65 /* currently used by parser, should be removed (replace with an -init..) */
66 - (void)setByday:(NSString *)_byDayList;
70 @implementation iCalRecurrenceRule
72 + (id) recurrenceRuleWithICalRepresentation: (NSString *) _iCalRep
74 iCalRecurrenceRule *rule;
76 rule = [self elementWithTag: @"rrule"];
77 if ([_iCalRep length] > 0)
78 [rule addValues: [_iCalRep componentsSeparatedByString: @";"]];
85 if ((self = [super init]) != nil)
87 [self setTag: @"rrule"];
93 - (id) initWithString: (NSString *) _str
95 if ((self = [self init]))
97 [self setRrule: _str];
103 - (void) setRrule: (NSString *) _rrule
105 NSEnumerator *newValues;
108 newValues = [[_rrule componentsSeparatedByString: @";"] objectEnumerator];
109 newValue = [newValues nextObject];
112 [self addValue: newValue];
113 newValue = [newValues nextObject];
117 - (iCalRecurrenceFrequency) valueForFrequency: (NSString *) value
120 iCalRecurrenceFrequency freq;
122 if ([value length] > 0)
124 frequency = [value uppercaseString];
125 if ([frequency isEqualToString:@"WEEKLY"])
126 freq = iCalRecurrenceFrequenceWeekly;
127 else if ([frequency isEqualToString:@"MONTHLY"])
128 freq = iCalRecurrenceFrequenceMonthly;
129 else if ([frequency isEqualToString:@"DAILY"])
130 freq = iCalRecurrenceFrequenceDaily;
131 else if ([frequency isEqualToString:@"YEARLY"])
132 freq = iCalRecurrenceFrequenceYearly;
133 else if ([frequency isEqualToString:@"HOURLY"])
134 freq = iCalRecurrenceFrequenceHourly;
135 else if ([frequency isEqualToString:@"MINUTELY"])
136 freq = iCalRecurrenceFrequenceMinutely;
137 else if ([frequency isEqualToString:@"SECONDLY"])
138 freq = iCalRecurrenceFrequenceSecondly;
148 - (NSString *) frequencyForValue: (iCalRecurrenceFrequency) freq
154 case iCalRecurrenceFrequenceWeekly:
155 frequency = @"WEEKLY";
157 case iCalRecurrenceFrequenceMonthly:
158 frequency = @"MONTHLY";
160 case iCalRecurrenceFrequenceDaily:
161 frequency = @"DAILY";
163 case iCalRecurrenceFrequenceYearly:
164 frequency = @"YEARLY";
166 case iCalRecurrenceFrequenceHourly:
167 frequency = @"HOURLY";
169 case iCalRecurrenceFrequenceMinutely:
170 frequency = @"MINUTELY";
172 case iCalRecurrenceFrequenceSecondly:
173 frequency = @"SECONDLY";
184 - (void) setFrequency: (iCalRecurrenceFrequency) _frequency
186 [self setNamedValue: @"freq" to: [self frequencyForValue: _frequency]];
189 - (iCalRecurrenceFrequency) frequency
191 return [self valueForFrequency: [self namedValue: @"freq"]];
194 - (void) setRepeatCount: (int) _repeatCount
196 [self setNamedValue: @"count"
197 to: [NSString stringWithFormat: @"%d", _repeatCount]];
202 return [[self namedValue: @"count"] intValue];
205 - (void) setUntilDate: (NSCalendarDate *) _untilDate
207 [self setNamedValue: @"until"
208 to: [_untilDate iCalFormattedDateTimeString]];
211 - (NSCalendarDate *) untilDate
213 #warning handling of default timezone needs to be implemented
214 return [[self namedValue: @"until"] asCalendarDate];
217 - (void) setInterval: (NSString *) _interval
219 [self setNamedValue: @"interval" to: _interval];
222 - (void) setCount: (NSString *) _count
224 [self setNamedValue: @"count" to: _count];
227 - (void) setUntil: (NSString *) _until
229 [self setNamedValue: @"until" to: _until];
232 - (void) setRepeatInterval: (int) _repeatInterval
234 [self setNamedValue: @"interval"
235 to: [NSString stringWithFormat: @"%d", _repeatInterval]];
238 - (int) repeatInterval
240 return [[self namedValue: @"interval"] intValue];
243 - (void) setWkst: (NSString *) _weekStart
245 [self setNamedValue: @"wkst" to: _weekStart];
250 return [self namedValue: @"wkst"];
253 - (void) setWeekStart: (iCalWeekDay) _weekStart
255 [self setWkst: [self iCalRepresentationForWeekDay: _weekStart]];
258 - (iCalWeekDay) weekStart
260 return [self weekDayFromICalRepresentation: [self wkst]];
263 - (void) setByDayMask: (unsigned) _mask
265 NSMutableArray *days;
267 unsigned char maskDays[] = { iCalWeekDayMonday, iCalWeekDayTuesday,
268 iCalWeekDayWednesday, iCalWeekDayThursday,
269 iCalWeekDayFriday, iCalWeekDaySaturday,
271 days = [NSMutableArray arrayWithCapacity: 7];
274 for (count = 0; count < 7; count++)
275 if (_mask & maskDays[count])
277 [self iCalRepresentationForWeekDay: maskDays[count]]];
280 [self setNamedValue: @"byday" to: [days componentsJoinedByString: @","]];
283 - (unsigned int) byDayMask
286 unsigned int mask, count, max;
287 NSString *day, *value;
291 value = [self namedValue: @"byday"];
292 if ([value length] > 0)
294 days = [value componentsSeparatedByString: @","];
296 for (count = 0; count < max; count++)
298 day = [days objectAtIndex: count];
299 day = [day substringFromIndex: [day length] - 2];
300 mask |= [self weekDayFromICalRepresentation: day];
308 - (int) byDayOccurence1
311 // return byDayOccurence1;
314 - (NSArray *) byMonthDay
316 return [[self namedValue: @"bymonthday"] componentsSeparatedByString: @","];
321 return !([self repeatCount] || [self untilDate]);
326 - (iCalWeekDay) weekDayFromICalRepresentation: (NSString *) _day
328 if ([_day length] > 1) {
332 c0 = [_day characterAtIndex:0];
333 if (c0 == 'm' || c0 == 'M') return iCalWeekDayMonday;
334 if (c0 == 'w' || c0 == 'W') return iCalWeekDayWednesday;
335 if (c0 == 'f' || c0 == 'F') return iCalWeekDayFriday;
337 c1 = [_day characterAtIndex:1];
338 if (c0 == 't' || c0 == 'T') {
339 if (c1 == 'u' || c1 == 'U') return iCalWeekDayTuesday;
340 if (c1 == 'h' || c1 == 'H') return iCalWeekDayThursday;
342 if (c0 == 's' || c0 == 'S') {
343 if (c1 == 'a' || c1 == 'A') return iCalWeekDaySaturday;
344 if (c1 == 'u' || c1 == 'U') return iCalWeekDaySunday;
349 // // TODO: do not raise but rather return an error value?
350 // [NSException raise:NSGenericException
351 // format:@"Incorrect weekDay '%@' specified!", _day];
352 // return iCalWeekDayMonday; /* keep compiler happy */
355 - (NSString *) iCalRepresentationForWeekDay: (iCalWeekDay) _weekDay
359 case iCalWeekDayMonday: return @"MO";
360 case iCalWeekDayTuesday: return @"TU";
361 case iCalWeekDayWednesday: return @"WE";
362 case iCalWeekDayThursday: return @"TH";
363 case iCalWeekDayFriday: return @"FR";
364 case iCalWeekDaySaturday: return @"SA";
365 case iCalWeekDaySunday: return @"SU";
366 default: return @"MO"; // TODO: return error?
370 // - (iCalWeekDay) weekDayForiCalRepre: (NSString *) _weekDay
373 // NSString *weekDay;
375 // weekDay = [_weekDay uppercaseString];
376 // if ([weekDay isEqualToString: @"TU"])
377 // day = iCalWeekDayTuesday;
378 // else if ([weekDay isEqualToString: @"WE"])
379 // day = iCalWeekDayWednesday;
380 // else if ([weekDay isEqualToString: @"TH"])
381 // day = iCalWeekDayThursday;
382 // else if ([weekDay isEqualToString: @"FR"])
383 // day = iCalWeekDayFriday;
384 // else if ([weekDay isEqualToString: @"SA"])
385 // day = iCalWeekDaySaturday;
386 // else if ([weekDay isEqualToString: @"SU"])
387 // day = iCalWeekDaySunday;
389 // day = iCalWeekDayMonday;
394 // - (NSString *) wkst
396 // return [self iCalRepresentationForWeekDay:byDay.weekStart];
401 Each BYDAY value can also be preceded by a positive (+n) or negative
402 (-n) integer. If present, this indicates the nth occurrence of the
403 specific day within the MONTHLY or YEARLY RRULE. For example, within
404 a MONTHLY rule, +1MO (or simply 1MO) represents the first Monday
405 within the month, whereas -1MO represents the last Monday of the
406 month. If an integer modifier is not present, it means all days of
407 this type within the specified frequency. For example, within a
408 MONTHLY rule, MO represents all Mondays within the month.
410 // - (NSString *) byDayList
412 // NSMutableString *s;
413 // unsigned dow, mask, day;
416 // s = [NSMutableString stringWithCapacity:20];
418 // mask = byDay.mask;
419 // day = iCalWeekDayMonday;
421 // for (dow = 0 /* Sun */; dow < 7; dow++) {
424 // [s appendString:@","];
426 // if (byDay.useOccurence)
427 // // Note: we only support one occurrence for all currently
428 // [s appendFormat:@"%i", byDayOccurence1];
430 // [s appendString:[self iCalRepresentationForWeekDay:day]];
441 // - (void) _parseRuleString: (NSString *) _rrule
443 // // TODO: to be exact we would need a timezone to properly process the 'until'
445 // unsigned i, count;
446 // NSString *pFrequency = nil;
447 // NSString *pUntil = nil;
448 // NSString *pCount = nil;
449 // NSString *pByday = nil;
450 // NSString *pBymday = nil;
451 // NSString *pBysetpos = nil;
452 // NSString *pInterval = nil;
454 // for (i = 0, count = [values count]; i < count; i++) {
455 // NSString *prop, *key, *value;
457 // NSString **vHolder = NULL;
459 // prop = [values objectAtIndex:i];
460 // r = [prop rangeOfString:@"="];
461 // if (r.length > 0) {
462 // key = [prop substringToIndex:r.location];
463 // value = [prop substringFromIndex:NSMaxRange(r)];
470 // key = [[key stringByTrimmingSpaces] lowercaseString];
471 // if (![key isNotEmpty]) {
472 // [self errorWithFormat:@"empty component in rrule: %@", _rrule];
477 // switch ([key characterAtIndex:0]) {
479 // if ([key isEqualToString:@"byday"]) { vHolder = &pByday; break; }
480 // if ([key isEqualToString:@"bymonthday"]) { vHolder = &pBymday; break; }
481 // if ([key isEqualToString:@"bysetpos"]) { vHolder = &pBysetpos; break; }
484 // if ([key isEqualToString:@"count"]) { vHolder = &pCount; break; }
487 // if ([key isEqualToString:@"freq"]) { vHolder = &pFrequency; break; }
490 // if ([key isEqualToString:@"interval"]) { vHolder = &pInterval; break; }
493 // if ([key isEqualToString:@"until"]) { vHolder = &pUntil; break; }
499 // if (vHolder != NULL) {
500 // if ([*vHolder isNotEmpty])
501 // [self errorWithFormat:@"more than one '%@' in: %@", key, _rrule];
503 // *vHolder = [value copy];
506 // // TODO: we should just parse known keys and put remainders into a
507 // // separate dictionary
508 // [self logWithFormat:@"TODO: add explicit support for key: %@", key];
509 // [self takeValue:value forKey:key];
513 // /* parse and fill individual values */
514 // // TODO: this method should be a class method and create a new rrule object
516 // if ([pFrequency isNotEmpty])
517 // [self setNamedValue: @"FREQ" to: pFrequency];
519 // [self errorWithFormat:@"rrule contains no frequency: '%@'", _rrule];
520 // [pFrequency release]; pFrequency = nil;
522 // if (pInterval != nil)
523 // interval = [pInterval intValue];
524 // [pInterval release]; pInterval = nil;
526 // // TODO: we should parse byday in here
527 // if (pByday != nil) [self setByday:pByday];
528 // [pByday release]; pByday = nil;
530 // if (pBymday != nil) {
533 // t = [pBymday componentsSeparatedByString:@","];
534 // ASSIGNCOPY(byMonthDay, t);
536 // [pBymday release]; pBymday = nil;
538 // if (pBysetpos != nil)
539 // // TODO: implement
540 // [self errorWithFormat:@"rrule contains bysetpos, unsupported: %@", _rrule];
541 // [pBysetpos release]; pBysetpos = nil;
543 // if (pUntil != nil) {
544 // NSCalendarDate *pUntilDate;
546 // if (pCount != nil) {
547 // [self errorWithFormat:@"rrule contains 'count' AND 'until': %@", _rrule];
554 // "If specified as a date-time value, then it MUST be specified in an
556 // TODO: we still need some object representing a 'timeless' date.
558 // if (![pUntil hasSuffix:@"Z"] && [pUntil length] > 8) {
559 // [self warnWithFormat:@"'until' date has no explicit UTC marker: '%@'",
563 // pUntilDate = [NSCalendarDate calendarDateWithICalRepresentation:pUntil];
564 // if (pUntilDate != nil)
565 // [self setUntilDate:pUntilDate];
567 // [self errorWithFormat:@"could not parse 'until' in rrule: %@",
571 // [pUntil release]; pUntil = nil;
573 // if (pCount != nil)
574 // [self setRepeatCount:[pCount intValue]];
575 // [pCount release]; pCount = nil;
580 // - (void) setByday: (NSString *) _byDayList
582 // // TODO: each day can have an associated occurence, eg:
584 // // TODO: this should be moved to the parser
586 // unsigned i, count;
587 // NSString *iCalDay;
596 // byDay.useOccurence = 0;
597 // byDayOccurence1 = 0;
599 // days = [_byDayList componentsSeparatedByString:@","];
600 // for (i = 0, count = [days count]; i < count; i++)
602 // iCalDay = [days objectAtIndex:i]; // eg: MO or TU
603 // if ((len = [iCalDay length]) == 0)
605 // [self errorWithFormat:@"found an empty day in byday list: '%@'",
610 // c0 = [iCalDay characterAtIndex:0];
611 // if (((c0 == '+' || c0 == '-') && len > 2) || (isdigit(c0) && len > 1)) {
612 // occurence = [iCalDay intValue];
614 // offset = 1; /* skip occurence */
615 // while (offset < len && isdigit([iCalDay characterAtIndex:offset]))
618 // iCalDay = [iCalDay substringFromIndex:offset];
620 // if (byDay.useOccurence && (occurence != byDayOccurence1))
622 // [self errorWithFormat:
623 // @"we only supported one occurence (occ=%i,day=%@): '%@'",
624 // occurence, iCalDay, _byDayList];
628 // byDay.useOccurence = 1;
629 // byDayOccurence1 = occurence;
631 // else if (byDay.useOccurence)
632 // [self errorWithFormat:
633 // @"a byday occurence was specified on one day, but not on others"
634 // @" (unsupported): '%@'", _byDayList];
636 // day = [self weekDayFromICalRepresentation:iCalDay];
637 // byDay.mask |= day;
641 /* key/value coding */
643 - (void) handleTakeValue: (id) _value
644 forUnboundKey: (NSString *)_key
646 [self warnWithFormat:@"Cannot handle unbound key: '%@'", _key];
649 @end /* iCalRecurrenceRule */