]> err.no Git - sope/blobdiff - sope-ical/NGiCal/iCalRecurrenceRule.m
work on recurrences
[sope] / sope-ical / NGiCal / iCalRecurrenceRule.m
index 9278888f6e37fcf9e809b790f2ee529c57199a45..f437738a57e707ab7083a3aa6ac7474aaba576f9 100644 (file)
 */
 
 #include "iCalRecurrenceRule.h"
-#include "iCalDateHolder.h"
 #include "NSCalendarDate+ICal.h"
 #include "common.h"
 
 /*
- freq       = rrFreq;
- until      = rrUntil;
- count      = rrCount;
- interval   = rrInterval;
- bysecond   = rrBySecondList;
- byminute   = rrByMinuteList;
- byhour     = rrByHourList;
- byday      = rrByDayList;
- bymonthday = rrByMonthDayList;
- byyearday  = rrByYearDayList;
- byweekno   = rrByWeekNumberList;
- bymonth    = rrByMonthList;
- bysetpos   = rrBySetPosList;
- wkst       = rrWeekStart;
- */
-
-@interface iCalDateHolder (PrivateAPI)
-- (void)setString:(NSString *)_value;
-- (id)awakeAfterUsingSaxDecoder:(id)_decoder;
-@end
+  freq       = rrFreq;
+  until      = rrUntil;
+  count      = rrCount;
+  interval   = rrInterval;
+  bysecond   = rrBySecondList;
+  byminute   = rrByMinuteList;
+  byhour     = rrByHourList;
+  byday      = rrByDayList;
+  bymonthday = rrByMonthDayList;
+  byyearday  = rrByYearDayList;
+  byweekno   = rrByWeekNumberList;
+  bymonth    = rrByMonthList;
+  bysetpos   = rrBySetPosList;
+  wkst       = rrWeekStart;
+*/
 
+// TODO: private API in the header file?!
 @interface iCalRecurrenceRule (PrivateAPI)
+
 - (iCalWeekDay)weekDayFromICalRepresentation:(NSString *)_day;
 - (NSString *)iCalRepresentationForWeekDay:(iCalWeekDay)_weekDay;
 - (NSString *)freq;
 - (NSString *)wkst;
 - (NSString *)byDayList;
 
-- (void)_processRule;
-- (void)setRrule:(NSString *)_rrule;
+- (void)_parseRuleString:(NSString *)_rrule;
+- (void)setRrule:(NSString *)_rrule; // TODO: weird name?
+
+/* currently used by parser, should be removed (replace with an -init..) */
+- (void)setByday:(NSString *)_byDayList;
+- (void)setFreq:(NSString *)_freq;
+
 @end
 
 @implementation iCalRecurrenceRule
 
-+ (void)initialize {
-  static BOOL didInit = NO;
-  
-  if (didInit) return;
-  didInit = YES;
-}
-
 + (id)recurrenceRuleWithICalRepresentation:(NSString *)_iCalRep {
-  iCalRecurrenceRule *r;
-  
-  r = [[[self alloc] init] autorelease];
-  [r setRrule:_iCalRep];
-  return r;
+  return [[[self alloc] initWithString:_iCalRep] autorelease];
 }
 
