@implementation iCalMonthlyRecurrenceCalculator
+typedef BOOL NGMonthSet[12];
+typedef BOOL NGMonthDaySet[31];
+
+static void NGMonthDaySet_clear(NGMonthDaySet *daySet) {
+ register unsigned i;
+
+ for (i = 0; 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 = 0; i < 31; i++) {
+ if (!(*new)[i])
+ (*base)[i] = NO;
+ }
+ }
+}
+
+static void NGMonthDaySet_fillWithByMonthDay(NGMonthDaySet *daySet,
+ NSArray *byMonthDay,
+ int numDaysInMonth)
+{
+ /* list of days in the month */
+ unsigned i, count;
+
+ NGMonthDaySet_clear(daySet);
+
+ for (i = 0, count = [byMonthDay count]; i < count; i++) {
+ int dayInMonth; /* -31..-1 and 1..31 */
+
+ if ((dayInMonth = [[byMonthDay objectAtIndex:i] intValue]) == 0)
+ continue; /* invalid value */
+ if (dayInMonth > numDaysInMonth)
+ continue; /* this month has less days */
+ if (dayInMonth < -numDaysInMonth)
+ continue; /* this month has less days */
+
+ /* 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;
+ }
+}
+
+static void NGMonthDaySet_fillWithByDayX(NGMonthDaySet *daySet,
+ unsigned dayMask,
+ int occurrence1)
+{
+ unsigned i, count;
+
+ NGMonthDaySet_clear(daySet);
+ // TODO: complete me
+}
+
- (NSArray *)recurrenceRangesWithinCalendarDateRange:(NGCalendarDateRange *)_r{
/* main entry */
NSMutableArray *ranges;
NSTimeZone *timeZone;
- NSCalendarDate *firStart, *rStart, *rEnd, *until;
- unsigned i, count, interval;
+ NSCalendarDate *eventStartDate, *rStart, *rEnd, *until;
+ unsigned monthIdxInRange, numberOfMonthsInRange, interval;
int diff;
-
- firStart = [self->firstRange startDate];
- timeZone = [firStart timeZone];
+ 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)
+
+ eventStartDate = [self->firstRange startDate];
+ timeZone = [eventStartDate timeZone];
rStart = [_r startDate];
rEnd = [_r endDate];
interval = [self->rrule repeatInterval];
rEnd = until; // TODO: why is that? end is _before_ until?
}
- // 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++) {
+ 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;
NSCalendarDate *start, *end;
NGCalendarDateRange *r;
- int test;
+ unsigned numDaysInMonth;
+ int monthIdxInRecurrence;
+ NGMonthDaySet monthDays;
+ BOOL didByFill;
- test = diff + i;
+ monthIdxInRecurrence = diff + monthIdxInRange;
- if (test < 0)
+ if (monthIdxInRecurrence < 0)
continue;
- if ((test % interval) != 0)
+ /* first check whether we are in the interval */
+
+ 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 (!byMonthList[[cursor monthOfYear] - 1])
+ continue;
+
+
+ /* check 'day level' byXYZ rules */
- start = [firStart dateByAddingYears:0 months:(diff + i) days:0];
- [start setTimeZone:timeZone];
+ didByFill = NO;
+
+ if (byMonthDay != nil) { /* list of days in the month */
+ NGMonthDaySet ruleset;
+
+ NGMonthDaySet_fillWithByMonthDay(&ruleset, byMonthDay, numDaysInMonth);
+ NGMonthDaySet_copyOrUnion(&monthDays, &ruleset, !didByFill);
+ didByFill = YES;
+ }
+
+ if ([self->rrule byDayMask] != 0) { // TODO: replace the mask with an array
+ NGMonthDaySet ruleset;
+
+ NGMonthDaySet_fillWithByDayX(&ruleset,
+ [self->rrule byDayMask],
+ [self->rrule byDayOccurence1]);
+ NGMonthDaySet_copyOrUnion(&monthDays, &ruleset, !didByFill);
+ didByFill = YES;
+ }
+
+
+ // TODO: complete byday support
+
+ /* set start date */
+
+ start = cursor;
/* check whether we are still in the limits */
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];