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