]> err.no Git - sope/blob - sope-ical/NGiCal/iCalRecurrenceRule.m
3a32075cf74c35f85f0aa4171cc734601c6db5fb
[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
133 - (BOOL)isInfinite {
134   return (self->repeatCount != 0 || self->untilDate) ? NO : YES;
135 }
136
137
138 /* private */
139
140 - (iCalWeekDay)weekDayFromICalRepresentation:(NSString *)_day {
141   if ([_day length] > 1) {
142     /* be tolerant */
143     unichar c0, c1;
144     
145     c0 = [_day characterAtIndex:0];
146     if (c0 == 'm' || c0 == 'M') return iCalWeekDayMonday;
147     if (c0 == 'w' || c0 == 'W') return iCalWeekDayWednesday;
148     if (c0 == 'f' || c0 == 'F') return iCalWeekDayFriday;
149
150     c1 = [_day characterAtIndex:1];
151     if (c0 == 't' || c0 == 'T') {
152       if (c1 == 'u' || c1 == 'U') return iCalWeekDayTuesday;
153       if (c1 == 'h' || c1 == 'H') return iCalWeekDayThursday;
154     }
155     if (c0 == 's' || c0 == 'S') {
156       if (c1 == 'a' || c1 == 'A') return iCalWeekDaySaturday;
157       if (c1 == 'u' || c1 == 'U') return iCalWeekDaySunday;
158     }
159   }
160   
161   // TODO: do not raise but rather return an error value?
162   [NSException raise:NSGenericException
163                format:@"Incorrect weekDay '%@' specified!", _day];
164   return iCalWeekDayMonday; /* keep compiler happy */
165 }
166
167 - (NSString *)iCalRepresentationForWeekDay:(iCalWeekDay)_weekDay {
168   switch (_weekDay) {
169     case iCalWeekDayMonday:    return @"MO";
170     case iCalWeekDayTuesday:   return @"TU";
171     case iCalWeekDayWednesday: return @"WE";
172     case iCalWeekDayThursday:  return @"TH";
173     case iCalWeekDayFriday:    return @"FR";
174     case iCalWeekDaySaturday:  return @"SA";
175     case iCalWeekDaySunday:    return @"SU";
176     default:                   return @"MO"; // TODO: return error?
177   }
178 }
179
180 - (NSString *)freq {
181   switch (self->frequency) {
182     case iCalRecurrenceFrequenceWeekly:   return @"WEEKLY";
183     case iCalRecurrenceFrequenceMonthly:  return @"MONTHLY";
184     case iCalRecurrenceFrequenceDaily:    return @"DAILY";
185     case iCalRecurrenceFrequenceYearly:   return @"YEARLY";
186     case iCalRecurrenceFrequenceHourly:   return @"HOURLY";
187     case iCalRecurrenceFrequenceMinutely: return @"MINUTELY";
188     case iCalRecurrenceFrequenceSecondly: return @"SECONDLY";
189     default:
190       return @"UNDEFINED?";
191   }
192 }
193
194 - (NSString *)wkst {
195   return [self iCalRepresentationForWeekDay:self->byDay.weekStart];
196 }
197
198 /*
199   TODO:
200   Each BYDAY value can also be preceded by a positive (+n) or negative
201   (-n) integer. If present, this indicates the nth occurrence of the
202   specific day within the MONTHLY or YEARLY RRULE. For example, within
203   a MONTHLY rule, +1MO (or simply 1MO) represents the first Monday
204   within the month, whereas -1MO represents the last Monday of the
205   month. If an integer modifier is not present, it means all days of
206   this type within the specified frequency. For example, within a
207   MONTHLY rule, MO represents all Mondays within the month.
208 */
209 - (NSString *)byDayList {
210   NSMutableString *s;
211   unsigned        i, mask, day;
212   BOOL            needsComma;
213   
214   s          = [NSMutableString stringWithCapacity:20];
215   needsComma = NO;
216   mask       = self->byDay.mask;
217   day        = iCalWeekDayMonday;
218   
219   for (i = 0; i < 7; i++) {
220     if (mask & day) {
221       if (needsComma)
222         [s appendString:@","];
223       else if (self->byDay.useOccurence)
224         // Note: we only support one occurrence currently
225         [s appendFormat:@"%i", self->byDayOccurence1];
226       
227       [s appendString:[self iCalRepresentationForWeekDay:day]];
228       needsComma = YES;
229     }
230     day = (day << 1);
231   }
232   return s;
233 }
234
235 /* Rule */
236
237 - (void)setRrule:(NSString *)_rrule {
238   ASSIGNCOPY(self->rrule, _rrule);
239   [self _parseRuleString:self->rrule];
240 }
241
242 /* parsing rrule */
243
244 - (void)_parseRuleString:(NSString *)_rrule {
245   // TODO: to be exact we would need a timezone to properly process the 'until'
246   //       date
247   NSArray  *props;
248   unsigned i, count;
249   NSString *pFrequency = nil;
250   NSString *pUntil     = nil;
251   NSString *pCount     = nil;
252   NSString *pByday     = nil;
253   NSString *pBysetpos  = nil;
254   
255   props = [_rrule componentsSeparatedByString:@";"];
256   for (i = 0, count = [props count]; i < count; i++) {
257     NSString *prop, *key, *value;
258     NSRange  r;
259     NSString **vHolder = NULL;
260     
261     prop = [props objectAtIndex:i];
262     r    = [prop rangeOfString:@"="];
263     if (r.length > 0) {
264       key   = [prop substringToIndex:r.location];
265       value = [prop substringFromIndex:NSMaxRange(r)];
266     }
267     else {
268       key   = prop;
269       value = nil;
270     }
271     
272     key = [[key stringByTrimmingSpaces] lowercaseString];
273     if (![key isNotEmpty]) {
274       [self errorWithFormat:@"empty component in rrule: %@", _rrule];
275       continue;
276     }
277     
278     vHolder = NULL;
279     switch ([key characterAtIndex:0]) {
280     case 'b':
281       if ([key isEqualToString:@"byday"])    vHolder = &pByday;    break;
282       if ([key isEqualToString:@"bysetpos"]) vHolder = &pBysetpos; break;
283       break;
284     case 'c':
285       if ([key isEqualToString:@"count"])    vHolder = &pCount;    break;
286       break;
287     case 'f':
288       if ([key isEqualToString:@"freq"]) vHolder = &pFrequency; break;
289       break;
290     case 'u':
291       if ([key isEqualToString:@"until"]) vHolder = &pUntil; break;
292       break;
293     default:
294       break;
295     }
296     
297     if (vHolder != NULL) {
298       if ([*vHolder isNotEmpty])
299         [self errorWithFormat:@"more than one '%@' in: %@", key, _rrule];
300       else
301         *vHolder = [value copy];
302     }
303     else {
304       // TODO: we should just parse known keys and put remainders into a
305       //       separate dictionary
306       //[self logWithFormat:@"TODO: add explicit support for key: %@", key];
307       [self takeValue:value forKey:key];
308     }
309   }
310   
311   /* parse and fill individual values */
312   // TODO: this method should be a class method and create a new rrule object
313   
314   if ([pFrequency isNotEmpty])
315     [self setFreq:pFrequency];
316   else
317     [self errorWithFormat:@"rrule contains no frequency: '%@'", _rrule];
318   [pFrequency release]; pFrequency = nil;
319   
320   // TODO: we should parse byday in here
321   if (pByday != nil) [self setByday:pByday];
322   [pByday release]; pByday = nil;
323   
324   if (pBysetpos != nil)
325     // TODO: implement
326     [self errorWithFormat:@"rrule contains bysetpos, unsupported: %@", _rrule];
327   [pBysetpos release]; pBysetpos = nil;
328   
329   if (pUntil != nil) {
330     NSCalendarDate *pUntilDate;
331     
332     if (pCount != nil) {
333       [self errorWithFormat:@"rrule contains 'count' AND 'until': %@", _rrule];
334       [pCount release];
335       pCount = nil;
336     }
337     
338     /*
339       The spec says:
340         "If specified as a date-time value, then it MUST be specified in an
341          UTC time format."
342       TODO: we still need some object representing a 'timeless' date.
343     */
344     if (![pUntil hasSuffix:@"Z"]) {
345       [self warnWithFormat:@"'until' date has no explicit UTC marker: '%@'",
346               _rrule];
347     }
348     
349     pUntilDate = [NSCalendarDate calendarDateWithICalRepresentation:pUntil];
350     if (pUntilDate != nil)
351       [self setUntilDate:pUntilDate];
352     else {
353       [self errorWithFormat:@"could not parse 'until' in rrule: %@", 
354               _rrule];
355     }
356   }
357   [pUntil release]; pUntil = nil;
358   
359   if (pCount != nil) 
360     [self setRepeatCount:[pCount intValue]];
361   [pCount release]; pCount = nil;
362 }
363
364
365 /* properties */
366
367 - (void)setFreq:(NSString *)_freq {
368   // TODO: shouldn't we preserve what the user gives us?
369   // => only used by -_parseRuleString: parser?
370   _freq = [_freq uppercaseString];
371   if ([_freq isEqualToString:@"WEEKLY"])
372     self->frequency = iCalRecurrenceFrequenceWeekly;
373   else if ([_freq isEqualToString:@"MONTHLY"])
374     self->frequency = iCalRecurrenceFrequenceMonthly;
375   else if ([_freq isEqualToString:@"DAILY"])
376     self->frequency = iCalRecurrenceFrequenceDaily;
377   else if ([_freq isEqualToString:@"YEARLY"])
378     self->frequency = iCalRecurrenceFrequenceYearly;
379   else if ([_freq isEqualToString:@"HOURLY"])
380     self->frequency = iCalRecurrenceFrequenceHourly;
381   else if ([_freq isEqualToString:@"MINUTELY"])
382     self->frequency = iCalRecurrenceFrequenceMinutely;
383   else if ([_freq isEqualToString:@"SECONDLY"])
384     self->frequency = iCalRecurrenceFrequenceSecondly;
385   else {
386     [NSException raise:NSGenericException
387                  format:@"Incorrect frequency '%@' specified!", _freq];
388   }
389 }
390
391 - (void)setInterval:(NSString *)_interval {
392   self->interval = [_interval intValue];
393 }
394 - (void)setCount:(NSString *)_count {
395   self->repeatCount = [_count unsignedIntValue];
396 }
397 - (void)setUntil:(NSString *)_until {
398   NSCalendarDate *date;
399
400   date = [NSCalendarDate calendarDateWithICalRepresentation:_until];
401   ASSIGN(self->untilDate, date);
402 }
403
404 - (void)setWkst:(NSString *)_weekStart {
405   self->byDay.weekStart = [self weekDayFromICalRepresentation:_weekStart];
406 }
407
408 - (void)setByday:(NSString *)_byDayList {
409   // TODO: each day can have an associated occurence, eg:
410   //        +1MO,+2TU,-9WE
411   // TODO: this should be moved to the parser
412   NSArray  *days;
413   unsigned i, count;
414   
415   /* reset mask */
416   self->byDay.mask = 0;
417   self->byDay.useOccurence = 0;
418   self->byDayOccurence1 = 0;
419   
420   days  = [_byDayList componentsSeparatedByString:@","];
421   for (i = 0, count = [days count]; i < count; i++) {
422     NSString    *iCalDay;
423     iCalWeekDay day;
424     unsigned    len;
425     unichar     c0;
426     int         occurence;
427     
428     iCalDay = [days objectAtIndex:i]; // eg: MO or TU
429     if ((len = [iCalDay length]) == 0) {
430       [self errorWithFormat:@"found an empty day in byday list: '%@'", 
431               _byDayList];
432       continue;
433     }
434     
435     c0 = [iCalDay characterAtIndex:0];
436     if (((c0 == '+' || c0 == '-') && len > 2) || (isdigit(c0) && len > 1)) {
437       int offset;
438       
439       occurence = [iCalDay intValue];
440
441       offset = 1;
442       while (offset < len && isdigit([iCalDay characterAtIndex:offset]))
443         offset++;
444       
445       iCalDay = [iCalDay substringFromIndex:offset];
446       
447       if (self->byDay.useOccurence) {
448         [self errorWithFormat:
449                 @"we only supported one occurence (occ=%i,day=%@): '%@'", 
450                 occurence, iCalDay, _byDayList];
451         continue;
452       }
453       
454       self->byDay.useOccurence = 1;
455       self->byDayOccurence1 = occurence;
456     }
457     else if (self->byDay.useOccurence) {
458       [self errorWithFormat:
459               @"a byday occurence was specified on one day, but not on others"
460               @" (unsupported): '%@'", _byDayList];
461     }
462     
463     day = [self weekDayFromICalRepresentation:iCalDay];
464     self->byDay.mask |= day;
465   }
466 }
467
468 /* key/value coding */
469
470 - (void)handleTakeValue:(id)_value forUnboundKey:(NSString *)_key {
471   [self warnWithFormat:@"Cannot handle unbound key: '%@'", _key];
472 }
473
474
475 /* description */
476
477 - (NSString *)iCalRepresentation {
478   NSMutableString *s;
479   
480   s = [NSMutableString stringWithCapacity:80];
481
482   [s appendString:@"FREQ="];
483   [s appendString:[self freq]];
484
485   if ([self repeatInterval] != 1)
486     [s appendFormat:@";INTERVAL=%d", [self repeatInterval]];
487   
488   if (![self isInfinite]) {
489     if ([self repeatCount] > 0) {
490       [s appendFormat:@";COUNT=%d", [self repeatCount]];
491     }
492     else {
493       [s appendString:@";UNTIL="];
494       [s appendString:[[self untilDate] icalString]];
495     }
496   }
497   if (self->byDay.weekStart != iCalWeekDayMonday) {
498     [s appendString:@";WKST="];
499     [s appendString:[self iCalRepresentationForWeekDay:self->byDay.weekStart]];
500   }
501   if (self->byDay.mask != 0) {
502     [s appendString:@";BYDAY="];
503     [s appendString:[self byDayList]];
504   }
505   return s;
506 }
507
508 - (NSString *)description {
509   return [self iCalRepresentation];
510 }
511
512 @end /* iCalRecurrenceRule */