1/* 2 Copyright (C) 2004-2005 SKYRIX Software AG 3 Copyright (C) 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 <Foundation/NSArray.h> 24#import <Foundation/NSCalendarDate.h> 25#import <Foundation/NSEnumerator.h> 26#import <Foundation/NSString.h> 27#import <Foundation/NSTimeZone.h> 28 29#import <NGExtensions/NGCalendarDateRange.h> 30 31#import "NSCalendarDate+NGCards.h" 32#import "NSString+NGCards.h" 33#import "iCalDateTime.h" 34#import "iCalEvent.h" 35#import "iCalTimeZone.h" 36#import "iCalTimeZonePeriod.h" 37#import "iCalRecurrenceRule.h" 38#import "iCalRecurrenceCalculator.h" 39#import "iCalRepeatableEntityObject.h" 40 41@implementation iCalRepeatableEntityObject 42 43- (Class) classForTag: (NSString *) classTag 44{ 45 Class tagClass; 46 47 if ([classTag isEqualToString: @"RRULE"]) 48 tagClass = [iCalRecurrenceRule class]; 49 else if ([classTag isEqualToString: @"EXDATE"]) 50 tagClass = [iCalDateTime class]; 51 else 52 tagClass = [super classForTag: classTag]; 53 54 return tagClass; 55} 56 57/* Accessors */ 58 59- (void) removeAllRecurrenceRules 60{ 61 [self removeChildren: [self recurrenceRules]]; 62} 63 64- (void) addToRecurrenceRules: (id) _rrule 65{ 66 [self addChild: _rrule]; 67} 68 69- (void) setRecurrenceRules: (NSArray *) _rrules 70{ 71 [children removeObjectsInArray: [self childrenWithTag: @"rrule"]]; 72 [self addChildren: _rrules]; 73} 74 75- (BOOL) hasRecurrenceRules 76{ 77 return ([[self childrenWithTag: @"rrule"] count] > 0); 78} 79 80- (NSArray *) recurrenceRules 81{ 82 return [self childrenWithTag: @"rrule"]; 83} 84 85- (NSArray *) recurrenceRulesWithTimeZone: (id) timezone 86{ 87 NSArray *rules; 88 89 rules = [self recurrenceRules]; 90 return [self rules: rules withTimeZone: timezone]; 91} 92 93- (void) removeAllExceptionRules 94{ 95 [self removeChildren: [self exceptionRules]]; 96} 97 98- (void) addToExceptionRules: (id) _rrule 99{ 100 [self addChild: _rrule]; 101} 102 103- (void) setExceptionRules: (NSArray *) _rrules 104{ 105 [children removeObjectsInArray: [self childrenWithTag: @"exrule"]]; 106 [self addChildren: _rrules]; 107} 108 109- (BOOL) hasExceptionRules 110{ 111 return ([[self childrenWithTag: @"exrule"] count] > 0); 112} 113 114- (NSArray *) exceptionRules 115{ 116 return [self childrenWithTag: @"exrule"]; 117} 118 119- (NSArray *) exceptionRulesWithTimeZone: (id) timezone 120{ 121 NSArray *rules; 122 123 rules = [self exceptionRules]; 124 return [self rules: rules withTimeZone: timezone]; 125} 126 127/** 128 * Returns a new set of rules, but with "until dates" adjusted to the 129 * specified timezone. 130 * Used when calculating a recurrence/exception rule. 131 * @param theRules the iCalRecurrenceRule instances 132 * @param theTimeZone the timezone of the entity. 133 * @see recurrenceRulesWithTimeZone: 134 * @see exceptionRulesWithTimeZone: 135 * @return a new array of iCalRecurrenceRule instances, adjusted for the timezone. 136 */ 137- (NSArray *) rules: (NSArray *) theRules withTimeZone: (id) theTimeZone 138{ 139 NSArray *rules; 140 NSCalendarDate *untilDate; 141 NSMutableArray *fixedRules; 142 iCalRecurrenceRule *currentRule; 143 int offset; 144 unsigned int max, count; 145 146 rules = theRules; 147 if (theTimeZone) 148 { 149 max = [rules count]; 150 if (max) 151 { 152 fixedRules = [NSMutableArray arrayWithCapacity: max]; 153 for (count = 0; count < max; count++) 154 { 155 currentRule = [rules objectAtIndex: count]; 156 untilDate = [currentRule untilDate]; 157 if (untilDate) 158 { 159 if ([theTimeZone isKindOfClass: [iCalTimeZone class]]) 160 untilDate = [(iCalTimeZone *) theTimeZone computedDateForDate: untilDate]; 161 else 162 { 163 offset = [(NSTimeZone *) theTimeZone secondsFromGMTForDate: untilDate]; 164 untilDate = (NSCalendarDate *) [untilDate dateByAddingYears:0 months:0 days:0 hours:0 minutes:0 165 seconds:-offset]; 166 } 167 [currentRule setUntilDate: untilDate]; 168 } 169 [fixedRules addObject: currentRule]; 170 } 171 rules = fixedRules; 172 } 173 } 174 175 return rules; 176} 177 178- (void) removeAllExceptionDates 179{ 180 [self removeChildren: [self childrenWithTag: @"exdate"]]; 181} 182 183- (void) addToExceptionDates: (NSCalendarDate *) _rdate 184{ 185 iCalDateTime *dateTime; 186 187 dateTime = [iCalDateTime new]; 188 [dateTime setTag: @"exdate"]; 189 if ([self isKindOfClass: [iCalEvent class]] && [(iCalEvent *)self isAllDay]) 190 [dateTime setDate: _rdate]; 191 else 192 [dateTime setDateTime: _rdate]; 193 [self addChild: dateTime]; 194 [dateTime release]; 195} 196 197//- (void) setExceptionDates: (NSArray *) _rdates 198//{ 199// [children removeObjectsInArray: [self childrenWithTag: @"exdate"]]; 200// [self addChildren: _rdates]; 201//} 202 203- (BOOL) hasExceptionDates 204{ 205 return ([[self childrenWithTag: @"exdate"] count] > 0); 206} 207 208/** 209 * Return the exception dates of the entity in GMT. 210 * @return an array of strings. 211 */ 212- (NSArray *) exceptionDates 213{ 214 NSArray *exDates; 215 NSMutableArray *dates; 216 NSEnumerator *dateList; 217 NSCalendarDate *exDate; 218 NSString *dateString; 219 unsigned i; 220 221 dates = [NSMutableArray array]; 222 dateList = [[self childrenWithTag: @"exdate"] objectEnumerator]; 223 224 while ((dateString = [dateList nextObject])) 225 { 226 exDates = [(iCalDateTime*) dateString dateTimes]; 227 for (i = 0; i < [exDates count]; i++) 228 { 229 exDate = [exDates objectAtIndex: i]; 230 dateString = [NSString stringWithFormat: @"%@Z", 231 [exDate iCalFormattedDateTimeString]]; 232 [dates addObject: dateString]; 233 } 234 } 235 236 return dates; 237} 238 239/** 240 * Returns the exception dates for the entity, but adjusted to the entity timezone. 241 * Used when calculating a recurrence rule. 242 * @param theTimeZone the timezone of the entity. 243 * @see [iCalTimeZone computedDatesForStrings:] 244 * @return the exception dates, adjusted to the timezone. 245 */ 246- (NSArray *) exceptionDatesWithTimeZone: (id) theTimeZone 247{ 248 NSArray *dates, *exDates; 249 NSEnumerator *dateList; 250 NSCalendarDate *exDate; 251 NSString *dateString; 252 int offset; 253 unsigned i; 254 255 if (theTimeZone) 256 { 257 dates = [NSMutableArray array]; 258 dateList = [[self childrenWithTag: @"exdate"] objectEnumerator]; 259 260 while ((dateString = [dateList nextObject])) 261 { 262 exDates = [(iCalDateTime*) dateString dateTimes]; 263 for (i = 0; i < [exDates count]; i++) 264 { 265 exDate = [exDates objectAtIndex: i]; 266 267 // Example: timezone is -0400, date is 2012-05-24 (00:00:00 +0000), 268 // and changes to 2012-05-24 04:00:00 +0000 269 if ([theTimeZone isKindOfClass: [iCalTimeZone class]]) 270 { 271 exDate = [(iCalTimeZone *) theTimeZone computedDateForDate: exDate]; 272 } 273 else 274 { 275 offset = [(NSTimeZone *) theTimeZone secondsFromGMTForDate: exDate]; 276 exDate = (NSCalendarDate *) [exDate dateByAddingYears:0 months:0 days:0 hours:0 minutes:0 277 seconds:-offset]; 278 } 279 [(NSMutableArray *) dates addObject: exDate]; 280 } 281 } 282 } 283 else 284 dates = [self exceptionDates]; 285 286 return dates; 287} 288 289/* Convenience */ 290 291- (BOOL) isRecurrent 292{ 293 return [self hasRecurrenceRules]; 294} 295 296/* Matching */ 297 298- (BOOL) isWithinCalendarDateRange: (NGCalendarDateRange *) _range 299 firstInstanceCalendarDateRange: (NGCalendarDateRange *) _fir 300{ 301 NSArray *ranges; 302 303 ranges = [self recurrenceRangesWithinCalendarDateRange:_range 304 firstInstanceCalendarDateRange:_fir]; 305 return [ranges count] > 0; 306} 307 308- (NSArray *) recurrenceRangesWithinCalendarDateRange: (NGCalendarDateRange *)_r 309 firstInstanceCalendarDateRange: (NGCalendarDateRange *)_fir 310{ 311 return [iCalRecurrenceCalculator recurrenceRangesWithinCalendarDateRange: _r 312 firstInstanceCalendarDateRange: _fir 313 recurrenceRules: [self recurrenceRules] 314 exceptionRules: [self exceptionRules] 315 exceptionDates: [self exceptionDates]]; 316} 317 318 319/* this is the outmost bound possible, not necessarily the real last date */ 320- (NSCalendarDate *) 321lastPossibleRecurrenceStartDateUsingFirstInstanceCalendarDateRange: (NGCalendarDateRange *)_r 322{ 323 NSCalendarDate *date; 324 NSEnumerator *rRules; 325 iCalRecurrenceRule *rule; 326 iCalRecurrenceCalculator *calc; 327 NSCalendarDate *rdate; 328 329 date = nil; 330 331 rRules = [[self recurrenceRules] objectEnumerator]; 332 rule = [rRules nextObject]; 333 while (rule && ![rule isInfinite] && !date) 334 { 335 calc = [iCalRecurrenceCalculator 336 recurrenceCalculatorForRecurrenceRule: rule 337 withFirstInstanceCalendarDateRange: _r]; 338 rdate = [[calc lastInstanceCalendarDateRange] startDate]; 339 if (!rdate) 340 date = [_r startDate]; 341 else if (!date || ([date compare: rdate] == NSOrderedAscending)) 342 date = rdate; 343 else 344 rule = [rRules nextObject]; 345 } 346 347 return date; 348} 349 350- (NSCalendarDate *) firstRecurrenceStartDateWithEndDate: (NSCalendarDate *) endDate 351{ 352 NSCalendarDate *startDate, *firstOccurrenceStartDate, *endOfFirstRange; 353 NGCalendarDateRange *range, *firstInstanceRange; 354 iCalRecurrenceFrequency frequency; 355 iCalRecurrenceRule *rule; 356 NSArray *rules, *recurrences; 357 uint32_t units; 358 359 firstOccurrenceStartDate = nil; 360 361 rules = [self recurrenceRules]; 362 if ([rules count] > 0) 363 { 364 rule = [rules objectAtIndex: 0]; 365 frequency = [rule frequency]; 366 units = [rule repeatInterval]; 367 368 startDate = [self startDate]; 369 switch (frequency) 370 { 371 /* second-based units */ 372 case iCalRecurrenceFrequenceWeekly: 373 units *= 7; 374 case iCalRecurrenceFrequenceDaily: 375 units *= 24; 376 case iCalRecurrenceFrequenceHourly: 377 units *= 60; 378 case iCalRecurrenceFrequenceMinutely: 379 units *= 60; 380 case iCalRecurrenceFrequenceSecondly: 381 endOfFirstRange = [startDate dateByAddingYears: 0 months: 0 days: 0 382 hours: 0 minutes: 0 383 seconds: units]; 384 break; 385 386 /* month-based units */ 387 case iCalRecurrenceFrequenceYearly: 388 units *= 12; 389 case iCalRecurrenceFrequenceMonthly: 390 endOfFirstRange = [startDate dateByAddingYears: 0 months: (units + 1) 391 days: 0 392 hours: 0 minutes: 0 393 seconds: 0]; 394 break; 395 396 default: 397 endOfFirstRange = nil; 398 } 399 400 if (endOfFirstRange) 401 { 402 range = [NGCalendarDateRange calendarDateRangeWithStartDate: startDate 403 endDate: endOfFirstRange]; 404 firstInstanceRange = [NGCalendarDateRange calendarDateRangeWithStartDate: startDate 405 endDate: endDate]; 406 recurrences = [iCalRecurrenceCalculator recurrenceRangesWithinCalendarDateRange: range 407 firstInstanceCalendarDateRange: firstInstanceRange 408 recurrenceRules: rules 409 exceptionRules: nil 410 exceptionDates: nil]; 411 if ([recurrences count] > 0) 412 firstOccurrenceStartDate = [[recurrences objectAtIndex: 0] 413 startDate]; 414 } 415 } 416 417 return firstOccurrenceStartDate; 418} 419 420- (NSCalendarDate *) lastPossibleRecurrenceStartDate 421{ 422 [self subclassResponsibility: _cmd]; 423 424 return nil; 425} 426 427@end 428