]> err.no Git - sope/blobdiff - sope-ical/NGiCal/iCalRecurrenceCalculator.m
yet another bugfix for recurrence calculation
[sope] / sope-ical / NGiCal / iCalRecurrenceCalculator.m
index 7878d45bcc6aa2ec3d049b382709e7c30c13b74d..18ef6b3c225db7cb93233de63397060f11493580 100644 (file)
 
 @interface iCalRecurrenceCalculator (PrivateAPI)
 - (NSCalendarDate *)lastInstanceStartDate;
+
+- (unsigned)offsetFromSundayForJulianNumber:(long)_jn;
+- (unsigned)offsetFromSundayForWeekDay:(iCalWeekDay)_weekDay;
+- (unsigned)offsetFromSundayForCurrentWeekStart;
+  
+- (iCalWeekDay)weekDayForJulianNumber:(long)_jn;
 @end
 
 @implementation iCalRecurrenceCalculator
 
+static Class NSCalendarDateClass     = Nil;
+static Class iCalRecurrenceRuleClass = Nil;
 static Class dailyCalcClass   = Nil;
 static Class weeklyCalcClass  = Nil;
 static Class monthlyCalcClass = Nil;
@@ -66,12 +74,17 @@ static Class yearlyCalcClass  = Nil;
   if (didInit) return;
   didInit = YES;
 
+  NSCalendarDateClass     = [NSCalendarDate class];
+  iCalRecurrenceRuleClass = [iCalRecurrenceRule class];
+
   dailyCalcClass   = [iCalDailyRecurrenceCalculator   class];
   weeklyCalcClass  = [iCalWeeklyRecurrenceCalculator  class];
   monthlyCalcClass = [iCalMonthlyRecurrenceCalculator class];
   yearlyCalcClass  = [iCalYearlyRecurrenceCalculator  class];
 }
 
+/* factory */
+
 + (id)recurrenceCalculatorForRecurrenceRule:(iCalRecurrenceRule *)_rrule
          withFirstInstanceCalendarDateRange:(NGCalendarDateRange *)_range
 {
@@ -79,6 +92,105 @@ static Class yearlyCalcClass  = Nil;
                         firstInstanceCalendarDateRange:_range] autorelease];
 }
 
+/* complex calculation convenience */
+
++ (NSArray *)recurrenceRangesWithinCalendarDateRange:(NGCalendarDateRange *)_r
+  firstInstanceCalendarDateRange:(NGCalendarDateRange *)_fir
+  recurrenceRules:(NSArray *)_rRules
+  exceptionRules:(NSArray *)_exRules
+  exceptionDates:(NSArray *)_exDates
+{
+  id                       rule;
+  iCalRecurrenceCalculator *calc;
+  NSMutableArray           *ranges;
+  NSMutableArray           *exDates;
+  unsigned                 i, count, rCount;
+  
+  ranges = [NSMutableArray array];
+  count  = [_rRules count];
+  for (i = 0; i < count; i++) {
+    NSArray *rs;
+
+    rule = [_rRules objectAtIndex:i];
+    if (![rule isKindOfClass:iCalRecurrenceRuleClass])
+      rule = [iCalRecurrenceRule recurrenceRuleWithICalRepresentation:rule];
+  
+    calc = [self recurrenceCalculatorForRecurrenceRule:rule
+                 withFirstInstanceCalendarDateRange:_fir];
+    rs   = [calc recurrenceRangesWithinCalendarDateRange:_r];
+    [ranges addObjectsFromArray:rs];
+  }
+  
+  if (![ranges count])
+    return nil;
+  
+  /* test if any exceptions do match */
+  count = [_exRules count];
+  for (i = 0; i < count; i++) {
+    NSArray *rs;
+
+    rule = [_exRules objectAtIndex:i];
+    if (![rule isKindOfClass:iCalRecurrenceRuleClass])
+      rule = [iCalRecurrenceRule recurrenceRuleWithICalRepresentation:rule];
+
+    calc = [self recurrenceCalculatorForRecurrenceRule:rule
+                 withFirstInstanceCalendarDateRange:_fir];
+    rs   = [calc recurrenceRangesWithinCalendarDateRange:_r];
+    [ranges removeObjectsInArray:rs];
+  }
+  
+  if (![ranges count])
+    return nil;
+  
+  /* exception dates */
+
+  count  = [_exDates count];
+  if (!count) return ranges;
+
+  /* sort out exDates not within range */
+
+  exDates = [NSMutableArray arrayWithCapacity:count];
+  for (i = 0; i < count; i++) {
+    id exDate;
+
+    exDate = [_exDates objectAtIndex:i];
+    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++) {
+    NSCalendarDate      *exDate;
+    NGCalendarDateRange *r;
+    unsigned            k;
+
+    exDate = [exDates objectAtIndex:i];
+    for (k = 0; k < rCount; k++) {
+      unsigned rIdx;
+      
+      rIdx = (rCount - k) - 1;
+      r    = [ranges objectAtIndex:rIdx];
+      if ([r containsDate:exDate]) {
+        [ranges removeObjectAtIndex:rIdx];
+        rCount--;
+        break; /* this is safe because we know that ranges don't overlap */
+      }
+    }
+  }
+  return ranges;
+}
+
+
+/* init */
+
 - (id)initWithRecurrenceRule:(iCalRecurrenceRule *)_rrule
   firstInstanceCalendarDateRange:(NGCalendarDateRange *)_range
 {
@@ -105,6 +217,49 @@ static Class yearlyCalcClass  = Nil;
   return self;  
 }
 
