]> err.no Git - sope/blobdiff - sope-ical/NGiCal/iCalRecurrenceCalculator.m
improved API, bugfix for recurrent dates
[sope] / sope-ical / NGiCal / iCalRecurrenceCalculator.m
index 83b57d8dfddf34fb369264d58eaa87ff3e7ba863..d51e845a8b24d0056f46b3866a73ef77c1bc75e9 100644 (file)
 @interface iCalDailyRecurrenceCalculator : iCalRecurrenceCalculator
 {
 }
-- (unsigned)factor;
 @end
 
-@interface iCalWeeklyRecurrenceCalculator : iCalDailyRecurrenceCalculator
+@interface iCalWeeklyRecurrenceCalculator : iCalRecurrenceCalculator
 {
 }
 @end
 
 @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;
@@ -67,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
 {
@@ -80,6 +92,80 @@ 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;
+  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 are also possible */
+  rCount = [ranges count];
+  count  = [_exDates count];
+  for (i = 0; i < count; i++) {
+    id                  exDate;
+    NGCalendarDateRange *r;
+    unsigned            k;
+
+    exDate = [_exDates objectAtIndex:i];
+    if (![exDate isKindOfClass:NSCalendarDateClass]) {
+      exDate = [NSCalendarDate calendarDateWithICalRepresentation:exDate];
+    }
+    for (k = 0; k < rCount; k++) {
+      r = [ranges objectAtIndex:(rCount - k) - 1];
+      if ([r containsDate:exDate]) {
+        [ranges removeObjectAtIndex:k];
+      }
+    }
+  }
+  return ranges;
+}
+
+
+/* init */
+
 - (id)initWithRecurrenceRule:(iCalRecurrenceRule *)_rrule
   firstInstanceCalendarDateRange:(NGCalendarDateRange *)_range
 {
@@ -106,6 +192,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 */
 
@@ -179,7 +308,7 @@ static Class yearlyCalcClass  = Nil;
       jnRuleLast = [until julianNumber];
     }
     else {
-      jnRuleLast = (interval * [self->rrule repeatCount] * [self factor])
+      jnRuleLast = (interval * [self->rrule repeatCount])
       + jnFirst;
       if (jnRuleLast < jnStart)
         return nil;
@@ -189,27 +318,32 @@ static Class yearlyCalcClass  = Nil;
       jnEnd = jnRuleLast;
   }
 
-  ranges        = [NSMutableArray arrayWithCapacity:5];
   startEndCount = (jnEnd - jnStart) + 1;
+  ranges        = [NSMutableArray arrayWithCapacity:startEndCount];
   for (i = 0 ; i < startEndCount; i++) {
-    long jnTest;
-
-    jnTest = (jnStart + i) - jnFirst;
-    if ((jnTest % interval) == 0) {
-      NSCalendarDate      *start, *end;
-      NGCalendarDateRange *r;
+    long jnCurrent;
     
-      start = [NSCalendarDate dateForJulianNumber:jnStart + i];
-      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];
+    jnCurrent = jnStart + i;
+    if (jnCurrent >= jnFirst) {
+      long jnTest;
+      
+      jnTest = jnCurrent - jnFirst;
+      if ((jnTest % interval) == 0) {
+        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];
+        [ranges addObject:r];
+      }
     }
   }
-  
   return ranges;
 }
 
@@ -221,8 +355,7 @@ static Class yearlyCalcClass  = Nil;
     firStart   = [self->firstRange startDate];
     jnFirst    = [firStart julianNumber];
     jnRuleLast = ([self->rrule repeatInterval] *
-                  [self->rrule repeatCount] *
-                  [self factor]) +
+                  [self->rrule repeatCount]) +
                   jnFirst;
     until      = [NSCalendarDate dateForJulianNumber:jnRuleLast];
     until      = [until hour:  [firStart hourOfDay]
@@ -233,10 +366,6 @@ static Class yearlyCalcClass  = Nil;
   return [super lastInstanceStartDate];
 }
 
