/* 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 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 #import "iCalRecurrenceCalculator.h" #import #import "iCalRecurrenceRule.h" #import "NSCalendarDate+ICal.h" @interface iCalYearlyRecurrenceCalculator : iCalRecurrenceCalculator @end @interface iCalMonthlyRecurrenceCalculator : iCalRecurrenceCalculator @end @interface iCalRecurrenceCalculator (PrivateAPI) - (NSCalendarDate *) lastInstanceStartDate; @end @implementation iCalYearlyRecurrenceCalculator - (NSArray *) recurrenceRangesWithinCalendarDateRange: (NGCalendarDateRange *) _r { NSMutableArray *ranges; 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]; byMonth = [rrule byMonth]; diff = 0; repeatCount = 0; count = 0; referenceDate = nil; if (!firstRange) // Can happen when event/todo has a rrule with no dtstart return nil; if ([rEnd compare: firStart] == NSOrderedAscending) // Range ends before first occurrence return nil; // If rule is bound, check the bounds if (![rrule isInfinite]) { lastDate = nil; until = [rrule untilDate]; repeatCount = [rrule repeatCount]; if (until) { 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] == NSOrderedAscending) // 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 // 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. // The interval must be ignored as well since it refers to the years. [rrule setRepeatCount: 0]; [rrule setInterval: @"1"]; // 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 k, test; test = diff + yearIdxInRange; if ((test >= 0) && (test % interval) == 0) { if (byMonth) { monthlyCalc = [[iCalMonthlyRecurrenceCalculator alloc] initWithRecurrenceRule: rrule firstInstanceCalendarDateRange: firstRange]; [monthlyCalc autorelease]; // 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]]; rangeForMonth = [NGCalendarDateRange calendarDateRangeWithStartDate: rStart endDate: rEnd]; rangesInMonth = [monthlyCalc recurrenceRangesWithinCalendarDateRange: rangeForMonth]; for (k = 0; k < [rangesInMonth count] && (repeatCount == 0 || count < repeatCount); k++) { //NSLog(@"*** YEARLY found %@ (count = %i)", [[rangesInMonth objectAtIndex: k] startDate], count); count++; if ([_r doesIntersectWithDateRange: [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]]; if ([start compare: rEnd] == NSOrderedAscending || ([firstRange duration] == 0 && [start compare: rEnd] == NSOrderedSame)) { end = [start addTimeInterval: [firstRange duration]]; r = [NGCalendarDateRange calendarDateRangeWithStartDate: start endDate: end]; if ([_r doesIntersectWithDateRange: r] && (repeatCount == 0 || count < repeatCount)) { [ranges addObject: r]; count++; } } } } else { // Year was skipped, jump to following year monthDiff += (13 - currentMonth); currentMonth = 1; } } if (byMonth) { // Restore the repeat count and interval if (repeatCount > 0) [rrule setRepeatCount: repeatCount]; [rrule setRepeatInterval: interval]; } return ranges; } - (NSCalendarDate *) lastInstanceStartDate { NSCalendarDate *firStart, *lastInstanceStartDate; NGCalendarDateRange *r; NSArray *instances; lastInstanceStartDate = nil; if ([rrule repeatCount] > 0) { firStart = [firstRange startDate]; if ([rrule hasByMask]) { // Must perform the complete calculation 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; } @end /* iCalYearlyRecurrenceCalculator */