sogo/SOPE/NGCards/iCalMonthlyRecurrenceCalculator.m
Wolfgang Sourdeau cd510f7473 Monotone-Parent: 61d11066e0e001f91446e76044b712194a177089
Monotone-Revision: 4e643e3e8f08c6cdd2abe4483bdb2cdb7dc15066

Monotone-Author: wsourdeau@inverse.ca
Monotone-Date: 2007-06-07T16:17:51
Monotone-Branch: ca.inverse.sogo
2007-06-07 16:17:51 +00:00

408 lines
12 KiB
Objective-C

/*
Copyright (C) 2004-2005 SKYRIX Software AG
This file is part of SOPE.
SOPE is free software; you can redistribute it and/or modify it under
the terms of the GNU Lesser General Public License as published by the
Free Software Foundation; either version 2, or (at your option) any
later version.
SOPE is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or
FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
License for more details.
You should have received a copy of the GNU Lesser General Public
License along with SOPE; see the file COPYING. If not, write to the
Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
02111-1307, USA.
*/
#import <NGExtensions/NSCalendarDate+misc.h>
#import "iCalRecurrenceCalculator.h"
@interface iCalMonthlyRecurrenceCalculator : iCalRecurrenceCalculator
@end
#import <NGExtensions/NGCalendarDateRange.h>
#import "iCalRecurrenceRule.h"
#import "NSCalendarDate+ICal.h"
#import <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 *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];
/* 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) /* 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
// 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;
/*
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 */
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];
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;
}
- (NSCalendarDate *)lastInstanceStartDate {
if ([self->rrule repeatCount] > 0) {
NSCalendarDate *until;
unsigned months, interval;
interval = [self->rrule repeatInterval];
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];
}
@end /* iCalMonthlyRecurrenceCalculator */