diff --git a/.mtn-ignore b/.mtn-ignore index 4473e0e35..e0b5524a8 100644 --- a/.mtn-ignore +++ b/.mtn-ignore @@ -2,7 +2,6 @@ config.make shared_debug_obj obj err -log build\.log imgs-.* diff diff --git a/NEWS b/NEWS index 8d0656d60..3ed552211 100644 --- a/NEWS +++ b/NEWS @@ -4,10 +4,14 @@ - added support for LDAP password policies - added support for custom Sieve filters - fixed timezone issues occurring specifically in the southern hemisphere +- updated ckeditor to version 3.2 - tabs: enabled the scrolling when overflowing - updated Czech translation, thanks to Milos Wimmer - removed remaining .wo templates, thereby easing the effort for future translations - fixed regressions with Courier IMAP and Dovecot +- added support for BYDAY with multiple values and negative positions +- added support for BYMONTHDAY with multiple values and negative positions +- added support for BYMONTH with multiple values - added ability to delete events from a keypress - added the "remove" command to "sogo-tool", in order to remove user data and settings - added the ability to export address books in LDIF format from the web interface diff --git a/SOPE/NGCards/ChangeLog b/SOPE/NGCards/ChangeLog index 3c45fc14d..98b418634 100644 --- a/SOPE/NGCards/ChangeLog +++ b/SOPE/NGCards/ChangeLog @@ -1,3 +1,41 @@ +2010-04-19 Francis Lachapelle + + * iCalByDayMask.[h|m]: new class that computes complex BYDAY constraints. + + * iCalRecurrenceRule.m (-byDayMask): new method that returns an + instance of iCalByDayMask. + (-byMonth): new method that returns an array with the values of + the BYMONTH constraint. + (-hasByMask): new method that returns true if the rule has at + least one BYxxx mask defined. + + * iCalDailyRecurrenceCalculator.m + (-recurrenceRangesWithinCalendarDateRange:): make use of the new + iCalByDayMask class. + (-lastInstanceStartDate): make use of the previous method when a + BYxxx mask is defined in the rule. + + * iCalWeeklyRecurrenceCalculator.m + (-recurrenceRangesWithinCalendarDateRange:): make use of the new + iCalByDayMask class. + (-lastInstanceStartDate): make use of the previous method when a + BYxxx mask is defined in the rule. + + * iCalMonthlyRecurrenceCalculator.m + (-recurrenceRangesWithinCalendarDateRange:): added support for + BYMONTH, negative BYMONTHDAY, and BYDAY. + (-lastInstanceStartDate): make use of the previous method when a + BYxxx mask is defined in the rule. + + * iCalYearlyRecurrenceCalculator.m + (-recurrenceRangesWithinCalendarDateRange:): added support for + BYxxx constraints using the iCalMonthlyRecurrenceCalculator class. + (-lastInstanceStartDate): make use of the previous method when a + BYxxx mask is defined in the rule. + + * iCalTimeZonePeriod.m (-_occurenceForDate:byRRule:): make use of + the new iCalByDayMask class. + 2010-04-09 Wolfgang Sourdeau * iCalEvent.m (-propertyValue:): new method that accept a diff --git a/SOPE/NGCards/GNUmakefile b/SOPE/NGCards/GNUmakefile index a574802e3..e35b4f71a 100644 --- a/SOPE/NGCards/GNUmakefile +++ b/SOPE/NGCards/GNUmakefile @@ -31,6 +31,7 @@ libNGCards_HEADER_FILES = \ NGCards.h \ iCalAlarm.h \ iCalAttachment.h \ + iCalByDayMask.h \ iCalCalendar.h \ iCalDataSource.h \ iCalDateTime.h \ @@ -78,6 +79,7 @@ libNGCards_OBJC_FILES = \ \ iCalAlarm.m \ iCalAttachment.m \ + iCalByDayMask.m \ iCalCalendar.m \ iCalDailyRecurrenceCalculator.m \ iCalDateTime.m \ diff --git a/SOPE/NGCards/iCalDailyRecurrenceCalculator.m b/SOPE/NGCards/iCalDailyRecurrenceCalculator.m index 233f531e1..2563baa36 100644 --- a/SOPE/NGCards/iCalDailyRecurrenceCalculator.m +++ b/SOPE/NGCards/iCalDailyRecurrenceCalculator.m @@ -1,6 +1,7 @@ /* Copyright (C) 2004-2005 SKYRIX Software AG - + Copyright (C) 2006-2010 Inverse inc. + This file is part of SOPE. SOPE is free software; you can redistribute it and/or modify it under @@ -29,6 +30,7 @@ #import "iCalRecurrenceCalculator.h" #import "iCalRecurrenceRule.h" +#import "iCalByDayMask.h" @interface iCalDailyRecurrenceCalculator : iCalRecurrenceCalculator @end @@ -39,71 +41,122 @@ @implementation iCalDailyRecurrenceCalculator + /** + * TODO : Unsupported conditions for DAILY recurrences : + * + * BYYEAR + * BYYEARDAY + * BYWEEKNO + * BYMONTH + * BYMONTHDAY + * BYHOUR + * BYMINUTE + * + * There's no GUI to defined such conditions, so there's no + * problem for now. + */ - (NSArray *) recurrenceRangesWithinCalendarDateRange: (NGCalendarDateRange *) _r { NSMutableArray *ranges; NSCalendarDate *firStart, *startDate, *endDate, *currentStartDate, *currentEndDate; - long i; + iCalByDayMask *dayMask; + long i, count, repeatCount; unsigned interval; firStart = [firstRange startDate]; startDate = [_r startDate]; endDate = [_r endDate]; - + dayMask = nil; + repeatCount = 0; + if ([endDate compare: firStart] == NSOrderedAscending) // Range ends before first occurrence return nil; interval = [rrule repeatInterval]; + + if ([[rrule byDay] length]) + dayMask = [rrule byDayMask]; // If rule is bound, check the bounds - if (![rrule isInfinite]) + if (![rrule isInfinite]) { NSCalendarDate *until, *lastDate; + lastDate = nil; until = [rrule untilDate]; - if (until) - lastDate = until; + if (until) + { + lastDate = until; + } else - lastDate = [firStart dateByAddingYears: 0 months: 0 - days: (interval - * ([rrule repeatCount] - 1))]; - - if ([lastDate compare: startDate] == NSOrderedAscending) - // Range starts after last occurrence - return nil; + { + repeatCount = [rrule repeatCount]; + if (dayMask == nil) + // If there's no day mask, we can compute the date of the last + // occurrence of the recurrent rule. + lastDate = [firStart dateByAddingYears: 0 months: 0 + days: (interval + * (repeatCount - 1))]; + } - if ([lastDate compare: endDate] == NSOrderedAscending) - // Range ends after last occurence; adjust end date - endDate = lastDate; + if (lastDate != nil) + { + if ([lastDate compare: startDate] == NSOrderedAscending) + // Range starts after last occurrence + return nil; + + if ([lastDate compare: endDate] == NSOrderedAscending) + // Range ends after last occurence; adjust end date + endDate = lastDate; + } } currentStartDate = [firStart copy]; [currentStartDate autorelease]; ranges = [NSMutableArray array]; i = 1; - + count = 0; + while ([currentStartDate compare: endDate] == NSOrderedAscending || [currentStartDate compare: endDate] == NSOrderedSame) { - if ([startDate compare: currentStartDate] == NSOrderedAscending || - [startDate compare: currentStartDate] == NSOrderedSame) + BOOL wrongDay, isFirStart; + + wrongDay = NO; + isFirStart = NO; + + if (i == 1) + { + isFirStart = YES; + count++; + } + else if (repeatCount > 0 && dayMask) + { + // If the rule count is defined, stop once the count is reached. + if ([dayMask occursOnDay: [currentStartDate dayOfWeek]]) + count++; + else + wrongDay = YES; + + if (count > repeatCount) + break; + } + + if (wrongDay == NO && + ([startDate compare: currentStartDate] == NSOrderedAscending || + [startDate compare: currentStartDate] == NSOrderedSame)) { - BOOL wrongDay = NO; - unsigned int mask; NGCalendarDateRange *r; - if ([rrule byDayMask]) + if (isFirStart == NO && dayMask && repeatCount == 0) { - mask = ([currentStartDate dayOfWeek] - ? (unsigned int) 1 << ([currentStartDate dayOfWeek]) - : iCalWeekDaySunday); - if (([rrule byDayMask] & mask) != mask) + if (![dayMask occursOnDay: [currentStartDate dayOfWeek]]) wrongDay = YES; } - if (wrongDay == NO) + if (isFirStart == YES || wrongDay == NO) { currentEndDate = [currentStartDate addTimeInterval: [firstRange duration]]; r = [NGCalendarDateRange calendarDateRangeWithStartDate: currentStartDate @@ -115,6 +168,13 @@ currentStartDate = [firStart dateByAddingYears: 0 months: 0 days: (interval * i)]; + + if (repeatCount > 0 && count == repeatCount) + // The count variable is only usefull when a BYDAY constraint is + // defined; when there's no BYDAY constraint, the endDate has been + // adjusted to match the repeat count, if defined. + break; + i++; } return ranges; @@ -123,14 +183,29 @@ - (NSCalendarDate *) lastInstanceStartDate { NSCalendarDate *firStart, *lastInstanceStartDate; - + NGCalendarDateRange *r; + NSArray *instances; + + lastInstanceStartDate = nil; if ([rrule repeatCount] > 0) { - firStart = [firstRange startDate]; - - lastInstanceStartDate = [firStart dateByAddingYears: 0 months: 0 - days: ([rrule repeatInterval] - * ([rrule repeatCount] - 1))]; + if ([rrule hasByMask]) + { + // Must perform the complete calculation + firStart = [firstRange startDate]; + r = [NGCalendarDateRange calendarDateRangeWithStartDate: firStart + endDate: [NSCalendarDate distantFuture]]; + instances = [self recurrenceRangesWithinCalendarDateRange: r]; + if ([instances count]) + lastInstanceStartDate = [(NGCalendarDateRange *)[instances lastObject] startDate]; + } + else + { + // No BYxxx mask + lastInstanceStartDate = [firStart dateByAddingYears: 0 months: 0 + days: ([rrule repeatInterval] + * ([rrule repeatCount] - 1))]; + } } else lastInstanceStartDate = [super lastInstanceStartDate]; diff --git a/SOPE/NGCards/iCalMonthlyRecurrenceCalculator.m b/SOPE/NGCards/iCalMonthlyRecurrenceCalculator.m index 2ee949616..a208291fb 100644 --- a/SOPE/NGCards/iCalMonthlyRecurrenceCalculator.m +++ b/SOPE/NGCards/iCalMonthlyRecurrenceCalculator.m @@ -1,6 +1,7 @@ /* Copyright (C) 2004-2007 SKYRIX Software AG Copyright (C) 2007 Helge Hess + Copyright (C) 2010 Inverse inc. This file is part of SOPE. @@ -29,8 +30,11 @@ #import #import "iCalRecurrenceRule.h" +#import "iCalByDayMask.h" #import "NSCalendarDate+ICal.h" + #import +#import @interface iCalRecurrenceCalculator (PrivateAPI) @@ -74,15 +78,20 @@ NGMonthDaySet_copyOrUnion (NGMonthDaySet *base, NGMonthDaySet *new, } } -static BOOL NGMonthDaySet_fillWithByMonthDay (NGMonthDaySet *daySet, +/** + * This method fills split the positions of the BYMONTHDAY constraints + * into two separate arrays: one for the positive positions and one for the + * negatives positions (converted to their absolute values). + */ +static BOOL NGMonthDaySet_fillWithByMonthDay (NGMonthDaySet *positiveDaySet, + NGMonthDaySet *negativeDaySet, NSArray *byMonthDay) - { - /* list of days in the month */ unsigned i, count; BOOL ok; - NGMonthDaySet_clear (daySet); + NGMonthDaySet_clear (positiveDaySet); + NGMonthDaySet_clear (negativeDaySet); for (i = 0, count = [byMonthDay count], ok = YES; i < count; i++) { @@ -98,7 +107,7 @@ static BOOL NGMonthDaySet_fillWithByMonthDay (NGMonthDaySet *daySet, ok = NO; continue; /* error, value to large */ } - if (dayInMonth < -31) + if (dayInMonth < -31) { ok = NO; continue; /* error, value to large */ @@ -107,19 +116,16 @@ static BOOL NGMonthDaySet_fillWithByMonthDay (NGMonthDaySet *daySet, /* 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; + (*negativeDaySet)[abs(dayInMonth)] = YES; + else + (*positiveDaySet)[dayInMonth] = YES; } return ok; } static inline unsigned iCalDoWForNSDoW (int dow) { - switch (dow) + switch (dow) { case 0: return iCalWeekDaySunday; case 1: return iCalWeekDayMonday; @@ -133,100 +139,6 @@ static inline unsigned iCalDoWForNSDoW (int dow) } } -#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 @@ -238,10 +150,7 @@ static void NGMonthDaySet_fillWithByDayX (NGMonthDaySet *daySet, /* 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) + if (_until != nil) { /* Note: the 'until' in the rrule is inclusive as per spec */ if ([_until compare: _startDate] == NSOrderedAscending) @@ -259,29 +168,42 @@ static void NGMonthDaySet_fillWithByDayX (NGMonthDaySet *daySet, r = [[NGCalendarDateRange alloc] initWithStartDate: _startDate endDate: end]; if ([_r containsDateRange: r]) [_ranges addObject: r]; - [r release]; r = nil; + [r release]; + r = nil; return YES; } + /** + * TODO : Unsupported conditions for MONTHLY recurrences : + * + * BYYEAR + * BYYEARDAY + * BYWEEKNO + * BYHOUR + * BYMINUTE + * + * There's no GUI to defined such conditions, so there's no + * problem for now. + */ - (NSArray *) recurrenceRangesWithinCalendarDateRange: (NGCalendarDateRange *) _r { - /* main entry */ // TODO: check whether this is OK for multiday-events! NSMutableArray *ranges; NSTimeZone *timeZone; - NSCalendarDate *eventStartDate, *rStart, *rEnd, *until; + NSCalendarDate *eventStartDate, *rStart, *rEnd, *until, *referenceDate; int eventDayOfMonth; - unsigned monthIdxInRange, numberOfMonthsInRange, interval; - int diff; - NGMonthSet byMonthList = { // TODO: fill from rrule, this is the default - /* enable all months of the year */ + unsigned monthIdxInRange, numberOfMonthsInRange, interval, repeatCount; + int diff, count; + NGMonthSet byMonthList = { + // Enable all months of the year YES, YES, YES, YES, YES, YES, YES, YES, YES, YES, YES, YES }; - NSArray *byMonthDay; // array of ints (-31..-1 and 1..31) - NGMonthDaySet byMonthDaySet; + NSArray *byMonth, *byMonthDay; // array of ints (-31..-1 and 1..31) + NGMonthDaySet byPositiveMonthDaySet, byNegativeMonthDaySet; + iCalByDayMask *byDayMask; eventStartDate = [firstRange startDate]; eventDayOfMonth = [eventStartDate dayOfMonth]; @@ -289,48 +211,81 @@ static void NGMonthDaySet_fillWithByDayX (NGMonthDaySet *daySet, rStart = [_r startDate]; rEnd = [_r endDate]; interval = [rrule repeatInterval]; - until = [self lastInstanceStartDate]; // TODO: maybe replace + until = nil; + repeatCount = [rrule repeatCount]; + byMonth = [rrule byMonth]; byMonthDay = [rrule byMonthDay]; + byDayMask = [rrule byDayMask]; + diff = 0; - - /* check whether the range to be processed is beyond the 'until' date */ - if (until) + if (![rrule isInfinite]) { - if ([until compare: rStart] == NSOrderedAscending) /* until before start */ + if (repeatCount > 0 && ![rrule hasByMask]) + { + // When there's no BYxxx mask, we can find the date of the last + // occurrence. + until = [eventStartDate dateByAddingYears: 0 + months: (interval * (repeatCount - 1)) + days: 0]; + } + else + { + until = [rrule untilDate]; + } + } + + if (until != nil) + { + if ([until compare: rStart] == NSOrderedAscending) + // Range starts after last occurrence return nil; - if ([until compare: rEnd] == NSOrderedDescending) /* end before until */ - rEnd = until; // TODO: why is that? end is _before_ until? + if ([until compare: rEnd] == NSOrderedDescending) + // Range ends after last occurence; adjust end date + rEnd = until; } - - /* precalculate month days (same for all instances) */ + if (byMonth && [byMonth count] > 0) + { + int i; + for (i = 0; i <= 12; i++) + byMonthList[i] = [byMonth containsObject: [NSString stringWithFormat: @"%i", i + 1]]; + } - if (byMonthDay) + /* precalculate month days */ + + if (byMonthDay) { -#if HEAVY_DEBUG - NSLog (@"byMonthDay: %@", byMonthDay); -#endif - NGMonthDaySet_fillWithByMonthDay (&byMonthDaySet, byMonthDay); + NGMonthDaySet_fillWithByMonthDay (&byPositiveMonthDaySet, &byNegativeMonthDaySet, 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; + if (repeatCount > 0) + { + numberOfMonthsInRange = [eventStartDate monthsBetweenDate: rEnd] + 1; + } + else + { + 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; + // There's a bug in GNUstep in [NSCalendarDate dateByAddingYears:months:days:] + // that causes errors when adding subsequently a month. For this reason, + // we set the day of the reference date to 1. + referenceDate = [NSCalendarDate dateWithYear: [eventStartDate yearOfCommonEra] + month: [eventStartDate monthOfYear] + day: 1 + hour: [eventStartDate hourOfDay] + minute: [eventStartDate minuteOfHour] + second: 0 + timeZone: [eventStartDate timeZone]]; + + for (monthIdxInRange = 0, count = 0; + monthIdxInRange < numberOfMonthsInRange; monthIdxInRange++) { NSCalendarDate *cursor; @@ -349,63 +304,95 @@ static void NGMonthDaySet_fillWithByDayX (NGMonthDaySet *daySet, if ((monthIdxInRecurrence % interval) != 0) continue; - /* - Then the sequence is: - - check whether the month is in the BYMONTH list - */ - - /* - Note: the function below adds exactly a month, eg: - 2007-01-30 + 1month => 2007-02-*28*!! - */ - cursor = [eventStartDate dateByAddingYears: 0 - months: (diff + monthIdxInRange) - days: 0]; + cursor = [referenceDate dateByAddingYears: 0 + months: monthIdxInRecurrence + days: 0]; [cursor setTimeZone: timeZone]; numDaysInMonth = [cursor numberOfDaysInMonth]; - - - /* check whether we match the bymonth specification */ + + /* check whether we match the BYMONTH constraint */ if (!byMonthList[[cursor monthOfYear] - 1]) continue; - - /* check 'day level' byXYZ rules */ + /* check whether we match the BYMONTHDAY and BYDAY constraints */ didByFill = NO; - + if (byMonthDay) - { /* list of days in the month */ - NGMonthDaySet_copyOrUnion (&monthDays, &byMonthDaySet, !didByFill); - didByFill = YES; - } - - if ([rrule byDayMask] != 0) - { // TODO: replace the mask with an array - NGMonthDaySet ruleset; - unsigned firstDoWInMonth; - - firstDoWInMonth = [[cursor firstDayOfMonth] dayOfWeek]; - - NGMonthDaySet_fillWithByDayX (&ruleset, - [rrule byDayMask], - firstDoWInMonth, - [cursor numberOfDaysInMonth], - [rrule byDayOccurence1]); - NGMonthDaySet_copyOrUnion (&monthDays, &ruleset, !didByFill); - didByFill = YES; - } - - if (!didByFill) { - /* no rules applied, take the dayOfMonth of the startDate */ + // Initialize the monthDays array with the positive days positions + NGMonthDaySet_copyOrUnion (&monthDays, &byPositiveMonthDaySet, !didByFill); + + // Add to the array the days matching the negative days positions + int i; + for (i = 1; i <= 31; i++) + if (byNegativeMonthDaySet[i]) + monthDays[numDaysInMonth - i + 1] = YES; + didByFill = YES; + } + + if (byDayMask) + { + unsigned int firstDoWInMonth, currentWeekDay; + unsigned int weekDaysCount[7], currentWeekDaysCount[7]; + int i, positiveOrder, negativeOrder; + + firstDoWInMonth = [[cursor firstDayOfMonth] dayOfWeek]; + + if (!didByFill) + NGMonthDaySet_clear (&monthDays); + + // Fill weekDaysCount to handle negative positions + currentWeekDay = firstDoWInMonth; + memset(weekDaysCount, 0, 7 * sizeof(unsigned int)); + for (i = 1; i <= numDaysInMonth; i++) + { + weekDaysCount[currentWeekDay]++; + currentWeekDay++; + currentWeekDay = fmod (currentWeekDay, 7); + } + + currentWeekDay = firstDoWInMonth; + memset(currentWeekDaysCount, 0, 7 * sizeof(unsigned int)); + for (i = 1; i <= numDaysInMonth; i++) + { + if (!didByFill || monthDays[i]) + { + positiveOrder = currentWeekDaysCount[currentWeekDay] + 1; + negativeOrder = currentWeekDaysCount[currentWeekDay] - weekDaysCount[currentWeekDay]; + monthDays[i] = (([byDayMask occursOnDay: (iCalWeekDay)currentWeekDay + withWeekNumber: positiveOrder]) || + ([byDayMask occursOnDay: (iCalWeekDay)currentWeekDay + withWeekNumber: negativeOrder])); + } + currentWeekDaysCount[currentWeekDay]++; + currentWeekDay++; + currentWeekDay = fmod (currentWeekDay, 7); + } + didByFill = YES; + } + + if (didByFill) + { + if (diff + monthIdxInRange == 0) + { + // When dealing with the month of the first occurence, remove days + // that occur before the first occurrence. + int i; + for (i = 1; i < eventDayOfMonth; i++) + monthDays[i] = NO; + // The first occurrence must always be included. + monthDays[i] = YES; + } + } + else + { + // No rules applied, take the dayOfMonth of the startDate NGMonthDaySet_clear (&monthDays); monthDays[eventDayOfMonth] = YES; } - // TODO: add processing of byhour/byminute/bysecond etc - /* Next step is to create NSCalendarDate instances from our 'monthDays' set. We walk over each day of the 'monthDays' set. If its flag isn't @@ -420,64 +407,25 @@ static void NGMonthDaySet_fillWithByDayX (NGMonthDaySet *daySet, cursor[2]: 2007-02-28 <== Note: we have February! */ - for (dom = 1, doCont = YES; dom <= numDaysInMonth && doCont; dom++) + for (dom = 1, doCont = YES; dom <= numDaysInMonth && doCont; dom++) { NSCalendarDate *start; if (!monthDays[dom]) continue; - // TODO: what is this good for? - /* - Here we need to correct the date. Remember that the startdate given in - the event is not necessarily a date of the sequence! - - The 'numDaysInMonth' localvar contains the number of days in the - current month (eg 31 for January, 28 for most February's, etc) - - Eg: MONTHLY;BYDAY=-1WE (last wednesday, every month) - - cursor: 2007-01-30 (eventDayOfMonth = 30) - =>start: 2007-01-31 (dom = 31) - cursor: 2007-02-28 (eventDayOfMonth = 30) - =>start: 2007-02-28 (dom = 28) - - Note: in case the cursor already had an event-day overflow, that is the - 'eventDayOfMonth' is bigger than the 'numDaysInMonth', the cursor - will already be corrected! - Eg: - start was: 2007-01-30 - cursor will be: 2007-02-28 - */ - if (eventDayOfMonth == dom) - { - start = cursor; - } - else - { - int maxDay = - eventDayOfMonth > numDaysInMonth ? numDaysInMonth : eventDayOfMonth; - - start = [cursor dateByAddingYears: 0 months: 0 days: (dom - maxDay)]; - } - - /* - Setup for 2007-02-28, MONTHLY;BYDAY=-1WE. - dom: 28 - eventDayOfMonth: 31 - cursor: 2007-02-28 - start: 2007-02-25 <== WRONG - */ - -#if HEAVY_DEBUG - NSLog (@"DOM %i EDOM %i NUMDAYS %i START: %@ CURSOR: %@", - dom, eventDayOfMonth, numDaysInMonth, - start, cursor); -#endif + start = [cursor dateByAddingYears: 0 months: 0 days: (dom - 1)]; doCont = [self _addInstanceWithStartDate: start - limitDate: until - limitRange: _r - toArray: ranges]; + limitDate: until + limitRange: _r + toArray: ranges]; + //NSLog(@"*** MONTHLY [%i/%i] adding %@%@ (count = %i)", dom, numDaysInMonth, start, (doCont?@"":@" .. NOT!"), count); + if (repeatCount > 0) + { + count++; + //NSLog(@"MONTHLY count = %i/%i", count, repeatCount); + doCont = (count < repeatCount); + } } if (!doCont) break; /* reached some limit */ } @@ -487,15 +435,30 @@ static void NGMonthDaySet_fillWithByDayX (NGMonthDaySet *daySet, - (NSCalendarDate *) lastInstanceStartDate { NSCalendarDate *firStart, *lastInstanceStartDate; + NGCalendarDateRange *r; + NSArray *instances; - if ([rrule repeatCount] > 0) + lastInstanceStartDate = nil; + if ([rrule repeatCount] > 0) { - firStart = [firstRange startDate]; - - lastInstanceStartDate = [firStart dateByAddingYears: 0 - months: ([rrule repeatInterval] - * ([rrule repeatCount] - 1)) - days: 0]; + if ([rrule hasByMask]) + { + // Must perform the complete calculation + firStart = [firstRange startDate]; + r = [NGCalendarDateRange calendarDateRangeWithStartDate: firStart + endDate: [NSCalendarDate distantFuture]]; + instances = [self recurrenceRangesWithinCalendarDateRange: r]; + if ([instances count]) + lastInstanceStartDate = [(NGCalendarDateRange *)[instances lastObject] startDate]; + } + else + { + // No BYxxx mask + lastInstanceStartDate = [firStart dateByAddingYears: 0 + months: ([rrule repeatInterval] + * ([rrule repeatCount] - 1)) + days: 0]; + } } else lastInstanceStartDate = [super lastInstanceStartDate]; diff --git a/SOPE/NGCards/iCalRecurrenceCalculator.m b/SOPE/NGCards/iCalRecurrenceCalculator.m index a30be6c74..284e74b2a 100644 --- a/SOPE/NGCards/iCalRecurrenceCalculator.m +++ b/SOPE/NGCards/iCalRecurrenceCalculator.m @@ -340,7 +340,6 @@ static Class yearlyCalcClass = Nil; /* NOTE: this is horribly inaccurate and doesn't even consider the use of repeatCount. It MUST be implemented by subclasses properly! - However, it does the trick for SOGo 1.0 - that's why it's left here. */ return [rrule untilDate]; } diff --git a/SOPE/NGCards/iCalRecurrenceRule.h b/SOPE/NGCards/iCalRecurrenceRule.h index 54542229b..810079ad8 100644 --- a/SOPE/NGCards/iCalRecurrenceRule.h +++ b/SOPE/NGCards/iCalRecurrenceRule.h @@ -1,5 +1,6 @@ /* Copyright (C) 2004-2005 SKYRIX Software AG + Copyright (C) 2006-2010 Inverse inc. This file is part of SOPE. @@ -46,34 +47,25 @@ typedef enum { } iCalRecurrenceFrequency; typedef enum { - iCalWeekDaySunday = 1, - iCalWeekDayMonday = 2, - iCalWeekDayTuesday = 4, - iCalWeekDayWednesday = 8, - iCalWeekDayThursday = 16, - iCalWeekDayFriday = 32, - iCalWeekDaySaturday = 64, + iCalWeekDayUnknown = -1, + iCalWeekDaySunday = 0, + iCalWeekDayMonday = 1, + iCalWeekDayTuesday = 2, + iCalWeekDayWednesday = 3, + iCalWeekDayThursday = 4, + iCalWeekDayFriday = 5, + iCalWeekDaySaturday = 6 } iCalWeekDay; +extern NSString *iCalWeekDayString[]; + @class NSString, NSCalendarDate, NGCalendarDateRange, NSArray; +@class iCalByDayMask; @interface iCalRecurrenceRule : CardElement -// { -// iCalRecurrenceFrequency frequency; -// int interval; -// unsigned repeatCount; -// NSCalendarDate *untilDate; -// struct { -// unsigned weekStart: 7; -// unsigned mask: 7; -// unsigned useOccurence:1; -// unsigned reserved:1; -// } byDay; -// int byDayOccurence1; -// NSArray *byMonthDay; - -// NSString *rrule; -// } +{ + iCalByDayMask *dayMask; +} + (id) recurrenceRuleWithICalRepresentation: (NSString *) _iCalRep; - (id) initWithString: (NSString *) _str; @@ -92,12 +84,14 @@ typedef enum { - (void) setWeekStart: (iCalWeekDay) _weekStart; - (iCalWeekDay) weekStart; -- (void) setByDayMask: (unsigned int) _mask; -- (unsigned int) byDayMask; -- (int) byDayOccurence1; - +- (void) setByDay: (NSString *) newByDay; +- (NSString *) byDay; +- (void) setByDayMask: (iCalByDayMask *) newMask; +- (iCalByDayMask *) byDayMask; - (NSArray *) byMonthDay; - +- (NSArray *) byMonth; +- (BOOL) hasByMask; + /* count and untilDate are mutually exclusive */ - (void) setRepeatCount: (int) _repeatCount; @@ -112,8 +106,6 @@ typedef enum { - (void) setRrule: (NSString *) _rrule; // TODO: weird name? (better: RRule?) -// - (NSString *)iCalRepresentation; - @end #endif /* __NGiCal_iCalRecurrenceRule_H_ */ diff --git a/SOPE/NGCards/iCalRecurrenceRule.m b/SOPE/NGCards/iCalRecurrenceRule.m index a997849ad..3450a8789 100644 --- a/SOPE/NGCards/iCalRecurrenceRule.m +++ b/SOPE/NGCards/iCalRecurrenceRule.m @@ -1,5 +1,6 @@ /* Copyright (C) 2004-2005 SKYRIX Software AG + Copyright (C) 2006-2010 Inverse inc. This file is part of SOPE. @@ -19,6 +20,168 @@ 02111-1307, USA. */ +/* + See http://tools.ietf.org/html/rfc2445#section-4.3.10 + +4.3.10 Recurrence Rule + + Value Name: RECUR + + Purpose: This value type is used to identify properties that contain + a recurrence rule specification. + + Formal Definition: The value type is defined by the following + notation: + + recur = "FREQ"=freq *( + + ; either UNTIL or COUNT may appear in a 'recur', + ; but UNTIL and COUNT MUST NOT occur in the same 'recur' + + ( ";" "UNTIL" "=" enddate ) / + ( ";" "COUNT" "=" 1*DIGIT ) / + + ; the rest of these keywords are optional, + ; but MUST NOT occur more than once + + ( ";" "INTERVAL" "=" 1*DIGIT ) / + ( ";" "BYSECOND" "=" byseclist ) / + ( ";" "BYMINUTE" "=" byminlist ) / + ( ";" "BYHOUR" "=" byhrlist ) / + ( ";" "BYDAY" "=" bywdaylist ) / + ( ";" "BYMONTHDAY" "=" bymodaylist ) / + ( ";" "BYYEARDAY" "=" byyrdaylist ) / + ( ";" "BYWEEKNO" "=" bywknolist ) / + ( ";" "BYMONTH" "=" bymolist ) / + ( ";" "BYSETPOS" "=" bysplist ) / + ( ";" "WKST" "=" weekday ) / + ( ";" x-name "=" text ) + ) + + freq = "SECONDLY" / "MINUTELY" / "HOURLY" / "DAILY" + / "WEEKLY" / "MONTHLY" / "YEARLY" + + enddate = date + enddate =/ date-time ;An UTC value + + byseclist = seconds / ( seconds *("," seconds) ) + + seconds = 1DIGIT / 2DIGIT ;0 to 59 + + byminlist = minutes / ( minutes *("," minutes) ) + + minutes = 1DIGIT / 2DIGIT ;0 to 59 + + byhrlist = hour / ( hour *("," hour) ) + + hour = 1DIGIT / 2DIGIT ;0 to 23 + + bywdaylist = weekdaynum / ( weekdaynum *("," weekdaynum) ) + + weekdaynum = [([plus] ordwk / minus ordwk)] weekday + + plus = "+" + + minus = "-" + + ordwk = 1DIGIT / 2DIGIT ;1 to 53 + + weekday = "SU" / "MO" / "TU" / "WE" / "TH" / "FR" / "SA" + ;Corresponding to SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, + ;FRIDAY, SATURDAY and SUNDAY days of the week. + + bymodaylist = monthdaynum / ( monthdaynum *("," monthdaynum) ) + + monthdaynum = ([plus] ordmoday) / (minus ordmoday) + + ordmoday = 1DIGIT / 2DIGIT ;1 to 31 + + byyrdaylist = yeardaynum / ( yeardaynum *("," yeardaynum) ) + + yeardaynum = ([plus] ordyrday) / (minus ordyrday) + + ordyrday = 1DIGIT / 2DIGIT / 3DIGIT ;1 to 366 + + bywknolist = weeknum / ( weeknum *("," weeknum) ) + + weeknum = ([plus] ordwk) / (minus ordwk) + + bymolist = monthnum / ( monthnum *("," monthnum) ) + + monthnum = 1DIGIT / 2DIGIT ;1 to 12 + + bysplist = setposday / ( setposday *("," setposday) ) + + setposday = yeardaynum +*/ + +/* + Examples : + + Every other week on Monday, Wednesday and Friday until December 24, + 1997, but starting on Tuesday, September 2, 1997: + + DTSTART;TZID=US-Eastern:19970902T090000 + RRULE:FREQ=WEEKLY;INTERVAL=2;UNTIL=19971224T000000Z;WKST=SU; + BYDAY=MO,WE,FR + ==> (1997 9:00 AM EDT)September 2,3,5,15,17,19,29;October + 1,3,13,15,17 + (1997 9:00 AM EST)October 27,29,31;November 10,12,14,24,26,28; + December 8,10,12,22 + + Monthly on the 1st Friday for ten occurrences: + + DTSTART;TZID=US-Eastern:19970905T090000 + RRULE:FREQ=MONTHLY;COUNT=10;BYDAY=1FR + + ==> (1997 9:00 AM EDT)September 5;October 3 + (1997 9:00 AM EST)November 7;Dec 5 + (1998 9:00 AM EST)January 2;February 6;March 6;April 3 + (1998 9:00 AM EDT)May 1;June 5 + + Every other month on the 1st and last Sunday of the month for 10 + occurrences: + + DTSTART;TZID=US-Eastern:19970907T090000 + RRULE:FREQ=MONTHLY;INTERVAL=2;COUNT=10;BYDAY=1SU,-1SU + + ==> (1997 9:00 AM EDT)September 7,28 + (1997 9:00 AM EST)November 2,30 + + Monthly on the third to the last day of the month, forever: + + DTSTART;TZID=US-Eastern:19970928T090000 + RRULE:FREQ=MONTHLY;BYMONTHDAY=-3 + + ==> (1997 9:00 AM EDT)September 28 + (1997 9:00 AM EST)October 29;November 28;December 29 + (1998 9:00 AM EST)January 29;February 26 + ... + + Every other year on January, February, and March for 10 occurrences: + + DTSTART;TZID=US-Eastern:19970310T090000 + RRULE:FREQ=YEARLY;INTERVAL=2;COUNT=10;BYMONTH=1,2,3 + + ==> (1997 9:00 AM EST)March 10 + (1999 9:00 AM EST)January 10;February 10;March 10 + (2001 9:00 AM EST)January 10;February 10;March 10 + (2003 9:00 AM EST)January 10;February 10;March 10 + + Everyday in January, for 3 years: + + DTSTART;TZID=US-Eastern:19980101T090000 + RRULE:FREQ=YEARLY;UNTIL=20000131T090000Z; + BYMONTH=1;BYDAY=SU,MO,TU,WE,TH,FR,SA + or + RRULE:FREQ=DAILY;UNTIL=20000131T090000Z;BYMONTH=1 + + ==> (1998 9:00 AM EDT)January 1-31 + (1999 9:00 AM EDT)January 1-31 + (2000 9:00 AM EDT)January 1-31 + +*/ + #import #import #import @@ -27,13 +190,16 @@ #import +#import "NSCalendarDate+ICal.h" #import "NSCalendarDate+NGCards.h" #import "NSString+NGCards.h" -#import "NSCalendarDate+ICal.h" - +#import "iCalByDayMask.h" #import "iCalRecurrenceRule.h" +NSString *iCalWeekDayString[] = { @"SU", @"MO", @"TU", @"WE", @"TH", @"FR", + @"SA" }; + /* freq = rrFreq; until = rrUntil; @@ -60,11 +226,6 @@ - (NSString *) wkst; - (NSString *) byDayList; -// - (void)_parseRuleString:(NSString *)_rrule; - -/* currently used by parser, should be removed (replace with an -init..) */ -- (void)setByday:(NSString *)_byDayList; - @end @implementation iCalRecurrenceRule @@ -85,6 +246,7 @@ if ((self = [super init]) != nil) { [self setTag: @"rrule"]; + dayMask = nil; } return self; @@ -100,6 +262,12 @@ return self; } +- (void) dealloc +{ + [dayMask release]; + [super dealloc]; +} + - (void) setRrule: (NSString *) _rrule { NSEnumerator *newValues; @@ -273,54 +441,30 @@ return [self weekDayFromICalRepresentation: [self wkst]]; } -- (void) setByDayMask: (unsigned) _mask +- (void) setByDay: (NSString *) newByDay { - NSMutableArray *days; - unsigned int count; - unsigned char maskDays[] = { iCalWeekDaySunday, iCalWeekDayMonday, - iCalWeekDayTuesday, iCalWeekDayWednesday, - iCalWeekDayThursday, iCalWeekDayFriday, - iCalWeekDaySaturday }; - days = [NSMutableArray arrayWithCapacity: 7]; - if (_mask) - { - for (count = 0; count < 7; count++) - if (_mask & maskDays[count]) - [days addObject: - [self iCalRepresentationForWeekDay: maskDays[count]]]; - } - - [self setNamedValue: @"byday" to: [days componentsJoinedByString: @","]]; + [self setNamedValue: @"byday" to: newByDay]; } -- (unsigned int) byDayMask +- (NSString *) byDay { - NSArray *days; - unsigned int mask, count, max; - NSString *day, *value; - - mask = 0; - - value = [self namedValue: @"byday"]; - if ([value length] > 0) - { - days = [value componentsSeparatedByString: @","]; - max = [days count]; - for (count = 0; count < max; count++) - { - day = [days objectAtIndex: count]; - mask |= [self weekDayFromICalRepresentation: day]; - } - } - - return mask; + return [self namedValue: @"byday"]; } -#warning this is bad -- (int) byDayOccurence1 +- (void) setByDayMask: (iCalByDayMask *) newByDayMask { - return 0; -// return byDayOccurence1; + [self setByDay: [newByDayMask asRuleString]]; +} + +- (iCalByDayMask *) byDayMask +{ + if (dayMask == nil && [[self byDay] length]) + { + dayMask = [iCalByDayMask byDayMaskWithRuleString: [self byDay]]; + [dayMask retain]; + } + + return dayMask; } - (NSArray *) byMonthDay @@ -337,6 +481,35 @@ return byMonthDay; } +- (NSArray *) byMonth +{ + NSArray *byMonth; + NSString *byMonthStr; + + byMonthStr = [self namedValue: @"bymonth"]; + if ([byMonthStr length]) + byMonth = [byMonthStr componentsSeparatedByString: @","]; + else + byMonth = nil; + + return byMonth; +} + +- (BOOL) hasByMask +{ + /* There are more BYxxx rule parts but we don't support them yet : + * - BYYEARDAY + * - BYWEEKNO + * - BYHOUR + * - BYMINUTE + * - BYSECOND + * - BYSETPOS + */ + return ([[self namedValue: @"bymonthday"] length] || + [[self namedValue: @"byday"] length] || + [[self namedValue: @"bymonth"] length]); +} + - (BOOL) isInfinite { return !([self repeatCount] || [self untilDate]); @@ -356,6 +529,7 @@ dayLength = [_day length]; if (dayLength > 1) { + // Ignore any prefix, only consider last two characters [[_day uppercaseString] getCharacters: chars range: NSMakeRange (dayLength - 2, 2)]; diff --git a/SOPE/NGCards/iCalTimeZonePeriod.m b/SOPE/NGCards/iCalTimeZonePeriod.m index 69651dc97..f36ef8efb 100644 --- a/SOPE/NGCards/iCalTimeZonePeriod.m +++ b/SOPE/NGCards/iCalTimeZonePeriod.m @@ -26,6 +26,7 @@ #import "iCalDateTime.h" #import "iCalRecurrenceRule.h" +#import "iCalByDayMask.h" #import "iCalTimeZonePeriod.h" @@ -88,16 +89,16 @@ return ((negative) ? -seconds : seconds); } -- (unsigned int) dayOfWeekFromRruleDay: (iCalWeekDay) day -{ - unsigned int dayOfWeek; +// - (unsigned int) dayOfWeekFromRruleDay: (iCalWeekDay) day +// { +// unsigned int dayOfWeek; - dayOfWeek = 0; - while (day >> (dayOfWeek + 1)) - dayOfWeek++; +// dayOfWeek = 0; +// while (day >> (dayOfWeek + 1)) +// dayOfWeek++; - return dayOfWeek; -} +// return dayOfWeek; +// } - (NSCalendarDate *) startDate { @@ -105,23 +106,34 @@ dateTime]; } -/* This method returns the date corresponding for to the start of the period - in the year of the reference date. */ +/** + * This method returns the date corresponding for to the start of the period + * in the year of the reference date. + * We assume that a RRULE for a timezone will always be YEARLY with a BYMONTH + * and a BYDAY rule. + */ - (NSCalendarDate *) _occurenceForDate: (NSCalendarDate *) refDate byRRule: (iCalRecurrenceRule *) rrule { NSCalendarDate *tmpDate; - NSString *byDay; + iCalByDayMask *byDayMask; int dayOfWeek, dateDayOfWeek, offset, pos; NSCalendarDate *tzStart; - byDay = [rrule namedValue: @"byday"]; - dayOfWeek = [self dayOfWeekFromRruleDay: [rrule byDayMask]]; - pos = [[byDay substringToIndex: 2] intValue]; - if (!pos) - /* if byday = "SU", instead of "1SU"... */ - pos = 1; + byDayMask = [rrule byDayMask]; + dayOfWeek = 0; + if (byDayMask == nil) + { + dayOfWeek = 0; + pos = 1; + } + else + { + dayOfWeek = (int)[byDayMask firstDay]; + pos = [byDayMask firstOccurrence]; + } + tzStart = [self startDate]; [tzStart setTimeZone: [NSTimeZone timeZoneWithName: @"GMT"]]; tmpDate = [NSCalendarDate dateWithYear: [refDate yearOfCommonEra] diff --git a/SOPE/NGCards/iCalWeeklyRecurrenceCalculator.m b/SOPE/NGCards/iCalWeeklyRecurrenceCalculator.m index a714520f7..40d525f0f 100644 --- a/SOPE/NGCards/iCalWeeklyRecurrenceCalculator.m +++ b/SOPE/NGCards/iCalWeeklyRecurrenceCalculator.m @@ -1,5 +1,6 @@ /* Copyright (C) 2004-2005 SKYRIX Software AG + Copyright (C) 2006-2010 Inverse inc. This file is part of SOPE. @@ -28,6 +29,7 @@ #import #import "iCalRecurrenceRule.h" +#import "iCalByDayMask.h" #import "NSCalendarDate+ICal.h" @interface iCalRecurrenceCalculator (PrivateAPI) @@ -42,22 +44,36 @@ @end -/* - TODO: If BYDAY is specified, lastInstanceStartDate and recurrences will - differ significantly! -*/ @implementation iCalWeeklyRecurrenceCalculator + /** + * TODO : Unsupported conditions for WEEKLY recurrences : + * + * BYYEAR + * BYYEARDAY + * BYWEEKNO + * BYMONTH + * BYMONTHDAY + * BYHOUR + * BYMINUTE + * WKST + * + * There's no GUI to defined such conditions, so there's no + * problem for now. + */ - (NSArray *) recurrenceRangesWithinCalendarDateRange: (NGCalendarDateRange *) _r { NSMutableArray *ranges; NSCalendarDate *firStart, *startDate, *endDate, *currentStartDate, *currentEndDate; - long i; - unsigned interval, byDayMask; + long i, repeatCount, count; + unsigned interval; + iCalByDayMask *dayMask; firStart = [firstRange startDate]; startDate = [_r startDate]; endDate = [_r endDate]; + dayMask = nil; + repeatCount = 0; if ([endDate compare: firStart] == NSOrderedAscending) // Range ends before first occurrence @@ -65,33 +81,47 @@ interval = [rrule repeatInterval]; + if ([[rrule byDay] length]) + dayMask = [rrule byDayMask]; + // If rule is bound, check the bounds if (![rrule isInfinite]) { NSCalendarDate *until, *lastDate; + lastDate = nil; until = [rrule untilDate]; if (until) lastDate = until; - else - lastDate = [firStart dateByAddingYears: 0 months: 0 - days: (interval - * ([rrule repeatCount] - 1) * 7)]; + else + { + repeatCount = [rrule repeatCount]; + if (dayMask == nil) + // When there's no BYxxx mask, we can find the date of the last + // occurrence. + lastDate = [firStart dateByAddingYears: 0 months: 0 + days: (interval + * (repeatCount - 1) * 7)]; + } - if ([lastDate compare: startDate] == NSOrderedAscending) - // Range starts after last occurrence - return nil; - if ([lastDate compare: endDate] == NSOrderedAscending) - // Range ends after last occurence; adjust end date - endDate = lastDate; + if (lastDate != nil) + { + if ([lastDate compare: startDate] == NSOrderedAscending) + // Range starts after last occurrence + return nil; + if ([lastDate compare: endDate] == NSOrderedAscending) + // Range ends after last occurence; adjust end date + endDate = lastDate; + } } currentStartDate = [firStart copy]; [currentStartDate autorelease]; ranges = [NSMutableArray array]; - byDayMask = [rrule byDayMask]; i = 1; - if (!byDayMask) + count = 0; + + if (dayMask == nil) { while ([currentStartDate compare: endDate] == NSOrderedAscending || [currentStartDate compare: endDate] == NSOrderedSame) @@ -106,7 +136,7 @@ endDate: currentEndDate]; if ([_r containsDateRange: r]) [ranges addObject: r]; - } + } currentStartDate = [firStart dateByAddingYears: 0 months: 0 days: (interval * i * 7)]; @@ -115,53 +145,52 @@ } else { - unsigned dayOfWeek; NGCalendarDateRange *r; while ([currentStartDate compare: endDate] == NSOrderedAscending || [currentStartDate compare: endDate] == NSOrderedSame) { - if ([startDate compare: currentStartDate] == NSOrderedAscending || + BOOL isRecurrence = NO; + int days, week; + + if (repeatCount > 0 || + [startDate compare: currentStartDate] == NSOrderedAscending || [startDate compare: currentStartDate] == NSOrderedSame) { - int days, week; - - [currentStartDate years:NULL months:NULL days:(int *)&days hours:NULL - minutes:NULL seconds:NULL sinceDate:firStart]; - week = days / 7; - - if ((week % interval) == 0) + // If the rule count is defined, stop once the count is reached. + if (i == 1) { - // Date is in the proper week with respect to the - // week interval - BOOL isRecurrence = NO; - - if ([currentStartDate compare: firStart] == NSOrderedSame) - // Always add the event of the start date of - // the recurring event. + // Always add the start date the recurring event if within + // the lookup range. + isRecurrence = YES; + } + else + { + [currentStartDate years:NULL months:NULL days:(int *)&days hours:NULL + minutes:NULL seconds:NULL sinceDate: firStart]; + week = days / 7; + + if ((week % interval) == 0 && + [dayMask occursOnDay: [currentStartDate dayOfWeek]]) isRecurrence = YES; - else - { - // Only consider events that matches the day mask. - dayOfWeek = ([currentStartDate dayOfWeek] - ? (unsigned int) 1 << [currentStartDate dayOfWeek] - : iCalWeekDaySunday); - if (dayOfWeek & [rrule byDayMask]) - isRecurrence = YES; - } - if (isRecurrence) - { - currentEndDate = [currentStartDate addTimeInterval: [firstRange duration]]; - r = [NGCalendarDateRange calendarDateRangeWithStartDate: currentStartDate - endDate: currentEndDate]; - if ([_r containsDateRange: r]) - [ranges addObject: r]; - } + } + + if (isRecurrence) + { + count++; + if (repeatCount > 0 && count > repeatCount) + break; + currentEndDate = [currentStartDate addTimeInterval: [firstRange duration]]; + r = [NGCalendarDateRange calendarDateRangeWithStartDate: currentStartDate + endDate: currentEndDate]; + if ([_r containsDateRange: r]) + [ranges addObject: r]; } } currentStartDate = [currentStartDate dateByAddingYears: 0 - months: 0 - days: 1]; + months: 0 + days: 1]; + i++; } } @@ -171,14 +200,18 @@ - (NSCalendarDate *) lastInstanceStartDate { NSCalendarDate *firStart, *lastInstanceStartDate; + NGCalendarDateRange *r; + NSArray *instances; - if ([rrule repeatCount] > 0) + lastInstanceStartDate = nil; + if ([rrule repeatCount] > 0) { firStart = [firstRange startDate]; - - lastInstanceStartDate = [firStart dateByAddingYears: 0 months: 0 - days: (7 * [rrule repeatInterval] - * ([rrule repeatCount] - 1))]; + r = [NGCalendarDateRange calendarDateRangeWithStartDate: firStart + endDate: [NSCalendarDate distantFuture]]; + instances = [self recurrenceRangesWithinCalendarDateRange: r]; + if ([instances count]) + lastInstanceStartDate = [(NGCalendarDateRange *)[instances lastObject] startDate]; } else lastInstanceStartDate = [super lastInstanceStartDate]; diff --git a/SOPE/NGCards/iCalYearlyRecurrenceCalculator.m b/SOPE/NGCards/iCalYearlyRecurrenceCalculator.m index 9f27a9b95..210977a2e 100644 --- a/SOPE/NGCards/iCalYearlyRecurrenceCalculator.m +++ b/SOPE/NGCards/iCalYearlyRecurrenceCalculator.m @@ -1,5 +1,6 @@ /* Copyright (C) 2004-2005 SKYRIX Software AG + Copyright (C) 2006-2010 Inverse inc. This file is part of SOPE. @@ -34,77 +35,234 @@ - (NSCalendarDate *) lastInstanceStartDate; @end +@class iCalMonthlyRecurrenceCalculator; + @implementation iCalYearlyRecurrenceCalculator - (NSArray *) recurrenceRangesWithinCalendarDateRange: (NGCalendarDateRange *) _r { NSMutableArray *ranges; - NSCalendarDate *firStart, *rStart, *rEnd, *until; - unsigned i, count, interval; - int diff; + NSArray *byMonth; + NSCalendarDate *firStart, *lastDate, *rStart, *rEnd, *until, *referenceDate; + iCalMonthlyRecurrenceCalculator *monthlyCalc; + unsigned j, yearIdxInRange, numberOfYearsInRange, count, interval, monthDiff; + int diff, repeatCount, currentMonth; firStart = [firstRange startDate]; rStart = [_r startDate]; rEnd = [_r endDate]; interval = [rrule repeatInterval]; - until = [self lastInstanceStartDate]; + byMonth = [rrule byMonth]; + diff = 0; + repeatCount = 0; + count = 0; + referenceDate = nil; - if (until) + if ([rEnd compare: firStart] == NSOrderedAscending) + // Range ends before first occurrence + return nil; + + // If rule is bound, check the bounds + if (![rrule isInfinite]) { - if ([until compare: rStart] == NSOrderedAscending) - return nil; - if ([until compare: rEnd] == NSOrderedDescending) - rEnd = until; - } - - diff = [firStart yearsBetweenDate: rStart]; - if ((diff != 0) && [rStart compare: firStart] == NSOrderedAscending) - diff = -diff; + lastDate = nil; + until = [rrule untilDate]; + repeatCount = [rrule repeatCount]; - count = [rStart yearsBetweenDate: rEnd] + 1; - ranges = [NSMutableArray arrayWithCapacity: count]; - for (i = 0 ; i < count; i++) - { - int test; - - test = diff + i; - if ((test >= 0) && (test % interval) == 0) + if (until) { - NSCalendarDate *start, *end; - NGCalendarDateRange *r; - - start = [firStart dateByAddingYears: diff + i - months: 0 - days: 0]; - [start setTimeZone: [firStart timeZone]]; - end = [start addTimeInterval: [firstRange duration]]; - r = [NGCalendarDateRange calendarDateRangeWithStartDate: start - endDate: end]; - if ([_r containsDateRange: r]) - [ranges addObject: r]; + lastDate = until; + } + if (repeatCount > 0) + { + if (lastDate == nil && ![rrule hasByMask]) + // When there's no BYxxx mask, we can find the date of the last + // occurrence. + lastDate = [firStart dateByAddingYears: (interval * (repeatCount - 1)) + months: 0 + days: 0]; + referenceDate = firStart; + } + + if (lastDate != nil) + { + if ([lastDate compare: rStart] == NSOrderedAscending) + // Range starts after last occurrence + return nil; + if ([lastDate compare: rEnd] == NSOrderedDescending) + // Range ends after last occurence; adjust end date + rEnd = lastDate; } } + + if (referenceDate == nil) + { + diff = [firStart yearsBetweenDate: rStart]; + if ((diff != 0) && [rStart compare: firStart] == NSOrderedAscending) + diff = -diff; + referenceDate = rStart; + } + + // Initialize array to return with an approximation of the number total + // number of possible matches, ie the number of years spawned by the period. + numberOfYearsInRange = [referenceDate yearsBetweenDate: rEnd] + 1; + ranges = [NSMutableArray arrayWithCapacity: numberOfYearsInRange]; + + if (byMonth) + { + /* + * WARNING/TODO : if there's no BYMONTH rule but there's a BYMONTHDAY + * rule we should implicitely define a BYMONTH rule by extracting the + * month from the DTSTART field. However, this kind of definition is + * uncommon. + */ + + // Instantiate a MONTHLY calculator + if (repeatCount > 0) + // Fool the monthly calculator, otherwise it will verify the COUNT + // constraint and perform the calculation from the first occurence of + // the recurrence. This calculation is performed by the current method. + [rrule setRepeatCount: 0]; + + monthlyCalc = [[iCalMonthlyRecurrenceCalculator alloc] + initWithRecurrenceRule: rrule + firstInstanceCalendarDateRange: firstRange]; + [monthlyCalc autorelease]; + + // There's a bug in GNUstep in [NSCalendarDate dateByAddingYears:months:days:] + // that causes errors when adding subsequently a month. For this reason, + // we set the day of the reference date to 1. + referenceDate = [NSCalendarDate dateWithYear: [referenceDate yearOfCommonEra] + month: [referenceDate monthOfYear] + day: 1 + hour: [referenceDate hourOfDay] + minute: [referenceDate minuteOfHour] + second: 0 + timeZone: [referenceDate timeZone]]; + + // If the BYMONTH constraints exclude the month of the event DTSTART, we + // add the corresponding range manually if it is included in the period. + // Otherwise, it will be included by the monthly calculator in the loop + // bellow. + int month = [firStart monthOfYear]; + if (![byMonth containsObject: [NSString stringWithFormat: @"%i", month]]) + { + count++; + if ([_r containsDateRange: firstRange]) + { + [ranges addObject: firstRange]; + } + } + } + + monthDiff = 0; + currentMonth = [referenceDate monthOfYear]; + for (yearIdxInRange = 0 ; yearIdxInRange < numberOfYearsInRange; yearIdxInRange++) + { + int test, year; + + test = diff + yearIdxInRange; + if ((test >= 0) && (test % interval) == 0) + { + year = yearIdxInRange + [referenceDate yearOfCommonEra]; + + if (byMonth) + { + // When there's a BYMONTH constraint, evaluate each month of the constraint using + // the monthly calculator. + for (j = 0; currentMonth < 13 && j <= 12; j++, currentMonth++, monthDiff++) + { + if ([byMonth containsObject: [NSString stringWithFormat: @"%i", currentMonth]]) + { + NGCalendarDateRange *rangeForMonth; + NSArray *rangesInMonth; + + rStart = [referenceDate dateByAddingYears: 0 + months: monthDiff + days: 0]; + rEnd = [rStart dateByAddingYears: 0 + months: 0 + days: [rStart numberOfDaysInMonth] - 1]; + rangeForMonth = [NGCalendarDateRange calendarDateRangeWithStartDate: rStart + endDate: rEnd]; + rangesInMonth = [monthlyCalc recurrenceRangesWithinCalendarDateRange: rangeForMonth]; + + int k; + for (k = 0; k < [rangesInMonth count] && (repeatCount == 0 || count < repeatCount); k++) { + //NSLog(@"*** YEARLY found %@ (count = %i)", [[rangesInMonth objectAtIndex: k] startDate], count); + count++; + if ([_r containsDateRange: [rangesInMonth objectAtIndex: k]]) + { + [ranges addObject: [rangesInMonth objectAtIndex: k]]; + //NSLog(@"*** YEARLY adding %@ (count = %i)", [[rangesInMonth objectAtIndex: k] startDate], count); + } + } + } + } + // Done with the current year; start the next iteration from January + currentMonth = 1; + } + else + { + // No BYxxx mask + NSCalendarDate *start, *end; + NGCalendarDateRange *r; + + start = [firStart dateByAddingYears: diff + yearIdxInRange + months: 0 + days: 0]; + [start setTimeZone: [firStart timeZone]]; + end = [start addTimeInterval: [firstRange duration]]; + r = [NGCalendarDateRange calendarDateRangeWithStartDate: start + endDate: end]; + if ([_r containsDateRange: r] && (repeatCount == 0 || count < repeatCount)) + { + [ranges addObject: r]; + count++; + } + } + } + } + + if (byMonth && repeatCount > 0) + // Restore the repeat count + [rrule setRepeatCount: repeatCount]; + return ranges; } - (NSCalendarDate *) lastInstanceStartDate { NSCalendarDate *firStart, *lastInstanceStartDate; + NGCalendarDateRange *r; + NSArray *instances; - if ([rrule repeatCount] > 0) + lastInstanceStartDate = nil; + if ([rrule repeatCount] > 0) { - firStart = [firstRange startDate]; - - lastInstanceStartDate - = [firStart dateByAddingYears: ([rrule repeatInterval] - * ([rrule repeatCount] - 1)) - months: 0 - days: 0]; + if ([rrule hasByMask]) + { + // Must perform the complete calculation + firStart = [firstRange startDate]; + r = [NGCalendarDateRange calendarDateRangeWithStartDate: firStart + endDate: [NSCalendarDate distantFuture]]; + instances = [self recurrenceRangesWithinCalendarDateRange: r]; + if ([instances count]) + lastInstanceStartDate = [(NGCalendarDateRange *)[instances lastObject] startDate]; + } + else + { + // No BYxxx mask + lastInstanceStartDate = [firStart dateByAddingYears: ([rrule repeatInterval] + * ([rrule repeatCount] - 1)) + months: 0 + days: 0]; + } } else lastInstanceStartDate = [super lastInstanceStartDate]; - + return lastInstanceStartDate; } diff --git a/UI/Scheduler/UIxAppointmentEditor.m b/UI/Scheduler/UIxAppointmentEditor.m index cbfabe564..8873dd55c 100644 --- a/UI/Scheduler/UIxAppointmentEditor.m +++ b/UI/Scheduler/UIxAppointmentEditor.m @@ -1,6 +1,6 @@ /* UIxAppointmentEditor.m - this file is part of SOGo * - * Copyright (C) 2007-2009 Inverse inc. + * Copyright (C) 2007-2010 Inverse inc. * * Author: Wolfgang Sourdeau * @@ -419,13 +419,11 @@ { WOResponse *result; NSDictionary *data; - NSCalendarDate *firstDate, *eventDate; + NSCalendarDate *eventDate; NSTimeZone *timeZone; SOGoUserDefaults *ud; SOGoCalendarComponent *co; - iCalEvent *master; - BOOL resetAlarm; - signed int daylightOffset; + BOOL resetAlarm; [self event]; @@ -457,20 +455,6 @@ [co saveComponent: event]; } - if ([co isNew] && [co isKindOfClass: [SOGoAppointmentOccurence class]]) - { - // This is a new exception in a recurrent event -- compute the daylight - // saving time with respect to the first occurrence of the recurrent event. - master = (iCalEvent*)[[event parent] firstChildWithTag: @"vevent"]; - firstDate = [master startDate]; - - if ([timeZone isDaylightSavingTimeForDate: eventDate] != [timeZone isDaylightSavingTimeForDate: firstDate]) - { - daylightOffset = (signed int)[timeZone secondsFromGMTForDate: firstDate] - - (signed int)[timeZone secondsFromGMTForDate: eventDate]; - eventDate = [eventDate dateByAddingYears:0 months:0 days:0 hours:0 minutes:0 seconds:daylightOffset]; - } - } data = [NSDictionary dictionaryWithObjectsAndKeys: [componentCalendar displayName], @"calendar", [event tag], @"component", diff --git a/UI/Scheduler/UIxCalListingActions.m b/UI/Scheduler/UIxCalListingActions.m index e12eb7875..deec99071 100644 --- a/UI/Scheduler/UIxCalListingActions.m +++ b/UI/Scheduler/UIxCalListingActions.m @@ -273,27 +273,28 @@ static NSArray *tasksFields = nil; static NSString *fields[] = { @"startDate", @"c_startdate", @"endDate", @"c_enddate" }; - for (count = 0; count < 2; count++) - { - aDateField = fields[count * 2]; - aDate = [aRecord objectForKey: aDateField]; - daylightOffset = (int) ([userTimeZone secondsFromGMTForDate: aDate] - - [userTimeZone secondsFromGMTForDate: startDate]); - if (daylightOffset) - { - aDate = [aDate dateByAddingYears: 0 months: 0 days: 0 hours: 0 - minutes: 0 seconds: daylightOffset]; - [aRecord setObject: aDate forKey: aDateField]; - aDateValue = [NSNumber numberWithInt: [aDate timeIntervalSince1970]]; - [aRecord setObject: aDateValue forKey: fields[count * 2 + 1]]; - } - } + if (dayBasedView) + for (count = 0; count < 2; count++) + { + aDateField = fields[count * 2]; + aDate = [aRecord objectForKey: aDateField]; + daylightOffset = (int) ([userTimeZone secondsFromGMTForDate: aDate] + - [userTimeZone secondsFromGMTForDate: startDate]); + if (daylightOffset) + { + aDate = [aDate dateByAddingYears: 0 months: 0 days: 0 hours: 0 + minutes: 0 seconds: daylightOffset]; + [aRecord setObject: aDate forKey: aDateField]; + aDateValue = [NSNumber numberWithInt: [aDate timeIntervalSince1970]]; + [aRecord setObject: aDateValue forKey: fields[count * 2 + 1]]; + } + } aDateValue = [aRecord objectForKey: @"c_recurrence_id"]; aDate = [aRecord objectForKey: @"cycleStartDate"]; - aStartDate = [aRecord objectForKey: @"startDate"]; if (aDateValue && aDate) { + aStartDate = [aRecord objectForKey: @"startDate"]; if ([userTimeZone isDaylightSavingTimeForDate: aStartDate] != [userTimeZone isDaylightSavingTimeForDate: aDate]) { @@ -387,11 +388,9 @@ static NSArray *tasksFields = nil; forKey: @"c_owner"]; if (![[newInfo objectForKey: @"c_title"] length]) [self _fixComponentTitle: newInfo withType: component]; - if (dayBasedView - || [[newInfo objectForKey: @"c_isallday"] boolValue]) - // Possible improvement: only call _fixDates if event is recurrent - // or the view range span a daylight saving time change - [self _fixDates: newInfo]; + // Possible improvement: only call _fixDates if event is recurrent + // or the view range span a daylight saving time change + [self _fixDates: newInfo]; [infos addObject: [newInfo objectsForKeys: fields notFoundMarker: marker]]; } diff --git a/UI/Scheduler/UIxComponentEditor.m b/UI/Scheduler/UIxComponentEditor.m index 13989c335..63bee5845 100644 --- a/UI/Scheduler/UIxComponentEditor.m +++ b/UI/Scheduler/UIxComponentEditor.m @@ -31,6 +31,7 @@ #import #import +#import #import #import #import @@ -294,25 +295,6 @@ iRANGE(2); } } -- (NSString *) _dayMaskToInteger: (unsigned int) theMask -{ - iCalWeekDay maskDays[] = {iCalWeekDaySunday, iCalWeekDayMonday, - iCalWeekDayTuesday, iCalWeekDayWednesday, - iCalWeekDayThursday, iCalWeekDayFriday, - iCalWeekDaySaturday}; - unsigned int i; - NSMutableString *s; - - s = [NSMutableString string]; - - for (i = 0; i < 7; i++) - if ((theMask & maskDays[i])) - [s appendFormat: @"%d,", i]; - [s deleteSuffix: @","]; - - return s; -} - - (void) _loadRRules { SOGoUserDefaults *ud; @@ -326,15 +308,12 @@ iRANGE(2); rule = [[component recurrenceRules] lastObject]; + /* DAILY */ if ([rule frequency] == iCalRecurrenceFrequenceDaily) { repeatType = @"0"; - if ([rule byDayMask] == (iCalWeekDayMonday - | iCalWeekDayTuesday - | iCalWeekDayWednesday - | iCalWeekDayThursday - | iCalWeekDayFriday)) + if ([[rule byDayMask] isWeekDays]) { if ([rule isInfinite]) repeat = @"EVERY WEEKDAY"; @@ -350,12 +329,14 @@ iRANGE(2); [self setRepeat2: [NSString stringWithFormat: @"%d", [rule repeatInterval]]]; } } + + /* WEEKLY */ else if ([rule frequency] == iCalRecurrenceFrequenceWeekly) { repeatType = @"1"; [self setRepeat1: [NSString stringWithFormat: @"%d", [rule repeatInterval]]]; - if (![rule byDayMask]) + if (![[rule byDay] length]) { if ([rule repeatInterval] == 1) repeat = @"WEEKLY"; @@ -364,45 +345,81 @@ iRANGE(2); } else { - [self setRepeat2: [self _dayMaskToInteger: [rule byDayMask]]]; + [self setRepeat2: [[rule byDayMask] asRuleStringWithIntegers]]; } } + + /* MONTHLY */ else if ([rule frequency] == iCalRecurrenceFrequenceMonthly) { repeatType = @"2"; - if ([rule byDayMask]) + if ([[rule byDay] length]) { - // TODO + int firstOccurrence; + iCalByDayMask *dayMask; + + dayMask = [rule byDayMask]; + firstOccurrence = [dayMask firstOccurrence] - 1; + if (firstOccurrence < 0) + firstOccurrence = 5; + [self setRepeat2: @"0"]; + [self setRepeat3: [NSString stringWithFormat: @"%d", firstOccurrence]]; + [self setRepeat4: [NSString stringWithFormat: @"%d", [dayMask firstDay]]]; } else if ([[rule byMonthDay] count]) { - [self setRepeat2: @"1"]; - [self setRepeat5: [[rule byMonthDay] componentsJoinedByString: @","]]; + NSArray *days; + + days = [rule byMonthDay]; + if ([days count] > 0 && [[days objectAtIndex: 0] intValue] < 0) + { + // BYMONTHDAY=-1 + [self setRepeat2: @"0"]; + [self setRepeat3: @"5"]; // last .. + [self setRepeat4: @"7"]; // .. day of the month + } + else + { + [self setRepeat2: @"1"]; + [self setRepeat5: [[rule byMonthDay] componentsJoinedByString: @","]]; + } } else if ([rule repeatInterval] == 1) repeat = @"MONTHLY"; [self setRepeat1: [NSString stringWithFormat: @"%d", [rule repeatInterval]]]; } + + /* YEARLY */ else { repeatType = @"3"; - if ([rule namedValue: @"bymonth"]) + if ([[rule namedValue: @"bymonth"] length]) { - if (![rule byDayMask]) + if ([[rule byDay] length]) + { + int firstOccurrence; + iCalByDayMask *dayMask; + + dayMask = [rule byDayMask]; + firstOccurrence = [dayMask firstOccurrence] - 1; + if (firstOccurrence < 0) + firstOccurrence = 5; + + [self setRepeat2: @"1"]; + [self setRepeat5: [NSString stringWithFormat: @"%d", firstOccurrence]]; + [self setRepeat6: [NSString stringWithFormat: @"%d", [dayMask firstDay]]]; + [self setRepeat7: [NSString stringWithFormat: @"%d", [[rule namedValue: @"bymonth"] intValue]-1]]; + } + else { [self setRepeat2: @"0"]; [self setRepeat3: [rule namedValue: @"bymonthday"]]; [self setRepeat4: [NSString stringWithFormat: @"%d", [[rule namedValue: @"bymonth"] intValue]-1]]; } - else - { - // TODO - [self setRepeat2: @"1"]; - } } else if ([rule repeatInterval] == 1) repeat = @"YEARLY"; @@ -410,7 +427,7 @@ iRANGE(2); [self setRepeat1: [NSString stringWithFormat: @"%d", [rule repeatInterval]]]; } - // We decode the proper end date, recurrences count, etc. + /* We decode the proper end date, recurrences count, etc. */ if ([rule repeatCount]) { repeat = @"CUSTOM"; @@ -1639,9 +1656,9 @@ RANGE(2); switch (type) { - // DAILY: + // DAILY (0) // - // repeat1 holds the value of the radio button: + // repeat1 holds the value of the frequency radio button: // 0 -> Every X days // 1 -> Every weekday // @@ -1653,11 +1670,7 @@ RANGE(2); if ([[self repeat1] intValue] > 0) { - [theRule setByDayMask: (iCalWeekDayMonday - |iCalWeekDayTuesday - |iCalWeekDayWednesday - |iCalWeekDayThursday - |iCalWeekDayFriday)]; + [theRule setByDayMask: [iCalByDayMask byDayMaskWithWeekDays]]; } else { @@ -1670,7 +1683,7 @@ RANGE(2); } break; - // WEEKLY + // WEEKLY (1) // // repeat1 holds the value of "Every X week(s)" // @@ -1683,7 +1696,8 @@ RANGE(2); if ([[self repeat1] intValue] > 0) { NSArray *v; - int c, mask; + int c, day; + iCalWeekOccurrences days; [theRule setFrequency: iCalRecurrenceFrequenceWeekly]; [theRule setInterval: [self repeat1]]; @@ -1692,18 +1706,21 @@ RANGE(2); { v = [[self repeat2] componentsSeparatedByString: @","]; c = [v count]; - mask = 0; + memset(days, 0, 7 * sizeof(iCalWeekOccurrence)); while (c--) - mask |= 1 << ([[v objectAtIndex: c] intValue]); - - [theRule setByDayMask: mask]; + { + day = [[v objectAtIndex: c] intValue]; + if (day >= 0 && day <= 7) + days[day] = iCalWeekOccurrenceAll; + } + [theRule setByDayMask: [iCalByDayMask byDayMaskWithDays: days]]; } } } break; - // MONTHLY + // MONTHLY (2) // // repeat1 holds the value of "Every X month(s)" // @@ -1731,20 +1748,29 @@ RANGE(2); [theRule setInterval: [self repeat1]]; // We recur on specific days... - if ([[self repeat2] intValue] == 1 - && [[self repeat5] intValue] > 0) - { - [theRule setNamedValue: @"bymonthday" to: [self repeat5]]; - } - else - { - // TODO - } + if ([[self repeat2] intValue] == 0) + { + NSString *day; + int occurence; + + day = [theRule iCalRepresentationForWeekDay: [[self repeat4] intValue]]; + occurence = [[self repeat3] intValue] + 1; + if (occurence > 5) // the first/second/third/fourth/fifth .. + occurence = -1; // the last .. + [theRule setNamedValue: @"byday" + to: [NSString stringWithFormat: @"%d%@", + occurence, day]]; + } + else + { + if ([[self repeat5] intValue] > 0) + [theRule setNamedValue: @"bymonthday" to: [self repeat5]]; + } } } break; - // YEARLY + // YEARLY (3) // // repeat1 holds the value of "Every X year(s)" // @@ -1756,7 +1782,7 @@ RANGE(2); // repeat4 holds the value of the MONTH parameter (0 -> January, 1 -> February ... ) // ex: 3 February // - // repeat5 holds the value of the OCCURENCE parameter (0 -> First, 1 -> Second ..) + // repeat5 holds the value of the OCCURENCE parameter (0 -> First, 1 -> Second .., 5 -> Last) // repeat6 holds the value of the DAY parameter (0 -> Sunday, 1 -> Monday, etc..) // repeat7 holds the value of the MONTH parameter (0 -> January, 1 -> February ... ) // @@ -1771,7 +1797,19 @@ RANGE(2); // We recur Every .. of .. if ([[self repeat2] intValue] == 1) { - // TODO + NSString *day; + int occurence; + + day = [theRule iCalRepresentationForWeekDay: [[self repeat6] intValue]]; + occurence = [[self repeat5] intValue] + 1; + if (occurence > 5) // the first/second/third/fourth/fifth .. + occurence = -1; // the last .. + [theRule setNamedValue: @"byday" + to: [NSString stringWithFormat: @"%d%@", + occurence, day]]; + [theRule setNamedValue: @"bymonth" + to: [NSString stringWithFormat: @"%d", + [[self repeat7] intValue] + 1]]; } else { @@ -1883,11 +1921,7 @@ RANGE(2); } else if ([repeat caseInsensitiveCompare: @"EVERY WEEKDAY"] == NSOrderedSame) { - [rule setByDayMask: (iCalWeekDayMonday - |iCalWeekDayTuesday - |iCalWeekDayWednesday - |iCalWeekDayThursday - |iCalWeekDayFriday)]; + [rule setByDayMask: [iCalByDayMask byDayMaskWithWeekDays]]; [rule setFrequency: iCalRecurrenceFrequenceDaily]; } else if ([repeat caseInsensitiveCompare: @"MONTHLY"] == NSOrderedSame diff --git a/UI/WebServerResources/UIxRecurrenceEditor.js b/UI/WebServerResources/UIxRecurrenceEditor.js index ede7cd19a..7e0807af5 100644 --- a/UI/WebServerResources/UIxRecurrenceEditor.js +++ b/UI/WebServerResources/UIxRecurrenceEditor.js @@ -241,32 +241,26 @@ function handleMonthlyRecurrence() { var radioValue = $('recurrence_form').getRadioValue('monthlyRadioButtonName'); - // FIXME - right now we do not support rules - // such as The Second Tuesday... - if (radioValue == 0) - window.alert(recurrenceUnsupported); - else { - // We check if the monthlyMonthsField really contains an integer - var showError = true; + // We check if the monthlyMonthsField really contains an integer + var showError = true; - var fieldValue = "" + $('monthlyMonthsField').value; - if (fieldValue.length > 0) { - var v = parseInt(fieldValue); - if (!isNaN(v) && v > 0) { - validate = true; - showError = false; - parent$("repeat1").value = fieldValue; - parent$("repeat2").value = radioValue; - parent$("repeat3").value = $('monthlyRepeat').value; - parent$("repeat4").value = $('monthlyDay').value; - parent$("repeat5").value = getSelectedDays("month"); - } + var fieldValue = "" + $('monthlyMonthsField').value; + if (fieldValue.length > 0) { + var v = parseInt(fieldValue); + if (!isNaN(v) && v > 0) { + validate = true; + showError = false; + parent$("repeat1").value = fieldValue; + parent$("repeat2").value = radioValue; + parent$("repeat3").value = $('monthlyRepeat').value; + parent$("repeat4").value = $('monthlyDay').value; + parent$("repeat5").value = getSelectedDays("month"); } - - if (showError) - window.alert(monthFieldInvalid); } - + + if (showError) + window.alert(monthFieldInvalid); + return validate; } @@ -279,15 +273,20 @@ function validateYearlyRecurrence() { // We check if the yearlyYearsField really contains an integer var v = parseInt(fieldValue); if (!isNaN(v) && v > 0) { - errorToShow = 1; - fieldValue = "" + $('yearlyDayField').value; - if (fieldValue.length > 0) { - // We check if the yearlyYearsField really contains an integer - var v = parseInt(fieldValue); - if (!isNaN(v) && v > 0) { - errorToShow = -1; + var radioValue = $('recurrence_form').getRadioValue('yearlyRadioButtonName'); + if (radioValue == 0) { + errorToShow = 1; + fieldValue = "" + $('yearlyDayField').value; + if (fieldValue.length > 0) { + // We check if the yearlyDayField really contains an integer + var v = parseInt(fieldValue); + if (!isNaN(v) && v > 0) { + errorToShow = -1; + } } } + else + errorToShow = -1; } } @@ -300,34 +299,28 @@ function validateYearlyRecurrence() { function handleYearlyRecurrence() { var validate = false; - var radioValue = $('recurrence_form').getRadioValue('yearlyRadioButtonName'); - // FIXME - right now we do not support rules - // such as Every Second Tuesday of February - if (radioValue == 1) - window.alert(recurrenceUnsupported); - else { - if (validateYearlyRecurrence()) { - var fieldValue = "" + $('yearlyYearsField').value; - if (fieldValue.length > 0) { - // We check if the yearlyYearsField really contains an integer - var v = parseInt(fieldValue); - if (!isNaN(v) && v > 0) { - validate = true; - showError = false; - parent$("repeat1").value = fieldValue; - parent$("repeat2").value = radioValue; - parent$("repeat3").value = $('yearlyDayField').value; - parent$("repeat4").value = $('yearlyMonth1').value; - parent$("repeat5").value = $('yearlyRepeat').value; - parent$("repeat6").value = $('yearlyDay').value; - parent$("repeat7").value = $('yearlyMonth2').value; - } + if (validateYearlyRecurrence()) { + var radioValue = $('recurrence_form').getRadioValue('yearlyRadioButtonName'); + var fieldValue = "" + $('yearlyYearsField').value; + if (fieldValue.length > 0) { + // We check if the yearlyYearsField (interval) really contains an integer + var v = parseInt(fieldValue); + if (!isNaN(v) && v > 0) { + validate = true; + showError = false; + parent$("repeat1").value = fieldValue; + parent$("repeat2").value = radioValue; + parent$("repeat3").value = $('yearlyDayField').value; + parent$("repeat4").value = $('yearlyMonth1').value; + parent$("repeat5").value = $('yearlyRepeat').value; + parent$("repeat6").value = $('yearlyDay').value; + parent$("repeat7").value = $('yearlyMonth2').value; } } - else - validate = false; } - + else + validate = false; + return validate; }