]> err.no Git - sope/commitdiff
improved rrule parser
authorhelge <helge@e4a50df8-12e2-0310-a44c-efbce7f8a7e3>
Wed, 21 Sep 2005 12:09:26 +0000 (12:09 +0000)
committerhelge <helge@e4a50df8-12e2-0310-a44c-efbce7f8a7e3>
Wed, 21 Sep 2005 12:09:26 +0000 (12:09 +0000)
fixed some month rrule calculation

git-svn-id: http://svn.opengroupware.org/SOPE/trunk@1125 e4a50df8-12e2-0310-a44c-efbce7f8a7e3

sope-ical/NGiCal/ChangeLog
sope-ical/NGiCal/Version
sope-ical/NGiCal/iCalMonthlyRecurrenceCalculator.m
sope-ical/NGiCal/iCalRecurrenceCalculator.h
sope-ical/NGiCal/iCalRecurrenceCalculator.m
sope-ical/NGiCal/iCalRecurrenceRule.m

index 84247db3668d47502ed0f02e3d8e4ba7d1bda095..95123ffc22b9b8a23b186ac9e41d7fbc9eab7122 100644 (file)
@@ -1,3 +1,13 @@
+2005-09-21  Helge Hess  <helge.hess@skyrix.com>
+
+       * v4.5.65
+
+       * iCalMonthlyRecurrenceCalculator.m: fixed calculation of 'count' field
+
+       * iCalRecurrenceCalculator.m: minor code cleanups
+
+       * iCalRecurrenceRule.m: improved rrule parser
+
 2005-09-20  Helge Hess  <helge.hess@opengroupware.org>
 
        * iCalMonthlyRecurrenceCalculator.m: stop calculation if a byday part
index 0e2ffe99bf81001ef96bbeb99e543109fe718a9f..3e9a09867bfd8cae2d1f72d442f7e60efe48e7a3 100644 (file)
@@ -2,7 +2,7 @@
 
 MAJOR_VERSION=4
 MINOR_VERSION=5
-SUBMINOR_VERSION:=64
+SUBMINOR_VERSION:=65
 
 # v4.5.40 requires NGExtensions v4.5.145
 # v4.5.37 requires NGExtensions v4.5.140