-- (unsigned)factor {
-  return 1;
-}
-
 @end /* iCalDailyRecurrenceCalculator */
 
 
@@ -246,8 +375,142 @@ static Class yearlyCalcClass  = Nil;
 */
 @implementation iCalWeeklyRecurrenceCalculator
 
-- (unsigned)factor {
-  return 7;
+- (NSArray *)recurrenceRangesWithinCalendarDateRange:(NGCalendarDateRange *)_r {
+  NSMutableArray *ranges;
+  NSCalendarDate *firStart;
+  long           i, jnFirst, jnStart, jnEnd, startEndCount;
+  unsigned       interval, byDayMask;
+
+  firStart = [self->firstRange startDate];
+  jnFirst  = [firStart julianNumber];
+  jnEnd    = [[_r endDate] julianNumber];
+  
+  if (jnFirst > jnEnd)
+    return nil;
+  
+  jnStart  = [[_r startDate] julianNumber];
+  interval = [self->rrule repeatInterval];
+  
+  /* if rule is bound, check the bounds */
+  if (![self->rrule isInfinite]) {
+    NSCalendarDate *until;
+    long           jnRuleLast;
+    
+    until = [self->rrule untilDate];
+    if (until) {
+      if ([until compare:[_r startDate]] == NSOrderedAscending)
+        return nil;
+      jnRuleLast = [until julianNumber];
+    }
+    else {
+      jnRuleLast = (interval * [self->rrule repeatCount] * 7)
+      + jnFirst;
+      if (jnRuleLast < jnStart)
+        return nil;
+    }
+    /* jnStart < jnRuleLast < jnEnd ? */
+    if (jnEnd > jnRuleLast)
+      jnEnd = jnRuleLast;
+  }
+  
+  startEndCount = (jnEnd - jnStart) + 1;
+  ranges        = [NSMutableArray arrayWithCapacity:startEndCount];
+  byDayMask     = [self->rrule byDayMask];
+  if (!byDayMask) {
+    for (i = 0 ; i < startEndCount; i++) {
+      long jnCurrent;
+      
+      jnCurrent = jnStart + i;
+      if (jnCurrent >= jnFirst) {
+        long jnDiff;
+        
+        jnDiff = jnCurrent - jnFirst; /* difference in days */
+        if ((jnDiff % (interval * 7)) == 0) {
+          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];
+          [ranges addObject:r];
+        }
+      }
+    }
+  }
+  else {
+    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];
+            [ranges addObject:r];
+          }
+        }
+      }
+    }
+  }
+  return ranges;
+}
+
+- (NSCalendarDate *)lastInstanceStartDate {
+  if ([self->rrule repeatCount] > 0) {
+    long           jnFirst, jnRuleLast;
+    NSCalendarDate *firStart, *until;
+    
+    firStart   = [self->firstRange startDate];
+    jnFirst    = [firStart julianNumber];
+    jnRuleLast = ([self->rrule repeatInterval] *
+                  [self->rrule repeatCount] * 7) +
+                  jnFirst;
+    until      = [NSCalendarDate dateForJulianNumber:jnRuleLast];
+    until      = [until hour:  [firStart hourOfDay]
+                        minute:[firStart minuteOfHour]
+                        second:[firStart secondOfMinute]];
+    return until;
+  }
+  return [super lastInstanceStartDate];
 }
 
 @end /* iCalWeeklyRecurrenceCalculator */
@@ -286,6 +549,7 @@ static Class yearlyCalcClass  = Nil;
       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];
@@ -337,7 +601,7 @@ static Class yearlyCalcClass  = Nil;
   ranges = [NSMutableArray arrayWithCapacity:count];
   for (i = 0 ; i < count; i++) {
     unsigned test;
-    
+
     test = diff + i;
     if ((test % interval) == 0) {
       NSCalendarDate      *start, *end;
@@ -346,6 +610,7 @@ static Class yearlyCalcClass  = Nil;
       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];