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