]> err.no Git - scalable-opengroupware.org/blob - SOPE/NGCards/iCalRecurrenceRule.m
git-svn-id: http://svn.opengroupware.org/SOGo/inverse/trunk@1178 d1b88da0-ebda-0310...
[scalable-opengroupware.org] / SOPE / NGCards / 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 #import <Foundation/NSArray.h>
23 #import <Foundation/NSEnumerator.h>
24 #import <Foundation/NSException.h>
25 #import <NGExtensions/NSString+Ext.h>
26 #import <NGExtensions/NSObject+Logs.h>
27
28 #import <ctype.h>
29
30 #import "NSCalendarDate+NGCards.h"
31 #import "NSString+NGCards.h"
32
33 #import "NSCalendarDate+ICal.h"
34
35 #import "iCalRecurrenceRule.h"
36
37 /*
38   freq       = rrFreq;
39   until      = rrUntil;
40   count      = rrCount;
41   interval   = rrInterval;
42   bysecond   = rrBySecondList;
43   byminute   = rrByMinuteList;
44   byhour     = rrByHourList;
45   byday      = rrByDayList;
46   bymonthday = rrByMonthDayList;
47   byyearday  = rrByYearDayList;
48   byweekno   = rrByWeekNumberList;
49   bymonth    = rrByMonthList;
50   bysetpos   = rrBySetPosList;
51   wkst       = rrWeekStart;
52 */
53
54 // TODO: private API in the header file?!
55 @interface iCalRecurrenceRule (PrivateAPI)
56
57 - (iCalWeekDay) weekDayFromICalRepresentation: (NSString *) _day;
58 - (NSString *) iCalRepresentationForWeekDay: (iCalWeekDay) _waeekDay;
59 - (NSString *) freq;
60 - (NSString *) wkst;
61 - (NSString *) byDayList;
62
63 // - (void)_parseRuleString:(NSString *)_rrule;
64
65 /* currently used by parser, should be removed (replace with an -init..) */
66 - (void)setByday:(NSString *)_byDayList;
67
68 @end
69
70 @implementation iCalRecurrenceRule
71
72 + (id) recurrenceRuleWithICalRepresentation: (NSString *) _iCalRep
73 {
74   iCalRecurrenceRule *rule;
75
76   rule = [self elementWithTag: @"rrule"];
77   if ([_iCalRep length] > 0)
78     [rule addValues: [_iCalRep componentsSeparatedByString: @";"]];
79
80   return rule;
81 }
82
83 - (id) init
84 {
85   if ((self = [super init]) != nil)
86     {
87       [self setTag: @"rrule"];
88     }
89
90   return self;
91 }
92
93 - (id) initWithString: (NSString *) _str
94 {
95   if ((self = [self init]))
96     {
97       [self setRrule: _str];
98     }
99
100   return self;
101 }
102
103 - (void) setRrule: (NSString *) _rrule
104 {
105   NSEnumerator *newValues;
106   NSString *newValue;
107
108   newValues = [[_rrule componentsSeparatedByString: @";"] objectEnumerator];
109   newValue = [newValues nextObject];
110   while (newValue)
111     {
112       [self addValue: newValue];
113       newValue = [newValues nextObject];
114     }
115 }
116
117 - (iCalRecurrenceFrequency) valueForFrequency: (NSString *) value
118 {
119   NSString *frequency;
120   iCalRecurrenceFrequency freq;
121
122   if ([value length] > 0)
123     {
124       frequency = [value uppercaseString];
125       if ([frequency isEqualToString:@"WEEKLY"])
126         freq = iCalRecurrenceFrequenceWeekly;
127       else if ([frequency isEqualToString:@"MONTHLY"])
128         freq = iCalRecurrenceFrequenceMonthly;
129       else if ([frequency isEqualToString:@"DAILY"])
130         freq = iCalRecurrenceFrequenceDaily;
131       else if ([frequency isEqualToString:@"YEARLY"])
132         freq = iCalRecurrenceFrequenceYearly;
133       else if ([frequency isEqualToString:@"HOURLY"])
134         freq = iCalRecurrenceFrequenceHourly;
135       else if ([frequency isEqualToString:@"MINUTELY"])
136         freq = iCalRecurrenceFrequenceMinutely;
137       else if ([frequency isEqualToString:@"SECONDLY"])
138         freq = iCalRecurrenceFrequenceSecondly;
139       else
140         freq = NSNotFound;
141     }
142   else
143     freq = NSNotFound;
144
145   return freq;
146 }
147
148 - (NSString *) frequencyForValue: (iCalRecurrenceFrequency) freq
149 {
150   NSString *frequency;
151
152   switch (freq)
153     {
154     case iCalRecurrenceFrequenceWeekly:
155       frequency = @"WEEKLY";
156       break;
157     case iCalRecurrenceFrequenceMonthly:
158       frequency = @"MONTHLY";
159       break;
160     case iCalRecurrenceFrequenceDaily:
161       frequency = @"DAILY";
162       break;
163     case iCalRecurrenceFrequenceYearly:
164       frequency = @"YEARLY";
165       break;
166     case iCalRecurrenceFrequenceHourly:
167       frequency = @"HOURLY";
168       break;
169     case iCalRecurrenceFrequenceMinutely:
170       frequency = @"MINUTELY";
171       break;
172     case iCalRecurrenceFrequenceSecondly:
173       frequency = @"SECONDLY";
174       break;
175     default:
176       frequency = nil;
177     }
178
179   return frequency;
180 }
181
182 /* accessors */
183
184 - (void) setFrequency: (iCalRecurrenceFrequency) _frequency
185 {
186   [self setNamedValue: @"freq" to: [self frequencyForValue: _frequency]];
187 }
188
189 - (iCalRecurrenceFrequency) frequency
190 {
191   return [self valueForFrequency: [self namedValue: @"freq"]];
192 }
193
194 - (void) setRepeatCount: (int) _repeatCount
195 {
196   [self setNamedValue: @"count"
197         to: [NSString stringWithFormat: @"%d", _repeatCount]];
198 }
199
200 - (int) repeatCount
201 {
202   return [[self namedValue: @"count"] intValue];
203 }
204
205 - (void) setUntilDate: (NSCalendarDate *) _untilDate
206 {
207   [self setNamedValue: @"until"
208         to: [_untilDate iCalFormattedDateTimeString]];
209 }
210
211 - (NSCalendarDate *) untilDate
212 {
213 #warning handling of default timezone needs to be implemented
214   return [[self namedValue: @"until"] asCalendarDate];
215 }
216
217 - (void) setInterval: (NSString *) _interval
218 {
219   [self setNamedValue: @"interval" to: _interval];
220 }
221
222 - (void) setCount: (NSString *) _count
223 {
224   [self setNamedValue: @"count" to: _count];
225 }
226
227 - (void) setUntil: (NSString *) _until
228 {
229   [self setNamedValue: @"until" to: _until];
230 }
231
232 - (void) setRepeatInterval: (int) _repeatInterval
233 {
234   [self setNamedValue: @"interval"
235         to: [NSString stringWithFormat: @"%d", _repeatInterval]];
236 }
237
238 - (int) repeatInterval
239 {
240   return [[self namedValue: @"interval"] intValue];
241 }
242
243 - (void) setWkst: (NSString *) _weekStart
244 {
245   [self setNamedValue: @"wkst" to: _weekStart];
246 }
247
248 - (NSString *) wkst
249 {
250   return [self namedValue: @"wkst"];
251 }
252
253 - (void) setWeekStart: (iCalWeekDay) _weekStart
254 {
255   [self setWkst: [self iCalRepresentationForWeekDay: _weekStart]];
256 }
257
258 - (iCalWeekDay) weekStart
259 {
260   return [self weekDayFromICalRepresentation: [self wkst]];
261 }
262
263 - (void) setByDayMask: (unsigned) _mask
264 {
265   NSMutableArray *days;
266   unsigned int count;
267   unsigned char maskDays[] = { iCalWeekDayMonday, iCalWeekDayTuesday,
268                                iCalWeekDayWednesday, iCalWeekDayThursday,
269                                iCalWeekDayFriday, iCalWeekDaySaturday,
270                                iCalWeekDaySunday };
271   days = [NSMutableArray arrayWithCapacity: 7];
272   if (_mask)
273     {
274       for (count = 0; count < 7; count++)
275         if (_mask & maskDays[count])
276           [days addObject:
277                   [self iCalRepresentationForWeekDay: maskDays[count]]];
278     }
279
280   [self setNamedValue: @"byday" to: [days componentsJoinedByString: @","]];
281 }
282
283 - (unsigned int) byDayMask
284 {
285   NSArray *days;
286   unsigned int mask, count, max;
287   NSString *day, *value;
288
289   mask = 0;
290
291   value = [self namedValue: @"byday"];
292   if ([value length] > 0)
293     {
294       days = [value componentsSeparatedByString: @","];
295       max = [days count];
296       for (count = 0; count < max; count++)
297         {
298           day = [days objectAtIndex: count];
299           day = [day substringFromIndex: [day length] - 2];
300           mask |= [self weekDayFromICalRepresentation: day];
301         }
302     }
303
304   return mask;
305 }
306
307 #warning this is bad
308 - (int) byDayOccurence1
309 {
310   return 0;
311 //   return byDayOccurence1;
312 }
313
314 - (NSArray *) byMonthDay
315 {
316   return [[self namedValue: @"bymonthday"] componentsSeparatedByString: @","];
317 }
318
319 - (BOOL) isInfinite
320 {
321   return !([self repeatCount] || [self untilDate]);
322 }
323
324 /* private */
325
326 - (iCalWeekDay) weekDayFromICalRepresentation: (NSString *) _day
327 {
328   if ([_day length] > 1) {
329     /* be tolerant */
330     unichar c0, c1;
331     
332     c0 = [_day characterAtIndex:0];
333     if (c0 == 'm' || c0 == 'M') return iCalWeekDayMonday;
334     if (c0 == 'w' || c0 == 'W') return iCalWeekDayWednesday;
335     if (c0 == 'f' || c0 == 'F') return iCalWeekDayFriday;
336
337     c1 = [_day characterAtIndex:1];
338     if (c0 == 't' || c0 == 'T') {
339       if (c1 == 'u' || c1 == 'U') return iCalWeekDayTuesday;
340       if (c1 == 'h' || c1 == 'H') return iCalWeekDayThursday;
341     }
342     if (c0 == 's' || c0 == 'S') {
343       if (c1 == 'a' || c1 == 'A') return iCalWeekDaySaturday;
344       if (c1 == 'u' || c1 == 'U') return iCalWeekDaySunday;
345     }
346   }
347
348   return -1;
349 //   // TODO: do not raise but rather return an error value?
350 //   [NSException raise:NSGenericException
351 //             format:@"Incorrect weekDay '%@' specified!", _day];
352 //   return iCalWeekDayMonday; /* keep compiler happy */
353 }
354
355 - (NSString *) iCalRepresentationForWeekDay: (iCalWeekDay) _weekDay
356 {
357   switch (_weekDay)
358     {
359     case iCalWeekDayMonday: return @"MO";
360     case iCalWeekDayTuesday: return @"TU";
361     case iCalWeekDayWednesday: return @"WE";
362     case iCalWeekDayThursday: return @"TH";
363     case iCalWeekDayFriday: return @"FR";
364     case iCalWeekDaySaturday: return @"SA";
365     case iCalWeekDaySunday: return @"SU";
366     default:  return @"MO"; // TODO: return error?
367     }
368 }
369
370 // - (iCalWeekDay) weekDayForiCalRepre: (NSString *) _weekDay
371 // {
372 //   iCalWeekDay day;
373 //   NSString *weekDay;
374
375 //   weekDay = [_weekDay uppercaseString];
376 //   if ([weekDay isEqualToString: @"TU"])
377 //     day = iCalWeekDayTuesday;
378 //   else if ([weekDay isEqualToString: @"WE"])
379 //     day = iCalWeekDayWednesday;
380 //   else if ([weekDay isEqualToString: @"TH"])
381 //     day = iCalWeekDayThursday;
382 //   else if ([weekDay isEqualToString: @"FR"])
383 //     day = iCalWeekDayFriday;
384 //   else if ([weekDay isEqualToString: @"SA"])
385 //     day = iCalWeekDaySaturday;
386 //   else if ([weekDay isEqualToString: @"SU"])
387 //     day = iCalWeekDaySunday;
388 //   else
389 //     day = iCalWeekDayMonday;
390
391 //   return day;
392 // }
393
394 // - (NSString *) wkst
395 // {
396 //   return [self iCalRepresentationForWeekDay:byDay.weekStart];
397 // }
398
399 /*
400   TODO:
401   Each BYDAY value can also be preceded by a positive (+n) or negative
402   (-n) integer. If present, this indicates the nth occurrence of the
403   specific day within the MONTHLY or YEARLY RRULE. For example, within
404   a MONTHLY rule, +1MO (or simply 1MO) represents the first Monday
405   within the month, whereas -1MO represents the last Monday of the
406   month. If an integer modifier is not present, it means all days of
407   this type within the specified frequency. For example, within a
408   MONTHLY rule, MO represents all Mondays within the month.
409 */
410 // - (NSString *) byDayList
411 // {
412 //   NSMutableString *s;
413 //   unsigned        dow, mask, day;
414 //   BOOL            needsComma;
415   
416 //   s          = [NSMutableString stringWithCapacity:20];
417 //   needsComma = NO;
418 //   mask       = byDay.mask;
419 //   day        = iCalWeekDayMonday;
420   
421 //   for (dow = 0 /* Sun */; dow < 7; dow++) {
422 //     if (mask & day) {
423 //       if (needsComma)
424 //         [s appendString:@","];
425       
426 //       if (byDay.useOccurence)
427 //      // Note: we only support one occurrence for all currently
428 //      [s appendFormat:@"%i", byDayOccurence1];
429       
430 //       [s appendString:[self iCalRepresentationForWeekDay:day]];
431 //       needsComma = YES;
432 //     }
433 //     day = (day << 1);
434 //   }
435
436 //   return s;
437 // }
438
439 /* parsing rrule */
440
441 // - (void) _parseRuleString: (NSString *) _rrule
442 // {
443 //   // TODO: to be exact we would need a timezone to properly process the 'until'
444 //   //       date
445 //   unsigned i, count;
446 //   NSString *pFrequency = nil;
447 //   NSString *pUntil     = nil;
448 //   NSString *pCount     = nil;
449 //   NSString *pByday     = nil;
450 //   NSString *pBymday    = nil;
451 //   NSString *pBysetpos  = nil;
452 //   NSString *pInterval  = nil;
453   
454 //   for (i = 0, count = [values count]; i < count; i++) {
455 //     NSString *prop, *key, *value;
456 //     NSRange  r;
457 //     NSString **vHolder = NULL;
458     
459 //     prop = [values objectAtIndex:i];
460 //     r    = [prop rangeOfString:@"="];
461 //     if (r.length > 0) {
462 //       key   = [prop substringToIndex:r.location];
463 //       value = [prop substringFromIndex:NSMaxRange(r)];
464 //     }
465 //     else {
466 //       key   = prop;
467 //       value = nil;
468 //     }
469     
470 //     key = [[key stringByTrimmingSpaces] lowercaseString];
471 //     if (![key isNotEmpty]) {
472 //       [self errorWithFormat:@"empty component in rrule: %@", _rrule];
473 //       continue;
474 //     }
475     
476 //     vHolder = NULL;
477 //     switch ([key characterAtIndex:0]) {
478 //     case 'b':
479 //       if ([key isEqualToString:@"byday"])      { vHolder = &pByday;    break; }
480 //       if ([key isEqualToString:@"bymonthday"]) { vHolder = &pBymday;   break; }
481 //       if ([key isEqualToString:@"bysetpos"])   { vHolder = &pBysetpos; break; }
482 //       break;
483 //     case 'c':
484 //       if ([key isEqualToString:@"count"]) { vHolder = &pCount; break; }
485 //       break;
486 //     case 'f':
487 //       if ([key isEqualToString:@"freq"]) { vHolder = &pFrequency; break; }
488 //       break;
489 //     case 'i':
490 //       if ([key isEqualToString:@"interval"]) { vHolder = &pInterval; break; }
491 //       break;
492 //     case 'u':
493 //       if ([key isEqualToString:@"until"]) { vHolder = &pUntil; break; }
494 //       break;
495 //     default:
496 //       break;
497 //     }
498     
499 //     if (vHolder != NULL) {
500 //       if ([*vHolder isNotEmpty])
501 //         [self errorWithFormat:@"more than one '%@' in: %@", key, _rrule];
502 //       else
503 //         *vHolder = [value copy];
504 //     }
505 //     else {
506 //       // TODO: we should just parse known keys and put remainders into a
507 //       //       separate dictionary
508 //       [self logWithFormat:@"TODO: add explicit support for key: %@", key];
509 //       [self takeValue:value forKey:key];
510 //     }
511 //   }
512   
513 //   /* parse and fill individual values */
514 //   // TODO: this method should be a class method and create a new rrule object
515   
516 //   if ([pFrequency isNotEmpty])
517 //     [self setNamedValue: @"FREQ" to: pFrequency];
518 //   else
519 //     [self errorWithFormat:@"rrule contains no frequency: '%@'", _rrule];
520 //   [pFrequency release]; pFrequency = nil;
521
522 //   if (pInterval != nil)
523 //     interval = [pInterval intValue];
524 //   [pInterval release]; pInterval = nil;
525   
526 //   // TODO: we should parse byday in here
527 //   if (pByday != nil) [self setByday:pByday];
528 //   [pByday release]; pByday = nil;
529
530 //   if (pBymday != nil) {
531 //     NSArray *t;
532     
533 //     t = [pBymday componentsSeparatedByString:@","];
534 //     ASSIGNCOPY(byMonthDay, t);
535 //   }
536 //   [pBymday release]; pBymday = nil;
537   
538 //   if (pBysetpos != nil)
539 //     // TODO: implement
540 //     [self errorWithFormat:@"rrule contains bysetpos, unsupported: %@", _rrule];
541 //   [pBysetpos release]; pBysetpos = nil;
542   
543 //   if (pUntil != nil) {
544 //     NSCalendarDate *pUntilDate;
545     
546 //     if (pCount != nil) {
547 //       [self errorWithFormat:@"rrule contains 'count' AND 'until': %@", _rrule];
548 //       [pCount release];
549 //       pCount = nil;
550 //     }
551     
552 //     /*
553 //       The spec says:
554 //         "If specified as a date-time value, then it MUST be specified in an
555 //          UTC time format."
556 //       TODO: we still need some object representing a 'timeless' date.
557 //     */
558 //     if (![pUntil hasSuffix:@"Z"] && [pUntil length] > 8) {
559 //       [self warnWithFormat:@"'until' date has no explicit UTC marker: '%@'",
560 //               _rrule];
561 //     }
562     
563 //     pUntilDate = [NSCalendarDate calendarDateWithICalRepresentation:pUntil];
564 //     if (pUntilDate != nil)
565 //       [self setUntilDate:pUntilDate];
566 //     else {
567 //       [self errorWithFormat:@"could not parse 'until' in rrule: %@", 
568 //               _rrule];
569 //     }
570 //   }
571 //   [pUntil release]; pUntil = nil;
572   
573 //   if (pCount != nil) 
574 //     [self setRepeatCount:[pCount intValue]];
575 //   [pCount release]; pCount = nil;
576 // }
577
578 /* properties */
579
580 // - (void) setByday: (NSString *) _byDayList
581 // {
582 //   // TODO: each day can have an associated occurence, eg:
583 //   //        +1MO,+2TU,-9WE
584 //   // TODO: this should be moved to the parser
585 //   NSArray  *days;
586 //   unsigned i, count;
587 //   NSString    *iCalDay;
588 //   iCalWeekDay day;
589 //   unsigned    len;
590 //   unichar     c0;
591 //   int         occurence;
592 //   int offset;
593
594 //   /* reset mask */
595 //   byDay.mask = 0;
596 //   byDay.useOccurence = 0;
597 //   byDayOccurence1 = 0;
598   
599 //   days  = [_byDayList componentsSeparatedByString:@","];
600 //   for (i = 0, count = [days count]; i < count; i++)
601 //     {
602 //       iCalDay = [days objectAtIndex:i]; // eg: MO or TU
603 //       if ((len = [iCalDay length]) == 0)
604 //         {
605 //           [self errorWithFormat:@"found an empty day in byday list: '%@'", 
606 //                 _byDayList];
607 //           continue;
608 //         }
609     
610 //       c0 = [iCalDay characterAtIndex:0];
611 //       if (((c0 == '+' || c0 == '-') && len > 2) || (isdigit(c0) && len > 1)) {
612 //         occurence = [iCalDay intValue];
613       
614 //         offset = 1; /* skip occurence */
615 //         while (offset < len && isdigit([iCalDay characterAtIndex:offset]))
616 //           offset++;
617         
618 //         iCalDay = [iCalDay substringFromIndex:offset];
619         
620 //         if (byDay.useOccurence && (occurence != byDayOccurence1))
621 //           {
622 //             [self errorWithFormat:
623 //                     @"we only supported one occurence (occ=%i,day=%@): '%@'", 
624 //                   occurence, iCalDay, _byDayList];
625 //             continue;
626 //           }
627         
628 //         byDay.useOccurence = 1;
629 //         byDayOccurence1 = occurence;
630 //       }
631 //     else if (byDay.useOccurence)
632 //       [self errorWithFormat:
633 //            @"a byday occurence was specified on one day, but not on others"
634 //             @" (unsupported): '%@'", _byDayList];
635     
636 //       day = [self weekDayFromICalRepresentation:iCalDay];
637 //       byDay.mask |= day;
638 //     }
639 // }
640
641 /* key/value coding */
642
643 - (void) handleTakeValue: (id) _value
644            forUnboundKey: (NSString *)_key
645 {
646   [self warnWithFormat:@"Cannot handle unbound key: '%@'", _key];
647 }
648
649 @end /* iCalRecurrenceRule */