]> err.no Git - sope/blobdiff - sope-ical/NGiCal/iCalMonthlyRecurrenceCalculator.m
gstep-base compile fix
[sope] / sope-ical / NGiCal / iCalMonthlyRecurrenceCalculator.m
index 1eacf1daea18dbf18fa01e9010d3b6103a6d3f1f..b2809edad15ee46dcab0a71809d05cd4cafc61f8 100644 (file)
 #include "iCalRecurrenceRule.h"
 #include "NSCalendarDate+ICal.h"
 #include "common.h"
+#include <string.h>
 
 @interface iCalRecurrenceCalculator(PrivateAPI)
 - (NSCalendarDate *)lastInstanceStartDate;
 @end
 
+// #define HEAVY_DEBUG 1
+
 @implementation iCalMonthlyRecurrenceCalculator
 
+typedef BOOL NGMonthSet[12];
+typedef BOOL NGMonthDaySet[32]; // 0 is unused
+
+static void NGMonthDaySet_clear(NGMonthDaySet *daySet) {
+  register unsigned i;
+  
+  for (i = 1; i <= 31; i++)
+    (*daySet)[i] = NO;
+}
+
+static void NGMonthDaySet_copyOrUnion(NGMonthDaySet *base, NGMonthDaySet *new,
+                                      BOOL doCopy)
+{
+  register unsigned i;
+  
+  if (doCopy)
+    memcpy(base, new, sizeof(NGMonthDaySet));
+  else {
+    for (i = 1; i <= 31; i++) {
+      if (!(*new)[i])
+        (*base)[i] = NO;
+    }
+  }
+}
+
+static BOOL NGMonthDaySet_fillWithByMonthDay(NGMonthDaySet *daySet, 
+                                             NSArray *byMonthDay)
+{
+  /* list of days in the month */
+  unsigned i, count;
+  BOOL ok;
+  
+  NGMonthDaySet_clear(daySet);
+
+  for (i = 0, count = [byMonthDay count], ok = YES; i < count; i++) {
+    int dayInMonth; /* -31..-1 and 1..31 */
+        
+    if ((dayInMonth = [[byMonthDay objectAtIndex:i] intValue]) == 0) {
+      ok = NO;
+      continue; /* invalid value */
+    }
+    if (dayInMonth > 31) {
+      ok = NO;
+      continue; /* error, value to large */
+    }
+    if (dayInMonth < -31) {
+      ok = NO;
+      continue; /* error, value to large */
+    }
+    
+    /* adjust negative days */
+        
+    if (dayInMonth < 0) {
+      /* eg: -1 == last day in month, 30 days => 30 */
+      dayInMonth = 32 - dayInMonth /* because we count from 1 */;
+    }
+    
+    (*daySet)[dayInMonth] = YES;
+  }
+  return ok;
+}
+
+static inline unsigned iCalDoWForNSDoW(int dow) {
+  switch (dow) {
+  case 0: return iCalWeekDaySunday;
+  case 1: return iCalWeekDayMonday;
+  case 2: return iCalWeekDayTuesday;
+  case 3: return iCalWeekDayWednesday;
+  case 4: return iCalWeekDayThursday;
+  case 5: return iCalWeekDayFriday;
+  case 6: return iCalWeekDaySaturday;
+  case 7: return iCalWeekDaySunday;
+  default: return 0;
+  }
+}
+
+#if HEAVY_DEBUG
+static NSString *dowEN[8] = { 
+  @"SU", @"MO", @"TU", @"WE", @"TH", @"FR", @"SA", @"SU-"
+};
+#endif
+
+static void NGMonthDaySet_fillWithByDayX(NGMonthDaySet *daySet, 
+                                         unsigned dayMask,
+                                        unsigned firstDoWInMonth,
+                                        unsigned numberOfDaysInMonth,
+                                         int occurrence1)
+{
+  // TODO: this is called 'X' because the API doesn't allow for full iCalendar
+  //       functionality. The daymask must be a list of occurence+dow
+  register unsigned dayInMonth;
+  register int dow; /* current day of the week */
+  int occurrences[7] = { 0, 0, 0, 0, 0, 0, 0 } ;
+  
+  NGMonthDaySet_clear(daySet);
+  
+  if (occurrence1 >= 0) {
+    for (dayInMonth = 1, dow = firstDoWInMonth; dayInMonth<=31; dayInMonth++) {
+      // TODO: complete me
+      
+      if (dayMask & iCalDoWForNSDoW(dow)) {
+        if (occurrence1 == 0)
+         (*daySet)[dayInMonth] = YES;
+        else { /* occurrence1 > 0 */
+         occurrences[dow] = occurrences[dow] + 1;
+         
+         if (occurrences[dow] == occurrence1) 
+           (*daySet)[dayInMonth] = YES;
+        }
+      }
+      
+      dow = (dow == 6 /* Sat */) ? 0 /* Sun */ : (dow + 1);
+    }
+  }
+  else {
+    int lastDoWInMonthSet;
+    
+    /* get the last dow in the set (not necessarily the month!) */
+    for (dayInMonth = 1, dow = firstDoWInMonth; 
+        dayInMonth < numberOfDaysInMonth;dayInMonth++)
+      dow = (dow == 6 /* Sat */) ? 0 /* Sun */ : (dow + 1);
+    lastDoWInMonthSet = dow;
+    
+#if HEAVY_DEBUG
+    NSLog(@"LAST DOW IN SET: %i / %@", 
+         lastDoWInMonthSet, dowEN[lastDoWInMonthSet]);
+#endif
+    /* start at the end of the set */
+    for (dayInMonth = numberOfDaysInMonth, dow = lastDoWInMonthSet; 
+        dayInMonth >= 1; dayInMonth--) {
+      // TODO: complete me
+      
+#if HEAVY_DEBUG
+      NSLog(@"  CHECK day-of-month %02i, "
+           @" dow=%i/%@ (first=%i/%@, last=%i/%@)",
+           dayInMonth, 
+           dow, dowEN[dow],
+           firstDoWInMonth, dowEN[firstDoWInMonth],
+           lastDoWInMonthSet, dowEN[lastDoWInMonthSet]
+           );
+#endif
+      
+      if (dayMask & iCalDoWForNSDoW(dow)) {
+       occurrences[dow] = occurrences[dow] + 1;
+#if HEAVY_DEBUG
+       NSLog(@"    MATCH %i/%@ count: %i occurences=%i",
+             dow, dowEN[dow], occurrences[dow], occurrence1);
+#endif
+         
+       if (occurrences[dow] == -occurrence1) {
+#if HEAVY_DEBUG
+         NSLog(@"    COUNT MATCH");
+#endif
+         (*daySet)[dayInMonth] = YES;
+       }
+      }
+      
+      dow = (dow == 0 /* Sun */) ? 6 /* Sat */ : (dow - 1);
+    }
+  }
+}
+
+- (BOOL)_addInstanceWithStartDate:(NSCalendarDate *)_startDate
+  limitDate:(NSCalendarDate *)_until
+  limitRange:(NGCalendarDateRange *)_r
+  toArray:(NSMutableArray *)_ranges
+{
+  NGCalendarDateRange *r;
+  NSCalendarDate *end;
+  
+  /* 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:_startDate] == NSOrderedAscending)
+      /* start after until */
+      return NO; /* Note: we assume that the algorithm is sequential */
+  }
+
+  /* create end date */
+
+  end = [_startDate addTimeInterval:[self->firstRange duration]];
+  [end setTimeZone:[_startDate timeZone]];
+    
+  /* create range and check whether its in the requested range */
+  
+  r = [[NGCalendarDateRange alloc] initWithStartDate:_startDate endDate:end];
+  if ([_r containsDateRange:r])
+    [_ranges addObject:r];
+  [r release]; r = nil;
+  
+  return YES;
+}
+
 - (NSArray *)recurrenceRangesWithinCalendarDateRange:(NGCalendarDateRange *)_r{
+  /* main entry */
+  // TODO: check whether this is OK for multiday-events!
   NSMutableArray *ranges;
-  NSCalendarDate *firStart, *rStart, *rEnd, *until;
-  unsigned       i, count, interval;
+  NSTimeZone     *timeZone;
+  NSCalendarDate *eventStartDate, *rStart, *rEnd, *until;
+  int            eventDayOfMonth;
+  unsigned       monthIdxInRange, numberOfMonthsInRange, interval;
   int            diff;
+  NGMonthSet byMonthList = { // TODO: fill from rrule, this is the default
+    YES, YES, YES, YES, YES, YES, 
+    YES, YES, YES, YES, YES, YES
+  };
+  NSArray *byMonthDay = nil; // array of ints (-31..-1 and 1..31)
+  NGMonthDaySet byMonthDaySet;
+  
+  eventStartDate  = [self->firstRange startDate];
+  eventDayOfMonth = [eventStartDate dayOfMonth];
+  timeZone   = [eventStartDate timeZone];
+  rStart     = [_r startDate];
+  rEnd       = [_r endDate];
+  interval   = [self->rrule repeatInterval];
+  until      = [self lastInstanceStartDate]; // TODO: maybe replace
+  byMonthDay = [self->rrule byMonthDay];
+  
 
-  firStart = [self->firstRange startDate];
-  rStart   = [_r startDate];
-  rEnd     = [_r endDate];
-  interval = [self->rrule repeatInterval];
-  until    = [self lastInstanceStartDate];
-
-  if (until) {
-    if ([until compare:rStart] == NSOrderedAscending)
+  /* check whether the range to be processed is beyond the 'until' date */
+  
+  if (until != nil) {
+    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];
-  if ((diff != 0) && [rStart compare:firStart] == NSOrderedAscending)
+  
+  /* precalculate month days (same for all instances) */
+
+  if (byMonthDay != nil)
+    NGMonthDaySet_fillWithByMonthDay(&byMonthDaySet, byMonthDay);
+  
+  
+  // 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 = [eventStartDate monthsBetweenDate:rStart];
+  if ((diff != 0) && [rStart compare:eventStartDate] == NSOrderedAscending)
     diff = -diff;
+  
+  numberOfMonthsInRange  = [rStart monthsBetweenDate:rEnd] + 1;
+  ranges = [NSMutableArray arrayWithCapacity:numberOfMonthsInRange];
+  
+  /* 
+     Note: we do not add 'eventStartDate', this is intentional, the event date
+           itself is _not_ necessarily part of the sequence, eg with monthly
+           byday recurrences.
+  */
+  
+  for (monthIdxInRange = 0; monthIdxInRange < numberOfMonthsInRange; 
+       monthIdxInRange++) {
+    NSCalendarDate *cursor;
+    unsigned       numDaysInMonth;
+    int            monthIdxInRecurrence, dom;
+    NGMonthDaySet  monthDays;
+    BOOL           didByFill, doCont;
+    
+    monthIdxInRecurrence = diff + monthIdxInRange;
+    
+    if (monthIdxInRecurrence < 0)
+      continue;
+    
+    /* first check whether we are in the interval */
+    
+    if ((monthIdxInRecurrence % interval) != 0)
+      continue;
 
-  count  = [rStart monthsBetweenDate:rEnd] + 1;
-  ranges = [NSMutableArray arrayWithCapacity:count];
-  for (i = 0 ; i < count; i++) {
-    int test;
+    /*
+      Then the sequence is:
+      - check whether the month is in the BYMONTH list
+    */
+    
+    cursor = [eventStartDate dateByAddingYears:0
+                             months:(diff + monthIdxInRange)
+                             days:0];
+    [cursor setTimeZone:timeZone];
+    numDaysInMonth = [cursor numberOfDaysInMonth];
     
-    test = diff + i;
-    if ((test >= 0) && (test % interval) == 0) {
-      NSCalendarDate      *start, *end;
-      NGCalendarDateRange *r;
+
+    /* check whether we match the bymonth specification */
+    
+    if (!byMonthList[[cursor monthOfYear] - 1])
+      continue;
+    
+    
+    /* check 'day level' byXYZ rules */
+    
+    didByFill = NO;
+    
+    if (byMonthDay != nil) { /* list of days in the month */
+      NGMonthDaySet_copyOrUnion(&monthDays, &byMonthDaySet, !didByFill);
+      didByFill = YES;
+    }
+    
+    if ([self->rrule byDayMask] != 0) { // TODO: replace the mask with an array
+      NGMonthDaySet ruleset;
+      unsigned firstDoWInMonth;
+      
+      firstDoWInMonth = [[cursor firstDayOfMonth] dayOfWeek];
       
-      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];
+      NGMonthDaySet_fillWithByDayX(&ruleset, 
+                                   [self->rrule byDayMask],
+                                  firstDoWInMonth,
+                                  [cursor numberOfDaysInMonth],
+                                   [self->rrule byDayOccurence1]);
+      NGMonthDaySet_copyOrUnion(&monthDays, &ruleset, !didByFill);
+      didByFill = YES;
+    }
+    
+    if (!didByFill) {
+      /* no rules applied, take the dayOfMonth of the startDate */
+      NGMonthDaySet_clear(&monthDays);
+      monthDays[eventDayOfMonth] = YES;
     }
+    
+    // TODO: add processing of byhour/byminute/bysecond etc
+    
+    for (dom = 1, doCont = YES; dom <= numDaysInMonth && doCont; dom++) {
+      NSCalendarDate *start;
+      
+      if (!monthDays[dom])
+       continue;
+      
+      if (eventDayOfMonth == dom)
+       start = cursor;
+      else {
+       start = [cursor dateByAddingYears:0 months:0
+                       days:(dom - eventDayOfMonth)];
+      }
+      
+      doCont = [self _addInstanceWithStartDate:start
+                    limitDate:until
+                    limitRange:_r
+                    toArray:ranges];
+    }
+    if (!doCont) break; /* reached some limit */
   }
   return ranges;
 }
   if ([self->rrule repeatCount] > 0) {
     NSCalendarDate *until;
     unsigned       months, interval;
-
+    
     interval = [self->rrule repeatInterval];
-    months   = [self->rrule repeatCount] * interval;
-    until    = [[self->firstRange startDate] dateByAddingYears:0
-                                             months:months
-                                             days:0];
+    months   = [self->rrule repeatCount] - 1 /* the first counts as one! */;
+    
+    if (interval > 0)
+      months *= interval;
+    
+    until = [[self->firstRange startDate] dateByAddingYears:0
+                                          months:months
+                                          days:0];
     return until;
   }
   return [super lastInstanceStartDate];