@implementation iCalMonthlyRecurrenceCalculator
typedef BOOL NGMonthSet[12];
-typedef BOOL NGMonthDaySet[31];
+typedef BOOL NGMonthDaySet[32]; // 0 is unused
static void NGMonthDaySet_clear(NGMonthDaySet *daySet) {
register unsigned i;
- for (i = 0; i < 31; i++)
+ for (i = 1; i <= 31; i++)
(*daySet)[i] = NO;
}
if (doCopy)
memcpy(base, new, sizeof(NGMonthDaySet));
else {
- for (i = 0; i < 31; i++) {
+ for (i = 1; i <= 31; i++) {
if (!(*new)[i])
(*base)[i] = NO;
}
}
}
-static void NGMonthDaySet_fillWithByMonthDay(NGMonthDaySet *daySet,
- NSArray *byMonthDay,
- int numDaysInMonth)
+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]; i < count; i++) {
+ 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)
+ if ((dayInMonth = [[byMonthDay objectAtIndex:i] intValue]) == 0) {
+ ok = NO;
continue; /* invalid value */
- if (dayInMonth > numDaysInMonth)
- continue; /* this month has less days */
- if (dayInMonth < -numDaysInMonth)
- continue; /* this month has less days */
-
+ }
+ 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) {
(*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;
+ }
}
static void NGMonthDaySet_fillWithByDayX(NGMonthDaySet *daySet,
unsigned dayMask,
+ unsigned firstDoWInMonth,
int occurrence1)
{
- unsigned i, count;
+ // 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 unsigned dow; /* current day of the week */
+ int occurrences[7] = { 0, 0, 0, 0, 0, 0, 0 } ;
NGMonthDaySet_clear(daySet);
- // TODO: complete me
+
+ for (dayInMonth = 1, dow = firstDoWInMonth; dayInMonth <= 31; dayInMonth++) {
+ // TODO: complete me
+
+ if (dayMask & iCalDoWForNSDoW(dow)) {
+ if (occurrence1 == 0)
+ (*daySet)[dayInMonth] = YES;
+ else {
+ occurrences[dow] = occurrences[dow] + 1;
+
+ if (occurrences[dow] == occurrence1)
+ (*daySet)[dayInMonth] = YES;
+ }
+ }
+
+ dow = (dow == 6 /* Sat */) ? 0 /* Sun */ : (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 *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
};
NSArray *byMonthDay = nil; // array of ints (-31..-1 and 1..31)
+ NGMonthDaySet byMonthDaySet;
- eventStartDate = [self->firstRange startDate];
- timeZone = [eventStartDate 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;
- }
+ 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
// requested range. Not sure whether this is actually possible, eg
for (monthIdxInRange = 0; monthIdxInRange < numberOfMonthsInRange;
monthIdxInRange++) {
- NSCalendarDate *cursor;
- NSCalendarDate *start, *end;
- NGCalendarDateRange *r;
- unsigned numDaysInMonth;
- int monthIdxInRecurrence;
- NGMonthDaySet monthDays;
- BOOL didByFill;
+ NSCalendarDate *cursor;
+ unsigned numDaysInMonth;
+ int monthIdxInRecurrence, dom;
+ NGMonthDaySet monthDays;
+ BOOL didByFill, doCont;
monthIdxInRecurrence = diff + monthIdxInRange;
didByFill = NO;
if (byMonthDay != nil) { /* list of days in the month */
- NGMonthDaySet ruleset;
-
- NGMonthDaySet_fillWithByMonthDay(&ruleset, byMonthDay, numDaysInMonth);
- NGMonthDaySet_copyOrUnion(&monthDays, &ruleset, !didByFill);
+ 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];
NGMonthDaySet_fillWithByDayX(&ruleset,
[self->rrule byDayMask],
+ firstDoWInMonth,
[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 */
-
- // 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 */
+ if (!didByFill) {
+ /* no rules applied, take the dayOfMonth of the startDate */
+ NGMonthDaySet_clear(&monthDays);
+ monthDays[eventDayOfMonth] = YES;
}
- /* create end date */
-
- end = [start addTimeInterval:[self->firstRange duration]];
- [end setTimeZone:timeZone];
+ // TODO: add processing of byhour/byminute/bysecond etc
- /* create range and check whether its in the requested range */
-
- 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;
}
}
- (void)dealloc {
+ [self->byMonthDay release];
[self->untilDate release];
[self->rrule release];
[super dealloc];
}
- (void)setUntilDate:(NSCalendarDate *)_untilDate {
- ASSIGN(self->untilDate, _untilDate);
+ ASSIGNCOPY(self->untilDate, _untilDate);
}
- (NSCalendarDate *)untilDate {
return self->untilDate;
return self->byDayOccurence1;
}
+- (NSArray *)byMonthDay {
+ return self->byMonthDay;
+}
+
- (BOOL)isInfinite {
return (self->repeatCount != 0 || self->untilDate) ? NO : YES;
}
*/
- (NSString *)byDayList {
NSMutableString *s;
- unsigned i, mask, day;
+ unsigned dow, mask, day;
BOOL needsComma;
s = [NSMutableString stringWithCapacity:20];
mask = self->byDay.mask;
day = iCalWeekDayMonday;
- for (i = 0; i < 7; i++) {
+ for (dow = 0 /* Sun */; dow < 7; dow++) {
if (mask & day) {
if (needsComma)
[s appendString:@","];
- else if (self->byDay.useOccurence)
- // Note: we only support one occurrence currently
+
+ if (self->byDay.useOccurence)
+ // Note: we only support one occurrence for all currently
[s appendFormat:@"%i", self->byDayOccurence1];
[s appendString:[self iCalRepresentationForWeekDay:day]];
NSString *pUntil = nil;
NSString *pCount = nil;
NSString *pByday = nil;
+ NSString *pBymday = nil;
NSString *pBysetpos = nil;
props = [_rrule componentsSeparatedByString:@";"];
vHolder = NULL;
switch ([key characterAtIndex:0]) {
case 'b':
- if ([key isEqualToString:@"byday"]) vHolder = &pByday; break;
- if ([key isEqualToString:@"bysetpos"]) vHolder = &pBysetpos; break;
+ if ([key isEqualToString:@"byday"]) { vHolder = &pByday; break; }
+ if ([key isEqualToString:@"bymonthday"]) { vHolder = &pBymday; break; }
+ if ([key isEqualToString:@"bysetpos"]) { vHolder = &pBysetpos; break; }
break;
case 'c':
- if ([key isEqualToString:@"count"]) vHolder = &pCount; break;
+ if ([key isEqualToString:@"count"]) { vHolder = &pCount; break; }
break;
case 'f':
- if ([key isEqualToString:@"freq"]) vHolder = &pFrequency; break;
+ if ([key isEqualToString:@"freq"]) { vHolder = &pFrequency; break; }
break;
case 'u':
- if ([key isEqualToString:@"until"]) vHolder = &pUntil; break;
+ if ([key isEqualToString:@"until"]) { vHolder = &pUntil; break; }
break;
default:
break;
else {
// TODO: we should just parse known keys and put remainders into a
// separate dictionary
- //[self logWithFormat:@"TODO: add explicit support for key: %@", key];
+ [self logWithFormat:@"TODO: add explicit support for key: %@", key];
[self takeValue:value forKey:key];
}
}
// TODO: we should parse byday in here
if (pByday != nil) [self setByday:pByday];
[pByday release]; pByday = nil;
+
+ if (pBymday != nil) {
+ NSArray *t;
+
+ t = [pBymday componentsSeparatedByString:@","];
+ ASSIGNCOPY(self->byMonthDay, t);
+ }
+ [pBymday release]; pBymday = nil;
if (pBysetpos != nil)
// TODO: implement
int offset;
occurence = [iCalDay intValue];
-
- offset = 1;
+
+ offset = 1; /* skip occurence */
while (offset < len && isdigit([iCalDay characterAtIndex:offset]))
offset++;
iCalDay = [iCalDay substringFromIndex:offset];
- if (self->byDay.useOccurence) {
+ if (self->byDay.useOccurence && (occurence != self->byDayOccurence1)) {
[self errorWithFormat:
@"we only supported one occurence (occ=%i,day=%@): '%@'",
occurence, iCalDay, _byDayList];