]> err.no Git - sope/blob - sope-ical/NGiCal/iCalRecurrenceRule.m
some cycle improvs
[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   NSString *pInterval  = nil;
265   
266   props = [_rrule componentsSeparatedByString:@";"];
267   for (i = 0, count = [props count]; i < count; i++) {
268     NSString *prop, *key, *value;
269     NSRange  r;
270     NSString **vHolder = NULL;
271     
272     prop = [props objectAtIndex:i];
273     r    = [prop rangeOfString:@"="];
274     if (r.length > 0) {
275       key   = [prop substringToIndex:r.location];
276       value = [prop substringFromIndex:NSMaxRange(r)];
277     }
278     else {
279       key   = prop;
280       value = nil;
281     }
282     
283     key = [[key stringByTrimmingSpaces] lowercaseString];
284     if (![key isNotEmpty]) {
285       [self errorWithFormat:@"empty component in rrule: %@", _rrule];
286       continue;
287     }
288     
289     vHolder = NULL;
290     switch ([key characterAtIndex:0]) {
291     case 'b':
292       if ([key isEqualToString:@"byday"])      { vHolder = &pByday;    break; }
293       if ([key isEqualToString:@"bymonthday"]) { vHolder = &pBymday;   break; }
294       if ([key isEqualToString:@"bysetpos"])   { vHolder = &pBysetpos; break; }
295       break;
296     case 'c':
297       if ([key isEqualToString:@"count"]) { vHolder = &pCount; break; }
298       break;
299     case 'f':
300       if ([key isEqualToString:@"freq"]) { vHolder = &pFrequency; break; }
301       break;
302     case 'i':
303       if ([key isEqualToString:@"interval"]) { vHolder = &pInterval; break; }
304       break;
305     case 'u':
306       if ([key isEqualToString:@"until"]) { vHolder = &pUntil; break; }
307       break;
308     default:
309       break;
310     }
311     
312     if (vHolder != NULL) {
313       if ([*vHolder isNotEmpty])
314         [self errorWithFormat:@"more than one '%@' in: %@", key, _rrule];
315       else
316         *vHolder = [value copy];
317     }
318     else {
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];
323     }
324   }
325   
326   /* parse and fill individual values */
327   // TODO: this method should be a class method and create a new rrule object
328   
329   if ([pFrequency isNotEmpty])
330     [self setFreq:pFrequency];
331   else
332     [self errorWithFormat:@"rrule contains no frequency: '%@'", _rrule];
333   [pFrequency release]; pFrequency = nil;
334   
335   if (pInterval != nil)
336     self->interval = [pInterval intValue];
337   [pInterval release]; pInterval = nil;
338   
339   // TODO: we should parse byday in here
340   if (pByday != nil) [self setByday:pByday];
341   [pByday release]; pByday = nil;
342
343   if (pBymday != nil) {
344     NSArray *t;
345     
346     t = [pBymday componentsSeparatedByString:@","];
347     ASSIGNCOPY(self->byMonthDay, t);
348   }
349   [pBymday release]; pBymday = nil;
350   
351   if (pBysetpos != nil)
352     // TODO: implement
353     [self errorWithFormat:@"rrule contains bysetpos, unsupported: %@", _rrule];
354   [pBysetpos release]; pBysetpos = nil;
355   
356   if (pUntil != nil) {
357     NSCalendarDate *pUntilDate;
358     
359     if (pCount != nil) {
360       [self errorWithFormat:@"rrule contains 'count' AND 'until': %@", _rrule];
361       [pCount release];
362       pCount = nil;
363     }
364     
365     /*
366       The spec says:
367         "If specified as a date-time value, then it MUST be specified in an
368          UTC time format."
369       TODO: we still need some object representing a 'timeless' date.
370     */
371     if (![pUntil hasSuffix:@"Z"] && [pUntil length] > 8) {
372       [self warnWithFormat:@"'until' date has no explicit UTC marker: '%@'",
373               _rrule];
374     }
375     
376     pUntilDate = [NSCalendarDate calendarDateWithICalRepresentation:pUntil];
377     if (pUntilDate != nil)
378       [self setUntilDate:pUntilDate];
379     else {
380       [self errorWithFormat:@"could not parse 'until' in rrule: %@", 
381               _rrule];
382     }
383   }
384   [pUntil release]; pUntil = nil;
385   
386   if (pCount != nil) 
387     [self setRepeatCount:[pCount intValue]];
388   [pCount release]; pCount = nil;
389 }
390
391
392 /* properties */
393
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;
412   else {
413     [NSException raise:NSGenericException
414                  format:@"Incorrect frequency '%@' specified!", _freq];
415   }
416 }
417
418 - (void)setInterval:(NSString *)_interval {
419   self->interval = [_interval intValue];
420 }
421 - (void)setCount:(NSString *)_count {
422   self->repeatCount = [_count unsignedIntValue];
423 }
424 - (void)setUntil:(NSString *)_until {
425   NSCalendarDate *date;
426
427   date = [NSCalendarDate calendarDateWithICalRepresentation:_until];
428   ASSIGN(self->untilDate, date);
429 }
430
431 - (void)setWkst:(NSString *)_weekStart {
432   self->byDay.weekStart = [self weekDayFromICalRepresentation:_weekStart];
433 }
434
435 - (void)setByday:(NSString *)_byDayList {
436   // TODO: each day can have an associated occurence, eg:
437   //        +1MO,+2TU,-9WE
438   // TODO: this should be moved to the parser
439   NSArray  *days;
440   unsigned i, count;
441   
442   /* reset mask */
443   self->byDay.mask = 0;
444   self->byDay.useOccurence = 0;
445   self->byDayOccurence1 = 0;
446   
447   days  = [_byDayList componentsSeparatedByString:@","];
448   for (i = 0, count = [days count]; i < count; i++) {
449     NSString    *iCalDay;
450     iCalWeekDay day;
451     unsigned    len;
452     unichar     c0;
453     int         occurence;
454     
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: '%@'", 
458               _byDayList];
459       continue;
460     }
461     
462     c0 = [iCalDay characterAtIndex:0];
463     if (((c0 == '+' || c0 == '-') && len > 2) || (isdigit(c0) && len > 1)) {
464       int offset;
465       
466       occurence = [iCalDay intValue];
467       
468       offset = 1; /* skip occurence */
469       while (offset < len && isdigit([iCalDay characterAtIndex:offset]))
470         offset++;
471       
472       iCalDay = [iCalDay substringFromIndex:offset];
473       
474       if (self->byDay.useOccurence && (occurence != self->byDayOccurence1)) {
475         [self errorWithFormat:
476                 @"we only supported one occurence (occ=%i,day=%@): '%@'", 
477                 occurence, iCalDay, _byDayList];
478         continue;
479       }
480       
481       self->byDay.useOccurence = 1;
482       self->byDayOccurence1 = occurence;
483     }
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];
488     }
489     
490     day = [self weekDayFromICalRepresentation:iCalDay];
491     self->byDay.mask |= day;
492   }
493 }
494
495 /* key/value coding */
496
497 - (void)handleTakeValue:(id)_value forUnboundKey:(NSString *)_key {
498   [self warnWithFormat:@"Cannot handle unbound key: '%@'", _key];
499 }
500
501
502 /* description */
503
504 - (NSString *)iCalRepresentation {
505   NSMutableString *s;
506   
507   s = [NSMutableString stringWithCapacity:80];
508
509   [s appendString:@"FREQ="];
510   [s appendString:[self freq]];
511
512   if ([self repeatInterval] != 1)
513     [s appendFormat:@";INTERVAL=%d", [self repeatInterval]];
514   
515   if (![self isInfinite]) {
516     if ([self repeatCount] > 0) {
517       [s appendFormat:@";COUNT=%d", [self repeatCount]];
518     }
519     else {
520       [s appendString:@";UNTIL="];
521       [s appendString:[[self untilDate] icalString]];
522     }
523   }
524   if (self->byDay.weekStart != iCalWeekDayMonday) {
525     [s appendString:@";WKST="];
526     [s appendString:[self iCalRepresentationForWeekDay:self->byDay.weekStart]];
527   }
528   if (self->byDay.mask != 0) {
529     [s appendString:@";BYDAY="];
530     [s appendString:[self byDayList]];
531   }
532   return s;
533 }
534
535 - (NSString *)description {
536   return [self iCalRepresentation];
537 }
538
539 @end /* iCalRecurrenceRule */