-- (id)init {
-  self = [super init];
-  if (self) {
+- (id)init { /* designated initializer */
+  if ((self = [super init]) != nil) {
     self->byDay.weekStart = iCalWeekDayMonday;
     self->interval        = 1;
   }
   return self;
 }
 
+- (id)initWithString:(NSString *)_str {
+  if ((self = [self init]) != nil) {
+    [self setRrule:_str];
+  }
+  return self;
+}
+
 - (void)dealloc {
   [self->untilDate release];
   [self->rrule     release];
@@ -90,7 +86,7 @@
 }
 
 
-/* Accessors */
+/* accessors */
 
 - (void)setFrequency:(iCalRecurrenceFrequency)_frequency {
   self->frequency = _frequency;
   return self->byDay.weekStart;
 }
 
+- (void)setByDayMask:(unsigned)_mask {
+  self->byDay.mask = _mask;
+}
+- (unsigned)byDayMask {
+  return self->byDay.mask;
+}
+- (int)byDayOccurence1 {
+  return self->byDayOccurence1;
+}
+
 - (BOOL)isInfinite {
   return (self->repeatCount != 0 || self->untilDate) ? NO : YES;
 }
 
 
-/* Private */
+/* private */
 
 - (iCalWeekDay)weekDayFromICalRepresentation:(NSString *)_day {
-  _day = [_day uppercaseString];
-  if ([_day isEqualToString:@"MO"])
-    return iCalWeekDayMonday;
-  else if ([_day isEqualToString:@"TU"])
-    return iCalWeekDayTuesday;
-  else if ([_day isEqualToString:@"WE"])
-    return iCalWeekDayWednesday;
-  else if ([_day isEqualToString:@"TH"])
-    return iCalWeekDayThursday;
-  else if ([_day isEqualToString:@"FR"])
-    return iCalWeekDayFriday;
-  else if ([_day isEqualToString:@"SA"])
-    return iCalWeekDaySaturday;
-  else if ([_day isEqualToString:@"SU"])
-    return iCalWeekDaySunday;
-  else
-    [NSException raise:NSGenericException
-                 format:@"Incorrect weekDay '%@' specified!", _day];
+  if ([_day length] > 1) {
+    /* be tolerant */
+    unichar c0, c1;
+    
+    c0 = [_day characterAtIndex:0];
+    if (c0 == 'm' || c0 == 'M') return iCalWeekDayMonday;
+    if (c0 == 'w' || c0 == 'W') return iCalWeekDayWednesday;
+    if (c0 == 'f' || c0 == 'F') return iCalWeekDayFriday;
+
+    c1 = [_day characterAtIndex:1];
+    if (c0 == 't' || c0 == 'T') {
+      if (c1 == 'u' || c1 == 'U') return iCalWeekDayTuesday;
+      if (c1 == 'h' || c1 == 'H') return iCalWeekDayThursday;
+    }
+    if (c0 == 's' || c0 == 'S') {
+      if (c1 == 'a' || c1 == 'A') return iCalWeekDaySaturday;
+      if (c1 == 'u' || c1 == 'U') return iCalWeekDaySunday;
+    }
+  }
+  
+  // TODO: do not raise but rather return an error value?
+  [NSException raise:NSGenericException
+              format:@"Incorrect weekDay '%@' specified!", _day];
   return iCalWeekDayMonday; /* keep compiler happy */
 }
 
 - (NSString *)iCalRepresentationForWeekDay:(iCalWeekDay)_weekDay {
-  switch (self->byDay.weekStart) {
-    case iCalWeekDayMonday:
-      return @"MO";
-    case iCalWeekDayTuesday:
-      return @"TU";
-    case iCalWeekDayWednesday:
-      return @"WE";
-    case iCalWeekDayThursday:
-      return @"TH";
-    case iCalWeekDayFriday:
-      return @"FR";
-    case iCalWeekDaySaturday:
-      return @"SA";
-    case iCalWeekDaySunday:
-      return @"SU";
-    default:
-      return @"MO";
+  switch (_weekDay) {
+    case iCalWeekDayMonday:    return @"MO";
+    case iCalWeekDayTuesday:   return @"TU";
+    case iCalWeekDayWednesday: return @"WE";
+    case iCalWeekDayThursday:  return @"TH";
+    case iCalWeekDayFriday:    return @"FR";
+    case iCalWeekDaySaturday:  return @"SA";
+    case iCalWeekDaySunday:    return @"SU";
+    default:                   return @"MO"; // TODO: return error?
   }
 }
 
 - (NSString *)freq {
   switch (self->frequency) {
-    case iCalRecurrenceFrequenceWeekly:
-      return @"WEEKLY";
-    case iCalRecurrenceFrequenceMonthly:
-      return @"MONTHLY";
-    case iCalRecurrenceFrequenceDaily:
-      return @"DAILY";
-    case iCalRecurrenceFrequenceYearly:
-      return @"YEARLY";
-    case iCalRecurrenceFrequenceHourly:
-      return @"HOURLY";
-    case iCalRecurrenceFrequenceMinutely:
-      return @"MINUTELY";
-    case iCalRecurrenceFrequenceSecondly:
-      return @"SECONDLY";
+    case iCalRecurrenceFrequenceWeekly:   return @"WEEKLY";
+    case iCalRecurrenceFrequenceMonthly:  return @"MONTHLY";
+    case iCalRecurrenceFrequenceDaily:    return @"DAILY";
+    case iCalRecurrenceFrequenceYearly:   return @"YEARLY";
+    case iCalRecurrenceFrequenceHourly:   return @"HOURLY";
+    case iCalRecurrenceFrequenceMinutely: return @"MINUTELY";
+    case iCalRecurrenceFrequenceSecondly: return @"SECONDLY";
     default:
       return @"UNDEFINED?";
   }
 }
 
 /*
- TODO:
- Each BYDAY value can also be preceded by a positive (+n) or negative
- (-n) integer. If present, this indicates the nth occurrence of the
- specific day within the MONTHLY or YEARLY RRULE. For example, within
- a MONTHLY rule, +1MO (or simply 1MO) represents the first Monday
- within the month, whereas -1MO represents the last Monday of the
- month. If an integer modifier is not present, it means all days of
- this type within the specified frequency. For example, within a
- MONTHLY rule, MO represents all Mondays within the month.
 TODO:
 Each BYDAY value can also be preceded by a positive (+n) or negative
 (-n) integer. If present, this indicates the nth occurrence of the
 specific day within the MONTHLY or YEARLY RRULE. For example, within
 a MONTHLY rule, +1MO (or simply 1MO) represents the first Monday
 within the month, whereas -1MO represents the last Monday of the
 month. If an integer modifier is not present, it means all days of
 this type within the specified frequency. For example, within a
 MONTHLY rule, MO represents all Mondays within the month.
 */
 - (NSString *)byDayList {
   NSMutableString *s;
-  unsigned        i, day;
+  unsigned        i, mask, day;
   BOOL            needsComma;
-
+  
   s          = [NSMutableString stringWithCapacity:20];
   needsComma = NO;
+  mask       = self->byDay.mask;
   day        = iCalWeekDayMonday;
-
+  
   for (i = 0; i < 7; i++) {
-    if (self->byDay.mask && day) {
+    if (mask & day) {
       if (needsComma)
         [s appendString:@","];
+      else if (self->byDay.useOccurence)
+       // Note: we only support one occurrence currently
+       [s appendFormat:@"%i", self->byDayOccurence1];
+      
       [s appendString:[self iCalRepresentationForWeekDay:day]];
       needsComma = YES;
     }
-    day = day << 1;
+    day = (day << 1);
   }
   return s;
 }
 /* Rule */
 
 - (void)setRrule:(NSString *)_rrule {
-  ASSIGN(self->rrule, _rrule);
-  [self _processRule];
+  ASSIGNCOPY(self->rrule, _rrule);
+  [self _parseRuleString:self->rrule];
 }
 
-/* Processing existing rrule */
+/* parsing rrule */
 
-- (void)_processRule {
+- (void)_parseRuleString:(NSString *)_rrule {
+  // TODO: to be exact we would need a timezone to properly process the 'until'
+  //       date
   NSArray  *props;
   unsigned i, count;
+  NSString *pFrequency = nil;
+  NSString *pUntil     = nil;
+  NSString *pCount     = nil;
+  NSString *pByday     = nil;
+  NSString *pBysetpos  = nil;
   
-  props = [self->rrule componentsSeparatedByString:@";"];
-  count = [props count];
-  for (i = 0; i < count; i++) {
+  props = [_rrule componentsSeparatedByString:@";"];
+  for (i = 0, count = [props count]; i < count; i++) {
     NSString *prop, *key, *value;
     NSRange  r;
+    NSString **vHolder = NULL;
     
     prop = [props objectAtIndex:i];
     r    = [prop rangeOfString:@"="];
-    if (r.length) {
+    if (r.length > 0) {
       key   = [prop substringToIndex:r.location];
       value = [prop substringFromIndex:NSMaxRange(r)];
     }
       key   = prop;
       value = nil;
     }
-    [self takeValue:value forKey:[key lowercaseString]];
+    
+    key = [[key stringByTrimmingSpaces] lowercaseString];
+    if (![key isNotEmpty]) {
+      [self errorWithFormat:@"empty component in rrule: %@", _rrule];
+      continue;
+    }
+    
+    vHolder = NULL;
+    switch ([key characterAtIndex:0]) {
+    case 'b':
+      if ([key isEqualToString:@"byday"])    vHolder = &pByday;    break;
+      if ([key isEqualToString:@"bysetpos"]) vHolder = &pBysetpos; break;
+      break;
+    case 'c':
+      if ([key isEqualToString:@"count"])    vHolder = &pCount;    break;
+      break;
+    case 'f':
+      if ([key isEqualToString:@"freq"]) vHolder = &pFrequency; break;
+      break;
+    case 'u':
+      if ([key isEqualToString:@"until"]) vHolder = &pUntil; break;
+      break;
+    default:
+      break;
+    }
+    
+    if (vHolder != NULL) {
+      if ([*vHolder isNotEmpty])
+        [self errorWithFormat:@"more than one '%@' in: %@", key, _rrule];
+      else
+        *vHolder = [value copy];
+    }
+    else {
+      // TODO: we should just parse known keys and put remainders into a
+      //       separate dictionary
+      //[self logWithFormat:@"TODO: add explicit support for key: %@", key];
+      [self takeValue:value forKey:key];
+    }
+  }
+  
+  /* parse and fill individual values */
+  // TODO: this method should be a class method and create a new rrule object
+  
+  if ([pFrequency isNotEmpty])
+    [self setFreq:pFrequency];
+  else
+    [self errorWithFormat:@"rrule contains no frequency: '%@'", _rrule];
+  [pFrequency release]; pFrequency = nil;
+  
+  // TODO: we should parse byday in here
+  if (pByday != nil) [self setByday:pByday];
+  [pByday release]; pByday = nil;
+  
+  if (pBysetpos != nil)
+    // TODO: implement
+    [self errorWithFormat:@"rrule contains bysetpos, unsupported: %@", _rrule];
+  [pBysetpos release]; pBysetpos = nil;
+  
+  if (pUntil != nil) {
+    NSCalendarDate *pUntilDate;
+    
+    if (pCount != nil) {
+      [self errorWithFormat:@"rrule contains 'count' AND 'until': %@", _rrule];
+      [pCount release];
+      pCount = nil;
+    }
+    
+    /*
+      The spec says:
+        "If specified as a date-time value, then it MUST be specified in an
+         UTC time format."
+      TODO: we still need some object representing a 'timeless' date.
+    */
+    if (![pUntil hasSuffix:@"Z"]) {
+      [self warnWithFormat:@"'until' date has no explicit UTC marker: '%@'",
+              _rrule];
+    }
+    
+    pUntilDate = [NSCalendarDate calendarDateWithICalRepresentation:pUntil];
+    if (pUntilDate != nil)
+      [self setUntilDate:pUntilDate];
+    else {
+      [self errorWithFormat:@"could not parse 'until' in rrule: %@", 
+              _rrule];
+    }
   }
+  [pUntil release]; pUntil = nil;
+  
+  if (pCount != nil) 
+    [self setRepeatCount:[pCount intValue]];
+  [pCount release]; pCount = nil;
 }
 
 
 /* properties */
 
 - (void)setFreq:(NSString *)_freq {
+  // TODO: shouldn't we preserve what the user gives us?
+  // => only used by -_parseRuleString: parser?
   _freq = [_freq uppercaseString];
   if ([_freq isEqualToString:@"WEEKLY"])
     self->frequency = iCalRecurrenceFrequenceWeekly;
     self->frequency = iCalRecurrenceFrequenceMinutely;
   else if ([_freq isEqualToString:@"SECONDLY"])
     self->frequency = iCalRecurrenceFrequenceSecondly;
-  else
+  else {
     [NSException raise:NSGenericException
                  format:@"Incorrect frequency '%@' specified!", _freq];
+  }
 }
 
 - (void)setInterval:(NSString *)_interval {
   self->repeatCount = [_count unsignedIntValue];
 }
 - (void)setUntil:(NSString *)_until {
-  iCalDateHolder *dh;
   NSCalendarDate *date;
 
-  dh = [[iCalDateHolder alloc] init];
-  [dh setString:_until];
-  date = [dh awakeAfterUsingSaxDecoder:nil];
+  date = [NSCalendarDate calendarDateWithICalRepresentation:_until];
   ASSIGN(self->untilDate, date);
-  [dh release];
 }
 
 - (void)setWkst:(NSString *)_weekStart {
 }
 
 - (void)setByday:(NSString *)_byDayList {
+  // TODO: each day can have an associated occurence, eg:
+  //        +1MO,+2TU,-9WE
+  // TODO: this should be moved to the parser
   NSArray  *days;
   unsigned i, count;
-
+  
+  /* reset mask */
   self->byDay.mask = 0;
+  self->byDay.useOccurence = 0;
+  self->byDayOccurence1 = 0;
+  
   days  = [_byDayList componentsSeparatedByString:@","];
-  count = [days count];
-  for (i = 0; i < count; i++) {
+  for (i = 0, count = [days count]; i < count; i++) {
     NSString    *iCalDay;
     iCalWeekDay day;
+    unsigned    len;
+    unichar     c0;
+    int         occurence;
+    
+    iCalDay = [days objectAtIndex:i]; // eg: MO or TU
+    if ((len = [iCalDay length]) == 0) {
+      [self errorWithFormat:@"found an empty day in byday list: '%@'", 
+             _byDayList];
+      continue;
+    }
     
-    iCalDay = [days objectAtIndex:i];
-    day     = [self weekDayFromICalRepresentation:iCalDay];
+    c0 = [iCalDay characterAtIndex:0];
+    if (((c0 == '+' || c0 == '-') && len > 2) || (isdigit(c0) && len > 1)) {
+      int offset;
+      
+      occurence = [iCalDay intValue];
+
+      offset = 1;
+      while (offset < len && isdigit([iCalDay characterAtIndex:offset]))
+       offset++;
+      
+      iCalDay = [iCalDay substringFromIndex:offset];
+      
+      if (self->byDay.useOccurence) {
+       [self errorWithFormat:
+               @"we only supported one occurence (occ=%i,day=%@): '%@'", 
+               occurence, iCalDay, _byDayList];
+       continue;
+      }
+      
+      self->byDay.useOccurence = 1;
+      self->byDayOccurence1 = occurence;
+    }
+    else if (self->byDay.useOccurence) {
+      [self errorWithFormat:
+             @"a byday occurence was specified on one day, but not on others"
+             @" (unsupported): '%@'", _byDayList];
+    }
+    
+    day = [self weekDayFromICalRepresentation:iCalDay];
     self->byDay.mask |= day;
   }
 }
 /* key/value coding */
 
 - (void)handleTakeValue:(id)_value forUnboundKey:(NSString *)_key {
-  [self warnWithFormat:@"Don't know how to process '%@'!", _key];
+  [self warnWithFormat:@"Cannot handle unbound key: '%@'", _key];
 }
 
 
-/* Description */
+/* description */
 
 - (NSString *)iCalRepresentation {
   NSMutableString *s;
   
   s = [NSMutableString stringWithCapacity:80];
+
   [s appendString:@"FREQ="];
   [s appendString:[self freq]];
-  if ([self repeatInterval] != 1) {
+
+  if ([self repeatInterval] != 1)
     [s appendFormat:@";INTERVAL=%d", [self repeatInterval]];
-  }
+  
   if (![self isInfinite]) {
     if ([self repeatCount] > 0) {
       [s appendFormat:@";COUNT=%d", [self repeatCount]];
   return s;
 }
 
+- (NSString *)description {
+  return [self iCalRepresentation];
+}
 
-@end
+@end /* iCalRecurrenceRule */