X-Git-Url: https://err.no/cgi-bin/gitweb.cgi?a=blobdiff_plain;f=sope-ical%2FNGiCal%2FiCalMonthlyRecurrenceCalculator.m;h=b2809edad15ee46dcab0a71809d05cd4cafc61f8;hb=d97b72556b4354e6d444033a952c555139691986;hp=1eacf1daea18dbf18fa01e9010d3b6103a6d3f1f;hpb=36123d033a97586a9516bb52f6083952e50826a8;p=sope diff --git a/sope-ical/NGiCal/iCalMonthlyRecurrenceCalculator.m b/sope-ical/NGiCal/iCalMonthlyRecurrenceCalculator.m index 1eacf1da..b2809eda 100644 --- a/sope-ical/NGiCal/iCalMonthlyRecurrenceCalculator.m +++ b/sope-ical/NGiCal/iCalMonthlyRecurrenceCalculator.m @@ -28,56 +28,358 @@ #include "iCalRecurrenceRule.h" #include "NSCalendarDate+ICal.h" #include "common.h" +#include @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; } @@ -86,12 +388,16 @@ 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];