+/* helpers */
+
+- (unsigned)offsetFromSundayForJulianNumber:(long)_jn {
+  return (unsigned)((int)(_jn + 1.5)) % 7;
+}
+
+- (unsigned)offsetFromSundayForWeekDay:(iCalWeekDay)_weekDay {
+  unsigned offset;
+  
+  switch (_weekDay) {
+    case iCalWeekDaySunday:    offset = 0; break;
+    case iCalWeekDayMonday:    offset = 1; break;
+    case iCalWeekDayTuesday:   offset = 2; break;
+    case iCalWeekDayWednesday: offset = 3; break;
+    case iCalWeekDayThursday:  offset = 4; break;
+    case iCalWeekDayFriday:    offset = 5; break;
+    case iCalWeekDaySaturday:  offset = 6; break;
+    default:                   offset = 0; break;
+  }
+  return offset;
+}
+
+- (unsigned)offsetFromSundayForCurrentWeekStart {
+  return [self offsetFromSundayForWeekDay:[self->rrule weekStart]];
+}
+
+- (iCalWeekDay)weekDayForJulianNumber:(long)_jn {
+  unsigned    day;
+  iCalWeekDay weekDay;
+
+  day = [self offsetFromSundayForJulianNumber:_jn];
+  switch (day) {
+    case 0:  weekDay = iCalWeekDaySunday;    break;
+    case 1:  weekDay = iCalWeekDayMonday;    break;
+    case 2:  weekDay = iCalWeekDayTuesday;   break;
+    case 3:  weekDay = iCalWeekDayWednesday; break;
+    case 4:  weekDay = iCalWeekDayThursday;  break;
+    case 5:  weekDay = iCalWeekDayFriday;    break;
+    case 6:  weekDay = iCalWeekDaySaturday;  break;
+    default: weekDay = iCalWeekDaySunday;    break; /* keep compiler happy */
+  }
+  return weekDay;
+}
 
 /* calculation */
 
@@ -203,13 +358,15 @@ static Class yearlyCalcClass  = Nil;
         NGCalendarDateRange *r;
       
         start = [NSCalendarDate dateForJulianNumber:jnCurrent];
+        [start setTimeZone:[firStart timeZone]];
         start = [start hour:  [firStart hourOfDay]
                        minute:[firStart minuteOfHour]
                        second:[firStart secondOfMinute]];
         end   = [start addTimeInterval:[self->firstRange duration]];
         r     = [NGCalendarDateRange calendarDateRangeWithStartDate:start
                                      endDate:end];
-        [ranges addObject:r];
+        if ([_r containsDateRange:r])
+          [ranges addObject:r];
       }
     }
   }
@@ -299,19 +456,68 @@ static Class yearlyCalcClass  = Nil;
           NGCalendarDateRange *r;
           
           start = [NSCalendarDate dateForJulianNumber:jnCurrent];
+          [start setTimeZone:[firStart timeZone]];
           start = [start hour:  [firStart hourOfDay]
                          minute:[firStart minuteOfHour]
                          second:[firStart secondOfMinute]];
           end   = [start addTimeInterval:[self->firstRange duration]];
           r     = [NGCalendarDateRange calendarDateRangeWithStartDate:start
                                        endDate:end];
