]> err.no Git - sope/blob - sope-ical/NGiCal/iCalRecurrenceCalculator.m
d144efead70b613c7cb8a9b727896ced0033dd9b
[sope] / sope-ical / NGiCal / iCalRecurrenceCalculator.m
1 /*
2   Copyright (C) 2004-2005 SKYRIX Software AG
3   
4   This file is part of SOPE.
5   
6   SOPE is free software; you can redistribute it and/or modify it under
7   the terms of the GNU Lesser General Public License as published by the
8   Free Software Foundation; either version 2, or (at your option) any
9   later version.
10   
11   SOPE is distributed in the hope that it will be useful, but WITHOUT ANY
12   WARRANTY; without even the implied warranty of MERCHANTABILITY or
13   FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public
14   License for more details.
15   
16   You should have received a copy of the GNU Lesser General Public
17   License along with SOPE; see the file COPYING.  If not, write to the
18   Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
19   02111-1307, USA.
20 */
21
22 #include "iCalRecurrenceCalculator.h"
23 #include <NGExtensions/NGCalendarDateRange.h>
24 #include "iCalRecurrenceRule.h"
25 #include "NSCalendarDate+ICal.h"
26 #include "common.h"
27
28 /* class cluster */
29
30
31 /* Private */
32
33 @interface iCalRecurrenceCalculator (PrivateAPI)
34 - (NSCalendarDate *)lastInstanceStartDate;
35
36 - (unsigned)offsetFromSundayForJulianNumber:(long)_jn;
37 - (unsigned)offsetFromSundayForWeekDay:(iCalWeekDay)_weekDay;
38 - (unsigned)offsetFromSundayForCurrentWeekStart;
39   
40 - (iCalWeekDay)weekDayForJulianNumber:(long)_jn;
41 @end
42
43 @implementation iCalRecurrenceCalculator
44
45 static Class NSCalendarDateClass     = Nil;
46 static Class iCalRecurrenceRuleClass = Nil;
47 static Class dailyCalcClass   = Nil;
48 static Class weeklyCalcClass  = Nil;
49 static Class monthlyCalcClass = Nil;
50 static Class yearlyCalcClass  = Nil;
51
52 + (void)initialize {
53   static BOOL didInit = NO;
54   
55   if (didInit) return;
56   didInit = YES;
57
58   NSCalendarDateClass     = [NSCalendarDate class];
59   iCalRecurrenceRuleClass = [iCalRecurrenceRule class];
60
61   dailyCalcClass   = NSClassFromString(@"iCalDailyRecurrenceCalculator");
62   weeklyCalcClass  = NSClassFromString(@"iCalWeeklyRecurrenceCalculator");
63   monthlyCalcClass = NSClassFromString(@"iCalMonthlyRecurrenceCalculator");
64   yearlyCalcClass  = NSClassFromString(@"iCalYearlyRecurrenceCalculator");
65 }
66
67 /* factory */
68
69 + (id)recurrenceCalculatorForRecurrenceRule:(iCalRecurrenceRule *)_rrule
70   withFirstInstanceCalendarDateRange:(NGCalendarDateRange *)_range
71 {
72   return [[[self alloc] initWithRecurrenceRule:_rrule
73                         firstInstanceCalendarDateRange:_range] autorelease];
74 }
75
76 /* complex calculation convenience */
77
78 + (NSArray *)recurrenceRangesWithinCalendarDateRange:(NGCalendarDateRange *)_r
79   firstInstanceCalendarDateRange:(NGCalendarDateRange *)_fir
80   recurrenceRules:(NSArray *)_rRules
81   exceptionRules:(NSArray *)_exRules
82   exceptionDates:(NSArray *)_exDates
83 {
84   id                       rule;
85   iCalRecurrenceCalculator *calc;
86   NSMutableArray           *ranges;
87   NSMutableArray           *exDates;
88   unsigned                 i, count, rCount;
89   
90   ranges = [NSMutableArray arrayWithCapacity:64];
91   
92   for (i = 0, count  = [_rRules count]; i < count; i++) {
93     NSArray *rs;
94
95     rule = [_rRules objectAtIndex:i];
96     if (![rule isKindOfClass:iCalRecurrenceRuleClass])
97       rule = [iCalRecurrenceRule recurrenceRuleWithICalRepresentation:rule];
98   
99     calc = [self recurrenceCalculatorForRecurrenceRule:rule
100                  withFirstInstanceCalendarDateRange:_fir];
101     rs   = [calc recurrenceRangesWithinCalendarDateRange:_r];
102     [ranges addObjectsFromArray:rs];
103   }
104   
105   if ([ranges count] == 0)
106     return nil;
107   
108   /* test if any exceptions do match */
109   
110   for (i = 0, count = [_exRules count]; i < count; i++) {
111     NSArray *rs;
112
113     rule = [_exRules objectAtIndex:i];
114     if (![rule isKindOfClass:iCalRecurrenceRuleClass])
115       rule = [iCalRecurrenceRule recurrenceRuleWithICalRepresentation:rule];
116
117     calc = [self recurrenceCalculatorForRecurrenceRule:rule
118                  withFirstInstanceCalendarDateRange:_fir];
119     rs   = [calc recurrenceRangesWithinCalendarDateRange:_r];
120     [ranges removeObjectsInArray:rs];
121   }
122   
123   if (![ranges isNotEmpty])
124     return nil;
125   
126   /* exception dates */
127
128   if ((count = [_exDates count]) == 0)
129     return ranges;
130   
131   /* sort out exDates not within range */
132
133   exDates = [NSMutableArray arrayWithCapacity:count];
134   for (i = 0; i < count; i++) {
135     id exDate;
136
137     exDate = [_exDates objectAtIndex:i];
138     if (![exDate isKindOfClass:NSCalendarDateClass])
139       exDate = [NSCalendarDate calendarDateWithICalRepresentation:exDate];
140     
141     if ([_r containsDate:exDate])
142       [exDates addObject:exDate];
143   }
144
145   /* remove matching exDates from ranges */
146
147   if ((count = [exDates count]) == 0)
148     return ranges;
149   
150   for (i = 0, rCount = [ranges count]; i < count; i++) {
151     NSCalendarDate      *exDate;
152     NGCalendarDateRange *r;
153     unsigned            k;
154
155     exDate = [exDates objectAtIndex:i];
156     for (k = 0; k < rCount; k++) {
157       unsigned rIdx;
158       
159       rIdx = (rCount - k) - 1;
160       r    = [ranges objectAtIndex:rIdx];
161       if ([r containsDate:exDate]) {
162         [ranges removeObjectAtIndex:rIdx];
163         rCount--;
164         break; /* this is safe because we know that ranges don't overlap */
165       }
166     }
167   }
168   return ranges;
169 }
170
171
172 /* init */
173
174 - (id)initWithRecurrenceRule:(iCalRecurrenceRule *)_rrule
175   firstInstanceCalendarDateRange:(NGCalendarDateRange *)_range
176 {
177   iCalRecurrenceFrequency freq;
178   Class calcClass = Nil;
179
180   freq = [_rrule frequency];
181   if (freq == iCalRecurrenceFrequenceDaily)
182     calcClass = dailyCalcClass;
183   else if (freq == iCalRecurrenceFrequenceWeekly)
184     calcClass = weeklyCalcClass;
185   else if (freq == iCalRecurrenceFrequenceMonthly)
186     calcClass = monthlyCalcClass;
187   else if (freq == iCalRecurrenceFrequenceYearly)
188     calcClass = yearlyCalcClass;
189   else {
190     [self errorWithFormat:@"unsupported rrule frequency: %@", _rrule];
191     calcClass = Nil;
192     [self release];
193     return nil;
194   }
195   
196   [self autorelease]; // TODO: why autorelease?
197   if (calcClass == Nil)
198     return nil;
199   
200   if ((self = [[calcClass alloc] init]) != nil) {
201     self->rrule      = [_rrule retain];
202     self->firstRange = [_range retain];
203   }
204   return self;  
205 }
206
207 - (void)dealloc {
208   [self->firstRange release];
209   [self->rrule      release];
210   [super dealloc];
211 }
212
213 /* helpers */
214
215 - (unsigned)offsetFromSundayForJulianNumber:(long)_jn {
216   return (unsigned)((int)(_jn + 1.5)) % 7;
217 }
218
219 - (unsigned)offsetFromSundayForWeekDay:(iCalWeekDay)_weekDay {
220   unsigned offset;
221   
222   switch (_weekDay) {
223     case iCalWeekDaySunday:    offset = 0; break;
224     case iCalWeekDayMonday:    offset = 1; break;
225     case iCalWeekDayTuesday:   offset = 2; break;
226     case iCalWeekDayWednesday: offset = 3; break;
227     case iCalWeekDayThursday:  offset = 4; break;
228     case iCalWeekDayFriday:    offset = 5; break;
229     case iCalWeekDaySaturday:  offset = 6; break;
230     default:                   offset = 0; break;
231   }
232   return offset;
233 }
234
235 - (unsigned)offsetFromSundayForCurrentWeekStart {
236   return [self offsetFromSundayForWeekDay:[self->rrule weekStart]];
237 }
238
239 - (iCalWeekDay)weekDayForJulianNumber:(long)_jn {
240   unsigned    day;
241   iCalWeekDay weekDay;
242
243   day = [self offsetFromSundayForJulianNumber:_jn];
244   switch (day) {
245     case 0:  weekDay = iCalWeekDaySunday;    break;
246     case 1:  weekDay = iCalWeekDayMonday;    break;
247     case 2:  weekDay = iCalWeekDayTuesday;   break;
248     case 3:  weekDay = iCalWeekDayWednesday; break;
249     case 4:  weekDay = iCalWeekDayThursday;  break;
250     case 5:  weekDay = iCalWeekDayFriday;    break;
251     case 6:  weekDay = iCalWeekDaySaturday;  break;
252     default: 
253       [self errorWithFormat:@"got unexpected weekday: %d", day];
254       weekDay = iCalWeekDaySunday;
255       break; /* keep compiler happy */
256   }
257   return weekDay;
258 }
259
260 /* calculation */
261
262 - (NSArray *)recurrenceRangesWithinCalendarDateRange:(NGCalendarDateRange *)_r{
263   return nil; /* subclass responsibility */
264 }
265 - (BOOL)doesRecurrWithinCalendarDateRange:(NGCalendarDateRange *)_range {
266   NSArray *ranges;
267
268   ranges = [self recurrenceRangesWithinCalendarDateRange:_range];
269   return (ranges == nil || [ranges count] == 0) ? NO : YES;
270 }
271
272 - (NGCalendarDateRange *)firstInstanceCalendarDateRange {
273   return self->firstRange;
274 }
275
276 - (NGCalendarDateRange *)lastInstanceCalendarDateRange {
277   NSCalendarDate *start, *end;
278
279   if ((start = [self lastInstanceStartDate]) == nil)
280     return nil;
281   
282   end   = [start addTimeInterval:[self->firstRange duration]];
283   return [NGCalendarDateRange calendarDateRangeWithStartDate:start
284                               endDate:end];
285 }
286
287 - (NSCalendarDate *)lastInstanceStartDate {
288   NSCalendarDate *until;
289   
290   /* 
291      NOTE: this is horribly inaccurate and doesn't even consider the use
292            of repeatCount. It MUST be implemented by subclasses properly!
293            However, it does the trick for SOGo 1.0 - that's why it's left here.
294   */
295   if ((until = [self->rrule untilDate]) != nil)
296     return until;
297   
298   return nil;
299 }
300
301 @end /* iCalRecurrenceCalculator */