index be6e6f8af56e3b2506db7800ce184a6d2102dbb5..7f04abb0ea233cb3d5601d021fd4fa181da66906 100644 (file)
 @implementation iCalMonthlyRecurrenceCalculator
 
 - (NSArray *)recurrenceRangesWithinCalendarDateRange:(NGCalendarDateRange *)_r{
+  /* main entry */
   NSMutableArray *ranges;
+  NSTimeZone     *timeZone;
   NSCalendarDate *firStart, *rStart, *rEnd, *until;
   unsigned       i, count, interval;
   int            diff;
 
   firStart = [self->firstRange startDate];
+  timeZone = [firStart timeZone];
   rStart   = [_r startDate];
   rEnd     = [_r endDate];
   interval = [self->rrule repeatInterval];
-  until    = [self lastInstanceStartDate];
+  until    = [self lastInstanceStartDate]; // TODO: maybe replace
   
   if ([self->rrule byDayMask] != 0) {
     [self errorWithFormat:@"cannot process byday part of rrule: %@", 
     return nil;
   }
   
+  /* check whether the range to be processed is beyond the 'until' date */
+  
   if (until != nil) {
-    if ([until compare:rStart] == NSOrderedAscending)
+    if ([until compare:rStart] == NSOrderedAscending) /* until before start */
       return nil;
-    if ([until compare:rEnd] == NSOrderedDescending)
-      rEnd = until;
+    if ([until compare:rEnd] == NSOrderedDescending) /* end before until */
+      rEnd = until; // TODO: why is that? end is _before_ until?
   }
-
-  diff   = [firStart monthsBetweenDate:rStart];
+  
+  // TODO: I think the diff is to skip recurrence which are before the
+  //       requested range. Not sure whether this is actually possible, eg
+  //       the repeatCount must be processed from the start.
+  diff = [firStart monthsBetweenDate:rStart];
   if ((diff != 0) && [rStart compare:firStart] == NSOrderedAscending)
     diff = -diff;
-
+  
   count  = [rStart monthsBetweenDate:rEnd] + 1;
   ranges = [NSMutableArray arrayWithCapacity:count];
   for (i = 0 ; i < count; i++) {
+    NSCalendarDate      *start, *end;
+    NGCalendarDateRange *r;
     int test;
     
     test = diff + i;
-    if ((test >= 0) && (test % interval) == 0) {
-      NSCalendarDate      *start, *end;
-      NGCalendarDateRange *r;
-      
-      start = [firStart dateByAddingYears:0
-                        months:diff + i
-                        days:0];
-      [start setTimeZone:[firStart timeZone]];
-      end   = [start addTimeInterval:[self->firstRange duration]];
-      r     = [NGCalendarDateRange calendarDateRangeWithStartDate:start
-                                   endDate:end];
-      if ([_r containsDateRange:r])
-        [ranges addObject:r];
+    
+    if (test < 0)
+      continue;
+    
+    if ((test % interval) != 0)
+      continue;
+    
+    start = [firStart dateByAddingYears:0 months:(diff + i) days:0];
+    [start setTimeZone:timeZone];
+    
+    /* check whether we are still in the limits */
+    
+    // TODO: I think we should check in here whether we succeeded the
+    //       repeatCount. Currently we precalculate that info in the
+    //       -lastInstanceStartDate method.
+    if (until != nil) {
+      /* Note: the 'until' in the rrule is inclusive as per spec */
+      if ([until compare:start] == NSOrderedAscending) /* start after until */
+        break; /* Note: we assume that the algorithm is sequential */
     }
+    
+    /* create end date */
+
+    end   = [start addTimeInterval:[self->firstRange duration]];
+    [end setTimeZone:timeZone];
+    
+    /* create range and check whether its in the requested range */
+    
+    r = [[NGCalendarDateRange alloc] initWithStartDate:start endDate:end];
+    if ([_r containsDateRange:r])
+      [ranges addObject:r];
+    [r release]; r = nil;
   }
   return ranges;
 }
   if ([self->rrule repeatCount] > 0) {
     NSCalendarDate *until;
     unsigned       months, interval;
-
+    
     interval = [self->rrule repeatInterval];
-    months   = [self->rrule repeatCount] * interval;
+    months   = ([self->rrule repeatCount] - 1) * interval;
     until    = [[self->firstRange startDate] dateByAddingYears:0
                                              months:months
                                              days:0];
index 007a2ed86af91ee377187031f917759b4bae3edc..7f191c612c4b23c1ab4af56a052ef714205fae8e 100644 (file)
@@ -29,6 +29,8 @@
  
   Provides an API for performing common calculations performed in conjunction
   with iCalRecurrenceRule objects.
+
+  TODO: rather move this functionality to iCalRecurrenceRule?
 */
 
 @class NSArray;
index 6e048f7eff5387fef2cd0bdf2cf9898549da1a3d..d144efead70b613c7cb8a9b727896ced0033dd9b 100644 (file)
@@ -67,7 +67,7 @@ static Class yearlyCalcClass  = Nil;
 /* factory */
 
 + (id)recurrenceCalculatorForRecurrenceRule:(iCalRecurrenceRule *)_rrule
-         withFirstInstanceCalendarDateRange:(NGCalendarDateRange *)_range
+  withFirstInstanceCalendarDateRange:(NGCalendarDateRange *)_range
 {
   return [[[self alloc] initWithRecurrenceRule:_rrule
                         firstInstanceCalendarDateRange:_range] autorelease];
@@ -87,9 +87,9 @@ static Class yearlyCalcClass  = Nil;
   NSMutableArray           *exDates;
   unsigned                 i, count, rCount;
   
-  ranges = [NSMutableArray array];
-  count  = [_rRules count];
-  for (i = 0; i < count; i++) {
+  ranges = [NSMutableArray arrayWithCapacity:64];
+  
+  for (i = 0, count  = [_rRules count]; i < count; i++) {
     NSArray *rs;
 
     rule = [_rRules objectAtIndex:i];
@@ -102,12 +102,12 @@ static Class yearlyCalcClass  = Nil;
     [ranges addObjectsFromArray:rs];
   }
   
-  if (![ranges count])
+  if ([ranges count] == 0)
     return nil;
   
   /* test if any exceptions do match */
-  count = [_exRules count];
-  for (i = 0; i < count; i++) {
+  
+  for (i = 0, count = [_exRules count]; i < count; i++) {
     NSArray *rs;
 
     rule = [_exRules objectAtIndex:i];
@@ -120,14 +120,14 @@ static Class yearlyCalcClass  = Nil;
     [ranges removeObjectsInArray:rs];
   }
   
-  if (![ranges count])
+  if (![ranges isNotEmpty])
     return nil;
   
   /* exception dates */
 
-  count  = [_exDates count];
-  if (!count) return ranges;
-
+  if ((count = [_exDates count]) == 0)
+    return ranges;
+  
   /* sort out exDates not within range */
 
   exDates = [NSMutableArray arrayWithCapacity:count];
@@ -135,20 +135,19 @@ static Class yearlyCalcClass  = Nil;
     id exDate;
 
     exDate = [_exDates objectAtIndex:i];
-    if (![exDate isKindOfClass:NSCalendarDateClass]) {
+    if (![exDate isKindOfClass:NSCalendarDateClass])
       exDate = [NSCalendarDate calendarDateWithICalRepresentation:exDate];
-    }
+    
     if ([_r containsDate:exDate])
       [exDates addObject:exDate];
   }
 
   /* remove matching exDates from ranges */
 
-  count  = [exDates count];
-  if (!count) return ranges;
-
-  rCount = [ranges count];
-  for (i = 0; i < count; i++) {
+  if ((count = [exDates count]) == 0)
+    return ranges;
+  
+  for (i = 0, rCount = [ranges count]; i < count; i++) {
     NSCalendarDate      *exDate;
     NGCalendarDateRange *r;
     unsigned            k;
@@ -187,19 +186,24 @@ static Class yearlyCalcClass  = Nil;
     calcClass = monthlyCalcClass;
   else if (freq == iCalRecurrenceFrequenceYearly)
     calcClass = yearlyCalcClass;
-
-  [self autorelease];
+  else {
+    [self errorWithFormat:@"unsupported rrule frequency: %@", _rrule];
+    calcClass = Nil;
+    [self release];
+    return nil;
+  }
+  
+  [self autorelease]; // TODO: why autorelease?
   if (calcClass == Nil)
     return nil;
-
-  self = [[calcClass alloc] init];
-  ASSIGN(self->rrule, _rrule);
-  ASSIGN(self->firstRange, _range);
+  
+  if ((self = [[calcClass alloc] init]) != nil) {
+    self->rrule      = [_rrule retain];
+    self->firstRange = [_range retain];
+  }
   return self;  
 }
 
-/* dealloc */
-
 - (void)dealloc {
   [self->firstRange release];
   [self->rrule      release];
@@ -245,14 +249,17 @@ static Class yearlyCalcClass  = Nil;
     case 4:  weekDay = iCalWeekDayThursday;  break;
     case 5:  weekDay = iCalWeekDayFriday;    break;
     case 6:  weekDay = iCalWeekDaySaturday;  break;
-    default: weekDay = iCalWeekDaySunday;    break; /* keep compiler happy */
+    default: 
+      [self errorWithFormat:@"got unexpected weekday: %d", day];
+      weekDay = iCalWeekDaySunday;
+      break; /* keep compiler happy */
   }
   return weekDay;
 }
 
 /* calculation */
 
-- (NSArray *)recurrenceRangesWithinCalendarDateRange:(NGCalendarDateRange *)_r {
+- (NSArray *)recurrenceRangesWithinCalendarDateRange:(NGCalendarDateRange *)_r{
   return nil; /* subclass responsibility */
 }
 - (BOOL)doesRecurrWithinCalendarDateRange:(NGCalendarDateRange *)_range {
index 7a03c4673aff7f467c015006847992af46e7965e..3a32075cf74c35f85f0aa4171cc734601c6db5fb 100644 (file)
 - (NSString *)wkst;
 - (NSString *)byDayList;
 
-- (void)_processRule;
+- (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)setRrule:(NSString *)_rrule {
   ASSIGNCOPY(self->rrule, _rrule);
-  [self _processRule];
+  [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:@";"];
+  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:@"="];
       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;
 }
 
 
 
 - (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;
 - (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;
   
 /* 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];
 }