]> err.no Git - sope/blob - sope-ical/NGiCal/iCalRecurrenceRule.m
work on recurrences
[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->untilDate release];
84   [self->rrule     release];
85   [super dealloc];
86 }
87
88
89 /* accessors */
90
91 - (void)setFrequency:(iCalRecurrenceFrequency)_frequency {
92   self->frequency = _frequency;
93 }
94 - (iCalRecurrenceFrequency)frequency {
95   return self->frequency;
96 }
97
98 - (void)setRepeatCount:(unsigned)_repeatCount {
99   self->repeatCount = _repeatCount;
100 }
101 - (unsigned)repeatCount {
102   return self->repeatCount;
103 }
104
105 - (void)setUntilDate:(NSCalendarDate *)_untilDate {
106   ASSIGN(self->untilDate, _untilDate);
107 }
108 - (NSCalendarDate *)untilDate {
109   return self->untilDate;
110 }
111
112 - (void)setRepeatInterval:(int)_repeatInterval {
113   self->interval = _repeatInterval;
114 }
115 - (int)repeatInterval {
116   return self->interval;
117 }
118
119 - (void)setWeekStart:(iCalWeekDay)_weekStart {
120   self->byDay.weekStart = _weekStart;
121 }
122 - (iCalWeekDay)weekStart {
123   return self->byDay.weekStart;
124 }
125
126 - (void)setByDayMask:(unsigned)_mask {
127   self->byDay.mask = _mask;
128 }
129 - (unsigned)byDayMask {
130   return self->byDay.mask;
131 }
132 - (int)byDayOccurence1 {
133   return self->byDayOccurence1;
134 }
135
136 - (BOOL)isInfinite {
137   return (self->repeatCount != 0 || self->untilDate) ? NO : YES;
138 }
139
140
141 /* private */
142
143 - (iCalWeekDay)weekDayFromICalRepresentation:(NSString *)_day {
144   if ([_day length] > 1) {
145     /* be tolerant */
146     unichar c0, c1;
147     
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;
152
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;
157     }
158     if (c0 == 's' || c0 == 'S') {
159       if (c1 == 'a' || c1 == 'A') return iCalWeekDaySaturday;
160       if (c1 == 'u' || c1 == 'U') return iCalWeekDaySunday;
161     }
162   }
163   
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 */
168 }
169
170 - (NSString *)iCalRepresentationForWeekDay:(iCalWeekDay)_weekDay {
171   switch (_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?
180   }
181 }
182
183 - (NSString *)freq {
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";
192     default:
193       return @"UNDEFINED?";
194   }
195 }
196
197 - (NSString *)wkst {
198   return [self iCalRepresentationForWeekDay:self->byDay.weekStart];
199 }
200
201 /*
202   TODO:
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.
211 */
212 - (NSString *)byDayList {
213   NSMutableString *s;
214   unsigned        i, mask, day;
215   BOOL            needsComma;
216   
217   s          = [NSMutableString stringWithCapacity:20];
218   needsComma = NO;
219   mask       = self->byDay.mask;
220   day        = iCalWeekDayMonday;
221   
222   for (i = 0; i < 7; i++) {
223     if (mask & day) {
224       if (needsComma)
225         [s appendString:@","];
226       else if (self->byDay.useOccurence)
227         // Note: we only support one occurrence currently
228         [s appendFormat:@"%i", self->byDayOccurence1];
229       
230       [s appendString:[self iCalRepresentationForWeekDay:day]];
231       needsComma = YES;
232     }
233     day = (day << 1);
234   }
235   return s;
236 }
237
238 /* Rule */
239
240 - (void)setRrule:(NSString *)_rrule {
241   ASSIGNCOPY(self->rrule, _rrule);
242   [self _parseRuleString:self->rrule];
243 }
244
245 /* parsing rrule */
246
247 - (void)_parseRuleString:(NSString *)_rrule {
248   // TODO: to be exact we would need a timezone to properly process the 'until'
249   //       date
250   NSArray  *props;
251   unsigned i, count;
252   NSString *pFrequency = nil;
253   NSString *pUntil     = nil;
254   NSString *pCount     = nil;
255   NSString *pByday     = nil;
256   NSString *pBysetpos  = nil;
257   
258   props = [_rrule componentsSeparatedByString:@";"];
259   for (i = 0, count = [props count]; i < count; i++) {
260     NSString *prop, *key, *value;
261     NSRange  r;
262     NSString **vHolder = NULL;
263     
264     prop = [props objectAtIndex:i];
265     r    = [prop rangeOfString:@"="];
266     if (r.length > 0) {
267       key   = [prop substringToIndex:r.location];
268       value = [prop substringFromIndex:NSMaxRange(r)];
269     }
270     else {
271       key   = prop;
272       value = nil;
273     }
274     
275     key = [[key stringByTrimmingSpaces] lowercaseString];
276     if (![key isNotEmpty]) {
277       [self errorWithFormat:@"empty component in rrule: %@", _rrule];
278       continue;
279     }
280     
281     vHolder = NULL;
282     switch ([key characterAtIndex:0]) {
283     case 'b':
284       if ([key isEqualToString:@"byday"])    vHolder = &pByday;    break;
285       if ([key isEqualToString:@"bysetpos"]) vHolder = &pBysetpos; break;
286       break;
287     case 'c':
288       if ([key isEqualToString:@"count"])    vHolder = &pCount;    break;
289       break;
290     case 'f':
291       if ([key isEqualToString:@"freq"]) vHolder = &pFrequency; break;
292       break;
293     case 'u':
294       if ([key isEqualToString:@"until"]) vHolder = &pUntil; break;
295       break;
296     default:
297       break;
298     }
299     
300     if (vHolder != NULL) {
301       if ([*vHolder isNotEmpty])
302         [self errorWithFormat:@"more than one '%@' in: %@", key, _rrule];
303       else
304         *vHolder = [value copy];
305     }
306     else {
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];
311     }
312   }
313   
314   /* parse and fill individual values */
315   // TODO: this method should be a class method and create a new rrule object
316   
317   if ([pFrequency isNotEmpty])
318     [self setFreq:pFrequency];
319   else
320     [self errorWithFormat:@"rrule contains no frequency: '%@'", _rrule];
321   [pFrequency release]; pFrequency = nil;
322   
323   // TODO: we should parse byday in here
324   if (pByday != nil) [self setByday:pByday];
325   [pByday release]; pByday = nil;
326   
327   if (pBysetpos != nil)
328     // TODO: implement
329     [self errorWithFormat:@"rrule contains bysetpos, unsupported: %@", _rrule];
330   [pBysetpos release]; pBysetpos = nil;
331   
332   if (pUntil != nil) {
333     NSCalendarDate *pUntilDate;
334     
335     if (pCount != nil) {
336       [self errorWithFormat:@"rrule contains 'count' AND 'until': %@", _rrule];
337       [pCount release];
338       pCount = nil;
339     }
340     
341     /*
342       The spec says:
343         "If specified as a date-time value, then it MUST be specified in an
344          UTC time format."
345       TODO: we still need some object representing a 'timeless' date.
346     */
347     if (![pUntil hasSuffix:@"Z"]) {
348       [self warnWithFormat:@"'until' date has no explicit UTC marker: '%@'",
349               _rrule];
350     }
351     
352     pUntilDate = [NSCalendarDate calendarDateWithICalRepresentation:pUntil];
353     if (pUntilDate != nil)
354       [self setUntilDate:pUntilDate];
355     else {
356       [self errorWithFormat:@"could not parse 'until' in rrule: %@", 
357               _rrule];
358     }
359   }
360   [pUntil release]; pUntil = nil;
361   
362   if (pCount != nil) 
363     [self setRepeatCount:[pCount intValue]];
364   [pCount release]; pCount = nil;
365 }
366
367
368 /* properties */
369
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;
388   else {
389     [NSException raise:NSGenericException
390                  format:@"Incorrect frequency '%@' specified!", _freq];
391   }
392 }
393
394 - (void)setInterval:(NSString *)_interval {
395   self->interval = [_interval intValue];
396 }
397 - (void)setCount:(NSString *)_count {
398   self->repeatCount = [_count unsignedIntValue];
399 }
400 - (void)setUntil:(NSString *)_until {
401   NSCalendarDate *date;
402
403   date = [NSCalendarDate calendarDateWithICalRepresentation:_until];
404   ASSIGN(self->untilDate, date);
405 }
406
407 - (void)setWkst:(NSString *)_weekStart {
408   self->byDay.weekStart = [self weekDayFromICalRepresentation:_weekStart];
409 }
410
411 - (void)setByday:(NSString *)_byDayList {
412   // TODO: each day can have an associated occurence, eg:
413   //        +1MO,+2TU,-9WE
414   // TODO: this should be moved to the parser
415   NSArray  *days;
416   unsigned i, count;
417   
418   /* reset mask */
419   self->byDay.mask = 0;
420   self->byDay.useOccurence = 0;
421   self->byDayOccurence1 = 0;
422   
423   days  = [_byDayList componentsSeparatedByString:@","];
424   for (i = 0, count = [days count]; i < count; i++) {
425     NSString    *iCalDay;
426     iCalWeekDay day;
427     unsigned    len;
428     unichar     c0;
429     int         occurence;
430     
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: '%@'", 
434               _byDayList];
435       continue;
436     }
437     
438     c0 = [iCalDay characterAtIndex:0];
439     if (((c0 == '+' || c0 == '-') && len > 2) || (isdigit(c0) && len > 1)) {
440       int offset;
441       
442       occurence = [iCalDay intValue];
443
444       offset = 1;
445       while (offset < len && isdigit([iCalDay characterAtIndex:offset]))
446         offset++;
447       
448       iCalDay = [iCalDay substringFromIndex:offset];
449       
450       if (self->byDay.useOccurence) {
451         [self errorWithFormat:
452                 @"we only supported one occurence (occ=%i,day=%@): '%@'", 
453                 occurence, iCalDay, _byDayList];
454         continue;
455       }
456       
457       self->byDay.useOccurence = 1;
458       self->byDayOccurence1 = occurence;
459     }
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];
464     }
465     
466     day = [self weekDayFromICalRepresentation:iCalDay];
467     self->byDay.mask |= day;
468   }
469 }
470
471 /* key/value coding */
472
473 - (void)handleTakeValue:(id)_value forUnboundKey:(NSString *)_key {
474   [self warnWithFormat:@"Cannot handle unbound key: '%@'", _key];
475 }
476
477
478 /* description */
479
480 - (NSString *)iCalRepresentation {
481   NSMutableString *s;
482   
483   s = [NSMutableString stringWithCapacity:80];
484
485   [s appendString:@"FREQ="];
486   [s appendString:[self freq]];
487
488   if ([self repeatInterval] != 1)
489     [s appendFormat:@";INTERVAL=%d", [self repeatInterval]];
490   
491   if (![self isInfinite]) {
492     if ([self repeatCount] > 0) {
493       [s appendFormat:@";COUNT=%d", [self repeatCount]];
494     }
495     else {
496       [s appendString:@";UNTIL="];
497       [s appendString:[[self untilDate] icalString]];
498     }
499   }
500   if (self->byDay.weekStart != iCalWeekDayMonday) {
501     [s appendString:@";WKST="];
502     [s appendString:[self iCalRepresentationForWeekDay:self->byDay.weekStart]];
503   }
504   if (self->byDay.mask != 0) {
505     [s appendString:@";BYDAY="];
506     [s appendString:[self byDayList]];
507   }
508   return s;
509 }
510
511 - (NSString *)description {
512   return [self iCalRepresentation];
513 }
514
515 @end /* iCalRecurrenceRule */