]> err.no Git - sope/blob - sope-ical/NGiCal/iCalMonthlyRecurrenceCalculator.m
a9045ab38d8b8adf9ca859949fc98860f6e76ec4
[sope] / sope-ical / NGiCal / iCalMonthlyRecurrenceCalculator.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
24 @interface iCalMonthlyRecurrenceCalculator : iCalRecurrenceCalculator
25 @end
26
27 #include <NGExtensions/NGCalendarDateRange.h>
28 #include "iCalRecurrenceRule.h"
29 #include "NSCalendarDate+ICal.h"
30 #include "common.h"
31
32 @interface iCalRecurrenceCalculator(PrivateAPI)
33 - (NSCalendarDate *)lastInstanceStartDate;
34 @end
35
36 @implementation iCalMonthlyRecurrenceCalculator
37
38 typedef BOOL NGMonthSet[12];
39 typedef BOOL NGMonthDaySet[31];
40
41 static void NGMonthDaySet_clear(NGMonthDaySet *daySet) {
42   register unsigned i;
43   
44   for (i = 0; i < 31; i++)
45     (*daySet)[i] = NO;
46 }
47
48 static void NGMonthDaySet_copyOrUnion(NGMonthDaySet *base, NGMonthDaySet *new,
49                                       BOOL doCopy)
50 {
51   register unsigned i;
52   
53   if (doCopy)
54     memcpy(base, new, sizeof(NGMonthDaySet));
55   else {
56     for (i = 0; i < 31; i++) {
57       if (!(*new)[i])
58         (*base)[i] = NO;
59     }
60   }
61 }
62
63 static void NGMonthDaySet_fillWithByMonthDay(NGMonthDaySet *daySet, 
64                                              NSArray *byMonthDay,
65                                              int numDaysInMonth)
66 {
67   /* list of days in the month */
68   unsigned i, count;
69   
70   NGMonthDaySet_clear(daySet);
71
72   for (i = 0, count = [byMonthDay count]; i < count; i++) {
73     int dayInMonth; /* -31..-1 and 1..31 */
74         
75     if ((dayInMonth = [[byMonthDay objectAtIndex:i] intValue]) == 0)
76       continue; /* invalid value */
77     if (dayInMonth > numDaysInMonth)
78       continue; /* this month has less days */
79     if (dayInMonth < -numDaysInMonth)
80       continue; /* this month has less days */
81         
82     /* adjust negative days */
83         
84     if (dayInMonth < 0) {
85       /* eg: -1 == last day in month, 30 days => 30 */
86       dayInMonth = 32 - dayInMonth /* because we count from 1 */;
87     }
88     
89     (*daySet)[dayInMonth] = YES;
90   }
91 }
92
93 static void NGMonthDaySet_fillWithByDayX(NGMonthDaySet *daySet, 
94                                          unsigned dayMask,
95                                          int occurrence1)
96 {
97   unsigned i, count;
98   
99   NGMonthDaySet_clear(daySet);
100   // TODO: complete me
101 }
102
103 - (NSArray *)recurrenceRangesWithinCalendarDateRange:(NGCalendarDateRange *)_r{
104   /* main entry */
105   NSMutableArray *ranges;
106   NSTimeZone     *timeZone;
107   NSCalendarDate *eventStartDate, *rStart, *rEnd, *until;
108   unsigned       monthIdxInRange, numberOfMonthsInRange, interval;
109   int            diff;
110   NGMonthSet byMonthList = { // TODO: fill from rrule, this is the default
111     YES, YES, YES, YES, YES, YES, 
112     YES, YES, YES, YES, YES, YES
113   };
114   NSArray *byMonthDay = nil; // array of ints (-31..-1 and 1..31)
115   
116   eventStartDate = [self->firstRange startDate];
117   timeZone = [eventStartDate timeZone];
118   rStart   = [_r startDate];
119   rEnd     = [_r endDate];
120   interval = [self->rrule repeatInterval];
121   until    = [self lastInstanceStartDate]; // TODO: maybe replace
122   
123   if ([self->rrule byDayMask] != 0) {
124     [self errorWithFormat:@"cannot process byday part of rrule: %@", 
125             self->rrule];
126     return nil;
127   }
128   
129   /* check whether the range to be processed is beyond the 'until' date */
130   
131   if (until != nil) {
132     if ([until compare:rStart] == NSOrderedAscending) /* until before start */
133       return nil;
134     if ([until compare:rEnd] == NSOrderedDescending) /* end before until */
135       rEnd = until; // TODO: why is that? end is _before_ until?
136   }
137   
138   // TODO: I think the 'diff' is to skip recurrence which are before the
139   //       requested range. Not sure whether this is actually possible, eg
140   //       the repeatCount must be processed from the start.
141   diff = [eventStartDate monthsBetweenDate:rStart];
142   if ((diff != 0) && [rStart compare:eventStartDate] == NSOrderedAscending)
143     diff = -diff;
144   
145   numberOfMonthsInRange  = [rStart monthsBetweenDate:rEnd] + 1;
146   ranges = [NSMutableArray arrayWithCapacity:numberOfMonthsInRange];
147   
148   /* 
149      Note: we do not add 'eventStartDate', this is intentional, the event date
150            itself is _not_ necessarily part of the sequence, eg with monthly
151            byday recurrences.
152   */
153   
154   for (monthIdxInRange = 0; monthIdxInRange < numberOfMonthsInRange; 
155        monthIdxInRange++) {
156     NSCalendarDate      *cursor;
157     NSCalendarDate      *start, *end;
158     NGCalendarDateRange *r;
159     unsigned            numDaysInMonth;
160     int                 monthIdxInRecurrence;
161     NGMonthDaySet monthDays;
162     BOOL          didByFill;
163     
164     monthIdxInRecurrence = diff + monthIdxInRange;
165     
166     if (monthIdxInRecurrence < 0)
167       continue;
168     
169     /* first check whether we are in the interval */
170     
171     if ((monthIdxInRecurrence % interval) != 0)
172       continue;
173
174     /*
175       Then the sequence is:
176       - check whether the month is in the BYMONTH list
177     */
178     
179     cursor = [eventStartDate dateByAddingYears:0
180                              months:(diff + monthIdxInRange)
181                              days:0];
182     [cursor setTimeZone:timeZone];
183     numDaysInMonth = [cursor numberOfDaysInMonth];
184     
185
186     /* check whether we match the bymonth specification */
187     
188     if (!byMonthList[[cursor monthOfYear] - 1])
189       continue;
190     
191     
192     /* check 'day level' byXYZ rules */
193     
194     didByFill = NO;
195     
196     if (byMonthDay != nil) { /* list of days in the month */
197       NGMonthDaySet ruleset;
198       
199       NGMonthDaySet_fillWithByMonthDay(&ruleset, byMonthDay, numDaysInMonth);
200       NGMonthDaySet_copyOrUnion(&monthDays, &ruleset, !didByFill);
201       didByFill = YES;
202     }
203     
204     if ([self->rrule byDayMask] != 0) { // TODO: replace the mask with an array
205       NGMonthDaySet ruleset;
206       
207       NGMonthDaySet_fillWithByDayX(&ruleset, 
208                                    [self->rrule byDayMask],
209                                    [self->rrule byDayOccurence1]);
210       NGMonthDaySet_copyOrUnion(&monthDays, &ruleset, !didByFill);
211       didByFill = YES;
212     }
213     
214     
215     // TODO: complete byday support
216     
217     /* set start date */
218     
219     start = cursor;
220     
221     /* check whether we are still in the limits */
222     
223     // TODO: I think we should check in here whether we succeeded the
224     //       repeatCount. Currently we precalculate that info in the
225     //       -lastInstanceStartDate method.
226     if (until != nil) {
227       /* Note: the 'until' in the rrule is inclusive as per spec */
228       if ([until compare:start] == NSOrderedAscending) /* start after until */
229         break; /* Note: we assume that the algorithm is sequential */
230     }
231     
232     /* create end date */
233
234     end   = [start addTimeInterval:[self->firstRange duration]];
235     [end setTimeZone:timeZone];
236     
237     /* create range and check whether its in the requested range */
238     
239     r = [[NGCalendarDateRange alloc] initWithStartDate:start endDate:end];
240     if ([_r containsDateRange:r])
241       [ranges addObject:r];
242     [r release]; r = nil;
243   }
244   return ranges;
245 }
246
247 - (NSCalendarDate *)lastInstanceStartDate {
248   if ([self->rrule repeatCount] > 0) {
249     NSCalendarDate *until;
250     unsigned       months, interval;
251     
252     interval = [self->rrule repeatInterval];
253     months   = [self->rrule repeatCount] - 1 /* the first counts as one! */;
254     
255     if (interval > 0)
256       months *= interval;
257     
258     until = [[self->firstRange startDate] dateByAddingYears:0
259                                           months:months
260                                           days:0];
261     return until;
262   }
263   return [super lastInstanceStartDate];
264 }
265
266 @end /* iCalMonthlyRecurrenceCalculator */