]> err.no Git - sope/blob - sope-ical/NGiCal/iCalRecurrenceRule.m
fixed a Cocoa compile warning
[sope] / sope-ical / NGiCal / iCalRecurrenceRule.m
1 /*
2   Copyright (C) 2004-2005 SKYRIX Software AG
3
4   This file is part of SOPE.
5
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
9   later version.
10
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.
15
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
19   02111-1307, USA.
20 */
21
22 #include "iCalRecurrenceRule.h"
23 #include "NSCalendarDate+ICal.h"
24 #include <NGExtensions/NSString+Ext.h>
25 #include "common.h"
26
27 /*
28   freq       = rrFreq;
29   until      = rrUntil;
30   count      = rrCount;
31   interval   = rrInterval;
32   bysecond   = rrBySecondList;
33   byminute   = rrByMinuteList;
34   byhour     = rrByHourList;
35   byday      = rrByDayList;
36   bymonthday = rrByMonthDayList;
37   byyearday  = rrByYearDayList;
38   byweekno   = rrByWeekNumberList;
39   bymonth    = rrByMonthList;
40   bysetpos   = rrBySetPosList;
41   wkst       = rrWeekStart;
42 */
43
44 // TODO: private API in the header file?!
45 @interface iCalRecurrenceRule (PrivateAPI)
46
47 - (iCalWeekDay)weekDayFromICalRepresentation:(NSString *)_day;
48 - (NSString *)iCalRepresentationForWeekDay:(iCalWeekDay)_weekDay;
49 - (NSString *)freq;
50 - (NSString *)wkst;
51 - (NSString *)byDayList;
52
53 - (void)_parseRuleString:(NSString *)_rrule;
54 - (void)setRrule:(NSString *)_rrule; // TODO: weird name?
55
56 /* currently used by parser, should be removed (replace with an -init..) */
57 - (void)setByday:(NSString *)_byDayList;
58 - (void)setFreq:(NSString *)_freq;
59
60 @end
61
62 @implementation iCalRecurrenceRule
63
64 + (id)recurrenceRuleWithICalRepresentation:(NSString *)_iCalRep {
65   return [[[self alloc] initWithString:_iCalRep] autorelease];
66 }
67
68 - (id)init { /* designated initializer */
69   if ((self = [super init]) != nil) {
70     self->byDay.weekStart = iCalWeekDayMonday;
71     self->interval        = 1;
72   }
73   return self;
74 }
75
76 - (id)initWithString:(NSString *)_str {
77   if ((self = [self init]) != nil) {
78     [self setRrule:_str];
79   }
80   return self;
81 }
82
83 - (void)dealloc {
84   [self->byMonthDay release];
85   [self->untilDate release];
86   [self->rrule     release];
87   [super dealloc];
88 }
89
90
91 /* accessors */
92
93 - (void)setFrequency:(iCalRecurrenceFrequency)_frequency {
94   self->frequency = _frequency;
95 }
96 - (iCalRecurrenceFrequency)frequency {
97   return self->frequency;
98 }
99
100 - (void)setRepeatCount:(unsigned)_repeatCount {
101   self->repeatCount = _repeatCount;
102 }
103 - (unsigned)repeatCount {
104   return self->repeatCount;
105 }
106
107 - (void)setUntilDate:(NSCalendarDate *)_untilDate {
108   ASSIGNCOPY(self->untilDate, _untilDate);
109 }
110 - (NSCalendarDate *)untilDate {
111   return self->untilDate;
112 }
113
114 - (void)setRepeatInterval:(int)_repeatInterval {
115   self->interval = _repeatInterval;
116 }
117 - (int)repeatInterval {
118   return self->interval;
119 }
120
121 - (void)setWeekStart:(iCalWeekDay)_weekStart {
122   self->byDay.weekStart = _weekStart;
123 }
124 - (iCalWeekDay)weekStart {
125   return self->byDay.weekStart;
126 }
127
128 - (void)setByDayMask:(unsigned)_mask {
129   self->byDay.mask = _mask;
130 }
131 - (unsigned)byDayMask {
132   return self->byDay.mask;
133 }
134 - (int)byDayOccurence1 {
135   return self->byDayOccurence1;
136 }
137
138 - (NSArray *)byMonthDay {
139   return self->byMonthDay;
140 }
141
142 - (BOOL)isInfinite {
143   return (self->repeatCount != 0 || self->untilDate) ? NO : YES;
144 }
145
146
147 /* private */
148
149 - (iCalWeekDay)weekDayFromICalRepresentation:(NSString *)_day {
150   if ([_day length] > 1) {
151     /* be tolerant */
152     unichar c0, c1;
153     
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;
158
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;
163     }
164     if (c0 == 's' || c0 == 'S') {
165       if (c1 == 'a' || c1 == 'A') return iCalWeekDaySaturday;
166       if (c1 == 'u' || c1 == 'U') return iCalWeekDaySunday;
167     }
168   }
169   
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 */
174 }
175
176 - (NSString *)iCalRepresentationForWeekDay:(iCalWeekDay)_weekDay {
177   switch (_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?
186   }
187 }
188
189 - (NSString *)freq {
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";
198     default:
199       return @"UNDEFINED?";
200   }
201 }
202
203 - (NSString *)wkst {
204   return [self iCalRepresentationForWeekDay:self->byDay.weekStart];
205 }
206
207 /*
208   TODO:
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.
217 */
218 - (NSString *)byDayList {
219   NSMutableString *s;
220   unsigned        dow, mask, day;
221   BOOL            needsComma;
222   
223   s          = [NSMutableString stringWithCapacity:20];
224   needsComma = NO;
225   mask       = self->byDay.mask;
226   day        = iCalWeekDayMonday;
227   
228   for (dow = 0 /* Sun */; dow < 7; dow++) {
229     if (mask & day) {
230       if (needsComma)
231         [s appendString:@","];
232       
233       if (self->byDay.useOccurence)
234         // Note: we only support one occurrence for all currently
235         [s appendFormat:@"%i", self->byDayOccurence1];
236       
237       [s appendString:[self iCalRepresentationForWeekDay:day]];
238       needsComma = YES;
239     }
240     day = (day << 1);
241   }
242   return s;
243 }
244
245 /* Rule */
246
247 - (void)setRrule:(NSString *)_rrule {
248   ASSIGNCOPY(self->rrule, _rrule);
249   [self _parseRuleString:self->rrule];
250 }
251
252 /* parsing rrule */
253
254 - (void)_parseRuleString:(NSString *)_rrule {
255   // TODO: to be exact we would need a timezone to properly process the 'until'
256   //       date
257   NSArray  *props;
258   unsigned i, count;
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;
266   
267   props = [_rrule componentsSeparatedByString:@";"];
268   for (i = 0, count = [props count]; i < count; i++) {
269     NSString *prop, *key, *value;
270     NSRange  r;
271     NSString **vHolder = NULL;
272     
273     prop = [props objectAtIndex:i];
274     r    = [prop rangeOfString:@"="];
275     if (r.length > 0) {
276       key   = [prop substringToIndex:r.location];
277       value = [prop substringFromIndex:NSMaxRange(r)];
278     }
279     else {
280       key   = prop;
281       value = nil;
282     }
283     
284     key = [[key stringByTrimmingSpaces] lowercaseString];
285     if (![key isNotEmpty]) {
286       [self errorWithFormat:@"empty component in rrule: %@", _rrule];
287       continue;
288     }
289     
290     vHolder = NULL;
291     switch ([key characterAtIndex:0]) {
292     case 'b':
293       if ([key isEqualToString:@"byday"])      { vHolder = &pByday;    break; }
294       if ([key isEqualToString:@"bymonthday"]) { vHolder = &pBymday;   break; }
295       if ([key isEqualToString:@"bysetpos"])   { vHolder = &pBysetpos; break; }
296       break;
297     case 'c':
298       if ([key isEqualToString:@"count"]) { vHolder = &pCount; break; }
299       break;
300     case 'f':
301       if ([key isEqualToString:@"freq"]) { vHolder = &pFrequency; break; }
302       break;
303     case 'i':
304       if ([key isEqualToString:@"interval"]) { vHolder = &pInterval; break; }
305       break;
306     case 'u':
307       if ([key isEqualToString:@"until"]) { vHolder = &pUntil; break; }
308       break;
309     default:
310       break;
311     }
312     
313     if (vHolder != NULL) {
314       if ([*vHolder isNotEmpty])
315         [self errorWithFormat:@"more than one '%@' in: %@", key, _rrule];
316       else
317         *vHolder = [value copy];
318     }
319     else {
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];
324     }
325   }
326   
327   /* parse and fill individual values */
328   // TODO: this method should be a class method and create a new rrule object
329   
330   if ([pFrequency isNotEmpty])
331     [self setFreq:pFrequency];
332   else
333     [self errorWithFormat:@"rrule contains no frequency: '%@'", _rrule];
334   [pFrequency release]; pFrequency = nil;
335   
336   if (pInterval != nil)
337     self->interval = [pInterval intValue];
338   [pInterval release]; pInterval = nil;
339   
340   // TODO: we should parse byday in here
341   if (pByday != nil) [self setByday:pByday];
342   [pByday release]; pByday = nil;
343
344   if (pBymday != nil) {
345     NSArray *t;
346     
347     t = [pBymday componentsSeparatedByString:@","];
348     ASSIGNCOPY(self->byMonthDay, t);
349   }
350   [pBymday release]; pBymday = nil;
351   
352   if (pBysetpos != nil)
353     // TODO: implement
354     [self errorWithFormat:@"rrule contains bysetpos, unsupported: %@", _rrule];
355   [pBysetpos release]; pBysetpos = nil;
356   
357   if (pUntil != nil) {
358     NSCalendarDate *pUntilDate;
359     
360     if (pCount != nil) {
361       [self errorWithFormat:@"rrule contains 'count' AND 'until': %@", _rrule];
362       [pCount release];
363       pCount = nil;
364     }
365     
366     /*
367       The spec says:
368         "If specified as a date-time value, then it MUST be specified in an
369          UTC time format."
370       TODO: we still need some object representing a 'timeless' date.
371     */
372     if (![pUntil hasSuffix:@"Z"] && [pUntil length] > 8) {
373       [self warnWithFormat:@"'until' date has no explicit UTC marker: '%@'",
374               _rrule];
375     }
376     
377     pUntilDate = [NSCalendarDate calendarDateWithICalRepresentation:pUntil];
378     if (pUntilDate != nil)
379       [self setUntilDate:pUntilDate];
380     else {
381       [self errorWithFormat:@"could not parse 'until' in rrule: %@", 
382               _rrule];
383     }
384   }
385   [pUntil release]; pUntil = nil;
386   
387   if (pCount != nil) 
388     [self setRepeatCount:[pCount intValue]];
389   [pCount release]; pCount = nil;
390 }
391
392
393 /* properties */
394
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;
413   else {
414     [NSException raise:NSGenericException
415                  format:@"Incorrect frequency '%@' specified!", _freq];
416   }
417 }
418
419 - (void)setInterval:(NSString *)_interval {
420   self->interval = [_interval intValue];
421 }
422 - (void)setCount:(NSString *)_count {
423   self->repeatCount = [_count unsignedIntValue];
424 }
425 - (void)setUntil:(NSString *)_until {
426   NSCalendarDate *date;
427
428   date = [NSCalendarDate calendarDateWithICalRepresentation:_until];
429   ASSIGN(self->untilDate, date);
430 }
431
432 - (void)setWkst:(NSString *)_weekStart {
433   self->byDay.weekStart = [self weekDayFromICalRepresentation:_weekStart];
434 }
435
436 - (void)setByday:(NSString *)_byDayList {
437   // TODO: each day can have an associated occurence, eg:
438   //        +1MO,+2TU,-9WE
439   // TODO: this should be moved to the parser
440   NSArray  *days;
441   unsigned i, count;
442   
443   /* reset mask */
444   self->byDay.mask = 0;
445   self->byDay.useOccurence = 0;
446   self->byDayOccurence1 = 0;
447   
448   days  = [_byDayList componentsSeparatedByString:@","];
449   for (i = 0, count = [days count]; i < count; i++) {
450     NSString    *iCalDay;
451     iCalWeekDay day;
452     unsigned    len;
453     unichar     c0;
454     int         occurence;
455     
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: '%@'", 
459               _byDayList];
460       continue;
461     }
462     
463     c0 = [iCalDay characterAtIndex:0];
464     if (((c0 == '+' || c0 == '-') && len > 2) || (isdigit(c0) && len > 1)) {
465       int offset;
466       
467       occurence = [iCalDay intValue];
468       
469       offset = 1; /* skip occurence */
470       while (offset < len && isdigit([iCalDay characterAtIndex:offset]))
471         offset++;
472       
473       iCalDay = [iCalDay substringFromIndex:offset];
474       
475       if (self->byDay.useOccurence && (occurence != self->byDayOccurence1)) {
476         [self errorWithFormat:
477                 @"we only supported one occurence (occ=%i,day=%@): '%@'", 
478                 occurence, iCalDay, _byDayList];
479         continue;
480       }
481       
482       self->byDay.useOccurence = 1;
483       self->byDayOccurence1 = occurence;
484     }
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];
489     }
490     
491     day = [self weekDayFromICalRepresentation:iCalDay];
492     self->byDay.mask |= day;
493   }
494 }
495
496 /* key/value coding */
497
498 - (void)handleTakeValue:(id)_value forUnboundKey:(NSString *)_key {
499   [self warnWithFormat:@"Cannot handle unbound key: '%@'", _key];
500 }
501
502
503 /* description */
504
505 - (NSString *)iCalRepresentation {
506   NSMutableString *s;
507   
508   s = [NSMutableString stringWithCapacity:80];
509
510   [s appendString:@"FREQ="];
511   [s appendString:[self freq]];
512
513   if ([self repeatInterval] != 1)
514     [s appendFormat:@";INTERVAL=%d", [self repeatInterval]];
515   
516   if (![self isInfinite]) {
517     if ([self repeatCount] > 0) {
518       [s appendFormat:@";COUNT=%d", [self repeatCount]];
519     }
520     else {
521       [s appendString:@";UNTIL="];
522       [s appendString:[[self untilDate] icalString]];
523     }
524   }
525   if (self->byDay.weekStart != iCalWeekDayMonday) {
526     [s appendString:@";WKST="];
527     [s appendString:[self iCalRepresentationForWeekDay:self->byDay.weekStart]];
528   }
529   if (self->byDay.mask != 0) {
530     [s appendString:@";BYDAY="];
531     [s appendString:[self byDayList]];
532   }
533   return s;
534 }
535
536 - (NSString *)description {
537   return [self iCalRepresentation];
538 }
539
540 @end /* iCalRecurrenceRule */