#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;
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) {
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;
}
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];