1/*
2  Copyright (C) 2004-2005 SKYRIX Software AG
3  Copyright (C) 2006-2012 Inverse inc.
4
5  This file is part of SOPE.
6
7  SOPE is free software; you can redistribute it and/or modify it under
8  the terms of the GNU Lesser General Public License as published by the
9  Free Software Foundation; either version 2, or (at your option) any
10  later version.
11
12  SOPE is distributed in the hope that it will be useful, but WITHOUT ANY
13  WARRANTY; without even the implied warranty of MERCHANTABILITY or
14  FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
15  License for more details.
16
17  You should have received a copy of the GNU Lesser General Public
18  License along with SOPE; see the file COPYING. If not, write to the
19  Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
20  02111-1307, USA.
21*/
22
23#import <NGExtensions/NSCalendarDate+misc.h>
24#import <NGExtensions/NSObject+Logs.h>
25
26#import "iCalRecurrenceCalculator.h"
27
28@interface iCalWeeklyRecurrenceCalculator : iCalRecurrenceCalculator
29@end
30
31#import <NGExtensions/NGCalendarDateRange.h>
32#import "iCalByDayMask.h"
33#import "NSCalendarDate+ICal.h"
34
35@interface iCalRecurrenceCalculator (PrivateAPI)
36
37- (NSCalendarDate *) lastInstanceStartDate;
38
39- (unsigned) offsetFromSundayForJulianNumber: (long) _jn;
40- (unsigned) offsetFromSundayForWeekDay: (iCalWeekDay) _weekDay;
41- (unsigned) offsetFromSundayForCurrentWeekStart;
42
43- (iCalWeekDay) weekDayForJulianNumber: (long) _jn;
44
45@end
46
47@implementation iCalWeeklyRecurrenceCalculator
48
49/**
50 * TODO : Unsupported conditions for WEEKLY recurrences :
51 *
52 *   BYYEAR
53 *   BYYEARDAY
54 *   BYWEEKNO
55 *   BYMONTH
56 *   BYMONTHDAY
57 *   BYHOUR
58 *   BYMINUTE
59 *   WKST
60 *
61 * There's no GUI to defined such conditions, so there's no
62 * problem for now.
63 */
64- (NSArray *) recurrenceRangesWithinCalendarDateRange: (NGCalendarDateRange *) _r
65{
66  NSMutableArray *ranges;
67  NSCalendarDate *firStart, *startDate, *endDate, *currentStartDate, *currentEndDate;
68  long i, repeatCount, count;
69  unsigned interval;
70  iCalByDayMask *dayMask;
71  BOOL hasRepeatCount;
72
73  //[self logWithFormat: @"Recurrence rule is %@", rrule];
74
75  firStart = [firstRange startDate];
76  startDate = [_r startDate];
77  endDate = [_r endDate];
78  dayMask = nil;
79  repeatCount = 0;
80  hasRepeatCount = [rrule hasRepeatCount];
81
82  if (!firstRange)
83    // Can happen when event/todo has a rrule with no dtstart
84    return nil;
85
86  if ([endDate compare: firStart] == NSOrderedAscending)
87    // Range ends before first occurrence
88    return nil;
89
90  interval = [rrule repeatInterval];
91
92  if ([[rrule byDay] length])
93    dayMask = [rrule byDayMask];
94
95  // If rule is bound, check the bounds
96  if (![rrule isInfinite])
97    {
98      NSCalendarDate *until, *lastDate;
99
100      lastDate = nil;
101      until = [rrule untilDate];
102      if (until)
103        lastDate = until;
104      else
105        {
106          repeatCount = [rrule repeatCount];
107          if (dayMask == nil)
108            // When there's no BYxxx mask, we can find the date of the last
109            // occurrence.
110            lastDate = [firStart dateByAddingYears: 0 months: 0
111                                              days: (interval
112                                                     * (repeatCount - 1) * 7)];
113        }
114
115      if (lastDate != nil)
116        {
117          if ([lastDate compare: startDate] == NSOrderedAscending)
118            // Range starts after last occurrence
119            return nil;
120          if ([lastDate compare: endDate] == NSOrderedAscending)
121            // Range ends after last occurence; adjust end date
122            endDate = [lastDate addTimeInterval: [firstRange duration]];
123        }
124    }
125
126  currentStartDate = [firStart copy];
127  [currentStartDate autorelease];
128  ranges = [NSMutableArray array];
129  count = 0;
130
131  if (dayMask == nil)
132    {
133      i = 0;
134      while ([currentStartDate compare: endDate] == NSOrderedAscending ||
135             [currentStartDate compare: endDate] == NSOrderedSame)
136        {
137          currentEndDate = [currentStartDate addTimeInterval: [firstRange duration]];
138          if ([startDate compare: currentEndDate] == NSOrderedAscending ||
139              ([firstRange duration] == 0 && [startDate compare: currentEndDate] == NSOrderedSame))
140            {
141              NGCalendarDateRange *r;
142
143              r = [NGCalendarDateRange calendarDateRangeWithStartDate: currentStartDate
144                                                              endDate: currentEndDate];
145              [ranges addObject: r];
146            }
147          i++;
148          currentStartDate = [firStart dateByAddingYears: 0
149                                                  months: 0
150                                                    days: (interval * i * 7)];
151        }
152    }
153  else
154    {
155      NGCalendarDateRange *r;
156
157      i = [currentStartDate dayOfWeek]; // Set the first day of the week as Sunday and ignore WKST
158      while ([currentStartDate compare: endDate] == NSOrderedAscending ||
159             [currentStartDate compare: endDate] == NSOrderedSame)
160        {
161          BOOL isRecurrence = NO;
162          NSInteger week;
163
164          currentEndDate = [currentStartDate addTimeInterval: [firstRange duration]];
165          if (hasRepeatCount ||
166              [startDate compare: currentEndDate] == NSOrderedAscending ||
167              ([startDate compare: currentEndDate] == NSOrderedSame && [firstRange duration] == 0))
168            {
169              // If the rule count is defined, stop once the count is reached.
170              if ([currentStartDate compare: firStart] == NSOrderedSame)
171                {
172                  // Always add the start date of the recurring event if within
173                  // the lookup range.
174                  isRecurrence = YES;
175                }
176              else
177                {
178                  week = i / 7;
179
180                  if ((week % interval) == 0 &&
181                      [dayMask occursOnDay: [currentStartDate dayOfWeek]])
182                    isRecurrence = YES;
183                }
184
185              if (isRecurrence)
186                {
187                  count++;
188                  if (repeatCount > 0 && count > repeatCount)
189                    break;
190                  r = [NGCalendarDateRange calendarDateRangeWithStartDate: currentStartDate
191                                                                  endDate: currentEndDate];
192                  if ([_r doesIntersectWithDateRange: r])
193                    {
194                      [ranges addObject: r];
195                      // [self logWithFormat: @"Add range %@ - %@", [r startDate], [r endDate]];
196                    }
197                }
198            }
199          currentStartDate = [currentStartDate dateByAddingYears: 0
200                                                          months: 0
201                                                            days: 1];
202          i++;
203        }
204    }
205
206  return ranges;
207}
208
209- (NSCalendarDate *) lastInstanceStartDate
210{
211  NSCalendarDate *firStart, *lastInstanceStartDate;
212  NGCalendarDateRange *r;
213  NSArray *instances;
214
215  lastInstanceStartDate = nil;
216  if ([rrule repeatCount] > 0)
217    {
218      firStart = [firstRange startDate];
219      r = [NGCalendarDateRange calendarDateRangeWithStartDate: firStart
220                                                      endDate: [NSCalendarDate distantFuture]];
221      instances = [self recurrenceRangesWithinCalendarDateRange: r];
222      if ([instances count])
223        lastInstanceStartDate = [(NGCalendarDateRange *)[instances lastObject] startDate];
224    }
225  else
226    lastInstanceStartDate = [super lastInstanceStartDate];
227
228  return lastInstanceStartDate;
229}
230
231@end /* iCalWeeklyRecurrenceCalculator */
232