1/* iCalTimeZonePeriod.m - this file is part of SOPE 2 * 3 * Copyright (C) 2006-2014 Inverse inc. 4 * 5 * This file is free software; you can redistribute it and/or modify 6 * it under the terms of the GNU General Public License as published by 7 * the Free Software Foundation; either version 2, or (at your option) 8 * any later version. 9 * 10 * This file is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License 16 * along with this program; see the file COPYING. If not, write to 17 * the Free Software Foundation, Inc., 59 Temple Place - Suite 330, 18 * Boston, MA 02111-1307, USA. 19 */ 20 21#import <Foundation/NSArray.h> 22#import <Foundation/NSCalendarDate.h> 23#import <Foundation/NSString.h> 24#import <Foundation/NSTimeZone.h> 25 26#import "iCalDateTime.h" 27#import "iCalByDayMask.h" 28#import "iCalUTCOffset.h" 29 30#import "iCalTimeZonePeriod.h" 31 32@implementation iCalTimeZonePeriod 33 34- (Class) classForTag: (NSString *) classTag 35{ 36 Class tagClass; 37 38 if ([classTag isEqualToString: @"RRULE"]) 39 tagClass = [iCalRecurrenceRule class]; 40 else if ([classTag isEqualToString: @"RDATE"]) 41 tagClass = [iCalDateTime class]; 42 else if ([classTag isEqualToString: @"DTSTART"]) 43 tagClass = [iCalDateTime class]; 44 else if ([classTag isEqualToString: @"TZOFFSETFROM"] 45 || [classTag isEqualToString: @"TZOFFSETTO"]) 46 tagClass = [iCalUTCOffset class]; 47 else if ([classTag isEqualToString: @"TZNAME"]) 48 tagClass = [CardElement class]; 49 else 50 tagClass = [super classForTag: classTag]; 51 52 return tagClass; 53} 54 55- (int) _secondsOfOffset: (NSString *) offsetName 56{ 57 NSString *offsetTo; 58 BOOL negative; 59 NSRange cursor; 60 unsigned int length; 61 unsigned int seconds; 62 63 seconds = 0; 64 65 offsetTo = [[self uniqueChildWithTag: offsetName] 66 flattenedValuesForKey: @""]; 67 length = [offsetTo length]; 68 69 if (!length) 70 return seconds; 71 72 negative = [offsetTo hasPrefix: @"-"]; 73 if (negative) 74 { 75 length--; 76 cursor = NSMakeRange(1, 2); 77 } 78 else if ([offsetTo hasPrefix: @"+"]) 79 { 80 length--; 81 cursor = NSMakeRange(1, 2); 82 } 83 else 84 cursor = NSMakeRange(0, 2); 85 86 seconds = 3600 * [[offsetTo substringWithRange: cursor] intValue]; 87 cursor.location += 2; 88 seconds += 60 * [[offsetTo substringWithRange: cursor] intValue]; 89 if (length == 6) 90 { 91 cursor.location += 2; 92 seconds += [[offsetTo substringWithRange: cursor] intValue]; 93 } 94 95 return ((negative) ? -seconds : seconds); 96} 97 98// - (unsigned int) dayOfWeekFromRruleDay: (iCalWeekDay) day 99// { 100// unsigned int dayOfWeek; 101 102// dayOfWeek = 0; 103// while (day >> (dayOfWeek + 1)) 104// dayOfWeek++; 105 106// return dayOfWeek; 107// } 108 109- (void) dealloc 110{ 111 [startDate release]; 112 [super dealloc]; 113} 114 115- (NSCalendarDate *) startDate 116{ 117 if (!startDate) 118 { 119 startDate = [(iCalDateTime *) [self uniqueChildWithTag: @"dtstart"] 120 dateTime]; 121 [startDate retain]; 122 } 123 return startDate; 124} 125 126- (iCalRecurrenceRule *) recurrenceRule 127{ 128 return (iCalRecurrenceRule *) [self firstChildWithTag: @"rrule"]; 129} 130 131/** 132 * This method returns the date corresponding for to the start of the period 133 * in the year of the reference date. 134 * We assume that a RRULE for a timezone will always be YEARLY with a BYMONTH 135 * and a BYDAY rule. 136 */ 137- (NSCalendarDate *) _occurrenceForDate: (NSCalendarDate *) refDate 138 byRRule: (iCalRecurrenceRule *) rrule 139{ 140 NSCalendarDate *tmpDate; 141 iCalByDayMask *byDayMask; 142 int dayOfWeek, dateDayOfWeek, offset, pos; 143 NSCalendarDate *tzStart; 144 145 byDayMask = [rrule byDayMask]; 146 dayOfWeek = 0; 147 148 if (byDayMask == nil) 149 { 150 dayOfWeek = 0; 151 pos = 1; 152 } 153 else 154 { 155 dayOfWeek = (int)[byDayMask firstDay]; 156 pos = [byDayMask firstOccurrence]; 157 } 158 159 tzStart = [self startDate]; 160 161 [tzStart setTimeZone: [NSTimeZone timeZoneWithName: @"GMT"]]; 162 tmpDate = [NSCalendarDate dateWithYear: [refDate yearOfCommonEra] 163 month: [[[rrule byMonth] objectAtIndex: 0] intValue] 164 day: 1 165 hour: [tzStart hourOfDay] 166 minute: [tzStart minuteOfHour] second: 0 167 timeZone: [NSTimeZone timeZoneWithName: @"GMT"]]; 168 169 tmpDate = [tmpDate addYear: 0 170 month: ((pos > 0) ? 0 : 1) 171 day: 0 172 hour: 0 173 minute: 0 174 second: 0]; 175 176 /* If the day of the time change is "-XSU", we need to determine whether the 177 first day of next month is in the same week. In practice, as most time 178 changes occurs on sundays, it will be false only when that first day is a 179 sunday, but we want to remain algorithmically exact. */ 180 dateDayOfWeek = [tmpDate dayOfWeek]; 181 if (dateDayOfWeek > dayOfWeek && pos < 0) 182 pos++; 183 184 /* We check if the days of the week are identical. This is important because if they 185 are, "pos" actually includes the first day of tmpDate which means we must decrement 186 pos by 1. This happens for example in the eastern timezone (America/Montreal) 187 in 2015. We have: 188 189 BEGIN:VTIMEZONE 190 TZID:America/Montreal 191 X-LIC-LOCATION:America/Montreal 192 BEGIN:DAYLIGHT 193 TZOFFSETFROM:-0500 194 TZOFFSETTO:-0400 195 TZNAME:EDT 196 DTSTART:19700308T020000 197 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU 198 END:DAYLIGHT 199 BEGIN:STANDARD 200 TZOFFSETFROM:-0400 201 TZOFFSETTO:-0500 202 TZNAME:EST 203 DTSTART:19701101T020000 204 RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU 205 END:STANDARD 206 END:VTIMEZONE 207 208 The time changes occur on a Sunday, but in March, the 1st is a Sunday itself and in November 209 the 1st is also a Sunday. If we don't decrement "pos" by one, tmpDate (which is set to March or November 1st 210 because of "day: 1" will have 14 more days added for March and 7 more days added for November - which will 211 effectively shift the time change by a whole week. 212 213 In Europe/Berlin, we have a different use-case for November. In 2015, November 1st is a Sunday. 214 The time change in November must occur on October 25th but since tmpDate will be November 1st, 215 so a Sunday, dateDayOfWeek will be 0 and dayOfWeek will also be 0 we would decrement tmpDate by 14 days, 216 which is incorrect because it would shift the timezone change one week earlier. We take care about this 217 one with check if pos is greater or equal than 0 and if so, we don't decrement it. 218 219 BEGIN:VCALENDAR 220 PRODID:-//Inverse inc.//NONSGML Olson 2014g//EN 221 VERSION:2.0 222 BEGIN:VTIMEZONE 223 TZID:Europe/Berlin 224 X-LIC-LOCATION:Europe/Berlin 225 BEGIN:DAYLIGHT 226 TZOFFSETFROM:+0100 227 TZOFFSETTO:+0200 228 TZNAME:CEST 229 DTSTART:19700329T020000 230 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU 231 END:DAYLIGHT 232 BEGIN:STANDARD 233 TZOFFSETFROM:+0200 234 TZOFFSETTO:+0100 235 TZNAME:CET 236 DTSTART:19701025T030000 237 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU 238 END:STANDARD 239 END:VTIMEZONE 240 END:VCALENDAR 241 */ 242 if (dayOfWeek == dateDayOfWeek && pos >= 0) 243 pos--; 244 245 offset = (dayOfWeek - dateDayOfWeek) + (pos * 7); 246 tmpDate = [tmpDate addYear: 0 month: 0 day: offset 247 hour: 0 minute: 0 second: 0]; 248 249 return tmpDate; 250} 251 252- (NSCalendarDate *) _occurrenceFromRdate: (NSCalendarDate *) refDate 253 rDates: (NSArray *) rDatesIn; 254{ 255 NSArray *rDates; 256 NSEnumerator *dateList; 257 NSCalendarDate *rDateCur, *rDateOut; 258 NSString *dateString; 259 unsigned i; 260 261 rDateCur = nil; 262 rDateOut = nil; 263 264 dateList = [rDatesIn objectEnumerator]; 265 266 while ((dateString = [dateList nextObject])) 267 { 268 rDates = [(iCalDateTime*) dateString dateTimes]; 269 270 for (i = 0; i < [rDates count]; i++) 271 { 272 rDateCur = [rDates objectAtIndex: i]; 273 if (!rDateOut || ([rDateCur yearOfCommonEra] > [rDateOut yearOfCommonEra] && [refDate yearOfCommonEra] >= [rDateCur yearOfCommonEra])) 274 rDateOut = rDateCur; 275 } 276 } 277 278 return rDateOut; 279} 280 281 282- (NSCalendarDate *) occurrenceForDate: (NSCalendarDate *) refDate; 283{ 284 NSCalendarDate *tmpDate; 285 iCalRecurrenceRule *rrule; 286 NSArray *rDates; 287 288 tmpDate = nil; 289 rrule = (iCalRecurrenceRule *) [self uniqueChildWithTag: @"rrule"]; 290 rDates = (NSArray *) [self childrenWithTag: @"rdate"]; 291 292 if ([rDates count]) 293 { 294 tmpDate = [self _occurrenceFromRdate: refDate rDates: rDates]; 295 return tmpDate; 296 } 297 298 if ([rrule isVoid]) 299 tmpDate 300 = [(iCalDateTime *) [self uniqueChildWithTag: @"dtstart"] dateTime]; 301 else if ([rrule untilDate] == nil || [refDate compare: [rrule untilDate]] == NSOrderedAscending) 302 tmpDate = [self _occurrenceForDate: refDate byRRule: rrule]; 303 else if ([[self _occurrenceForDate: refDate byRRule: rrule] compare: [rrule untilDate] ] == NSOrderedAscending) 304 tmpDate = [rrule untilDate]; 305 306 return tmpDate; 307} 308 309- (int) secondsOffsetFromGMT 310{ 311 return [self _secondsOfOffset: @"tzoffsetto"]; 312} 313 314- (NSComparisonResult) compare: (iCalTimeZonePeriod *) otherPeriod 315{ 316 return [[self startDate] compare: [otherPeriod startDate]]; 317} 318 319@end 320