-          [ranges addObject:r];
+          if ([_r containsDateRange:r])
+            [ranges addObject:r];
         }
       }
     }
   }
   else {
-    [self errorWithFormat:@"Cannot calculate rules with byDayMask, yet!"];
+    long jnFirstWeekStart, weekStartOffset;
+
+    /* calculate jnFirst's week start - this depends on our setting of week
+       start */
+    weekStartOffset = [self offsetFromSundayForJulianNumber:jnFirst] -
+                      [self offsetFromSundayForCurrentWeekStart];
+
+    jnFirstWeekStart = jnFirst - weekStartOffset;
+
+    for (i = 0 ; i < startEndCount; i++) {
+      long jnCurrent;
+
+      jnCurrent = jnStart + i;
+      if (jnCurrent >= jnFirst) {
+        long jnDiff;
+        
+        /* we need to calculate a difference in weeks */
+        jnDiff = (jnCurrent - jnFirstWeekStart) % 7;
+        if ((jnDiff % interval) == 0) {
+          BOOL isRecurrence = NO;
+            
+          if (jnCurrent == jnFirst) {
+            isRecurrence = YES;
+          }
+          else {
+            iCalWeekDay weekDay;
+
+            weekDay = [self weekDayForJulianNumber:jnCurrent];
+            isRecurrence = (weekDay & [self->rrule byDayMask]) ? YES : NO;
+          }
+          if (isRecurrence) {
+            NSCalendarDate      *start, *end;
+            NGCalendarDateRange *r;
+                
+            start = [NSCalendarDate dateForJulianNumber:jnCurrent];
+            [start setTimeZone:[firStart timeZone]];
+            start = [start hour:  [firStart hourOfDay]
+                           minute:[firStart minuteOfHour]
+                           second:[firStart secondOfMinute]];
+            end   = [start addTimeInterval:[self->firstRange duration]];
+            r     = [NGCalendarDateRange calendarDateRangeWithStartDate:start
+                                         endDate:end];
+            if ([_r containsDateRange:r])
+              [ranges addObject:r];
+          }
+        }
+      }
+    }
   }
   return ranges;
 }
@@ -342,7 +548,8 @@ static Class yearlyCalcClass  = Nil;
 - (NSArray *)recurrenceRangesWithinCalendarDateRange:(NGCalendarDateRange *)_r {
   NSMutableArray *ranges;
   NSCalendarDate *firStart, *rStart, *rEnd, *until;
-  unsigned       i, count, interval, diff;
+  unsigned       i, count, interval;
+  int            diff;
 
   firStart = [self->firstRange startDate];
   rStart   = [_r startDate];
@@ -358,23 +565,28 @@ static Class yearlyCalcClass  = Nil;
   }
 
   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++) {
-    unsigned test;
+    int test;
     
     test = diff + i;
-    if ((test % interval) == 0) {
+    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];
-      [ranges addObject:r];
+      if ([_r containsDateRange:r])
+        [ranges addObject:r];
     }
   }
   return ranges;
@@ -402,7 +614,8 @@ static Class yearlyCalcClass  = Nil;
 - (NSArray *)recurrenceRangesWithinCalendarDateRange:(NGCalendarDateRange *)_r {
   NSMutableArray *ranges;
   NSCalendarDate *firStart, *rStart, *rEnd, *until;
-  unsigned       i, count, interval, diff;
+  unsigned       i, count, interval;
+  int            diff;
   
   firStart = [self->firstRange startDate];
   rStart   = [_r startDate];
@@ -418,23 +631,28 @@ static Class yearlyCalcClass  = Nil;
   }
   
   diff   = [firStart yearsBetweenDate:rStart];
+  if ((diff != 0) && [rStart compare:firStart] == NSOrderedAscending)
+    diff = -diff;
+
   count  = [rStart yearsBetweenDate:rEnd] + 1;
   ranges = [NSMutableArray arrayWithCapacity:count];
   for (i = 0 ; i < count; i++) {
-    unsigned test;
-    
+    int test;
+
     test = diff + i;
-    if ((test % interval) == 0) {
+    if ((test >= 0) && (test % interval) == 0) {
       NSCalendarDate      *start, *end;
       NGCalendarDateRange *r;
       
       start = [firStart dateByAddingYears:diff + i
                         months:0
                         days:0];
+      [start setTimeZone:[firStart timeZone]];
       end   = [start addTimeInterval:[self->firstRange duration]];
       r     = [NGCalendarDateRange calendarDateRangeWithStartDate:start
                                    endDate:end];
-      [ranges addObject:r];
+      if ([_r containsDateRange:r])
+        [ranges addObject:r];
     }
   }
   return ranges;