X-Git-Url: https://err.no/cgi-bin/gitweb.cgi?a=blobdiff_plain;ds=sidebyside;f=sope-ical%2FNGiCal%2FiCalMonthlyRecurrenceCalculator.m;h=b2809edad15ee46dcab0a71809d05cd4cafc61f8;hb=d97b72556b4354e6d444033a952c555139691986;hp=7f04abb0ea233cb3d5601d021fd4fa181da66906;hpb=2d9d8e82856cfc6c5c61b121890d66fa96aa1479;p=sope diff --git a/sope-ical/NGiCal/iCalMonthlyRecurrenceCalculator.m b/sope-ical/NGiCal/iCalMonthlyRecurrenceCalculator.m index 7f04abb0..b2809eda 100644 --- a/sope-ical/NGiCal/iCalMonthlyRecurrenceCalculator.m +++ b/sope-ical/NGiCal/iCalMonthlyRecurrenceCalculator.m @@ -28,34 +28,239 @@ #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; NSTimeZone *timeZone; - NSCalendarDate *firStart, *rStart, *rEnd, *until; - unsigned i, count, interval; + NSCalendarDate *eventStartDate, *rStart, *rEnd, *until; + int eventDayOfMonth; + unsigned monthIdxInRange, numberOfMonthsInRange, interval; int diff; - - firStart = [self->firstRange startDate]; - timeZone = [firStart timeZone]; - rStart = [_r startDate]; - rEnd = [_r endDate]; - interval = [self->rrule repeatInterval]; - until = [self lastInstanceStartDate]; // TODO: maybe replace - - if ([self->rrule byDayMask] != 0) { - [self errorWithFormat:@"cannot process byday part of rrule: %@", - self->rrule]; - return nil; - } + 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]; + + /* check whether the range to be processed is beyond the 'until' date */ if (until != nil) { @@ -64,54 +269,117 @@ if ([until compare:rEnd] == NSOrderedDescending) /* end before until */ rEnd = until; // TODO: why is that? end is _before_ until? } + + + /* 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 + // 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 = [eventStartDate monthsBetweenDate:rStart]; + if ((diff != 0) && [rStart compare:eventStartDate] == 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; + 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; - test = diff + i; + /* first check whether we are in the interval */ - if (test < 0) + if ((monthIdxInRecurrence % interval) != 0) continue; + + /* + 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]; + + + /* check whether we match the bymonth specification */ - if ((test % interval) != 0) + if (!byMonthList[[cursor monthOfYear] - 1]) continue; - start = [firStart dateByAddingYears:0 months:(diff + i) days:0]; - [start setTimeZone:timeZone]; - /* check whether we are still in the limits */ + /* check 'day level' byXYZ rules */ - // 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 */ + didByFill = NO; + + if (byMonthDay != nil) { /* list of days in the month */ + NGMonthDaySet_copyOrUnion(&monthDays, &byMonthDaySet, !didByFill); + didByFill = YES; } - /* create end date */ - - end = [start addTimeInterval:[self->firstRange duration]]; - [end setTimeZone:timeZone]; + if ([self->rrule byDayMask] != 0) { // TODO: replace the mask with an array + NGMonthDaySet ruleset; + unsigned firstDoWInMonth; + + firstDoWInMonth = [[cursor firstDayOfMonth] dayOfWeek]; + + 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; + } - /* create range and check whether its in the requested range */ + // TODO: add processing of byhour/byminute/bysecond etc - r = [[NGCalendarDateRange alloc] initWithStartDate:start endDate:end]; - if ([_r containsDateRange:r]) - [ranges addObject:r]; - [r release]; r = nil; + 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; } @@ -122,10 +390,14 @@ unsigned months, interval; interval = [self->rrule repeatInterval]; - months = ([self->rrule repeatCount] - 1) * 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];