Repository: pimutils/khal
Pull request #520 (2016-10-10 08:53:39)

Fix #489 for v0.7.x


Patch:
diff --git a/khal/aux.py b/khal/aux.py
index ff03d2e..6fe9862 100644
--- a/khal/aux.py
+++ b/khal/aux.py
@@ -25,8 +25,9 @@ strings to date(time) or event objects"""
 
 from .compat import to_unicode
 
-from datetime import date, datetime, timedelta
 from datetime import time as dtime
+from datetime import date, datetime, timedelta
+from collections import defaultdict
 import random
 import string
 
@@ -393,19 +394,85 @@ def new_event(dtstart=None, dtend=None, summary=None, timezone=None,
     return event
 
 
-def ics_from_list(vevent, random_uid=False):
-    """convert an iterable of icalendar.Event to an icalendar.Calendar
+def split_ics(ics, random_uid=False):
+    """split an ics string into several according to VEVENT's UIDs
+
+    and sort the right VTIMEZONEs accordingly
+    ignores all other ics components
+    :type ics: str
+    :param random_uid: assign random uids to all events
+    :type random_uid: bool
+    :rtype list:
+    """
+    cal = icalendar.Calendar.from_ical(ics)
+    tzs = {item['TZID']: item for item in cal.walk() if item.name == 'VTIMEZONE'}
+
+    events_grouped = defaultdict(list)
+    for item in cal.walk():
+        if item.name == 'VEVENT':
+            events_grouped[item['UID']].append(item)
+        else:
+            continue
+    return [ics_from_list(events, tzs, random_uid) for uid, events in
+            sorted(events_grouped.items())]
+
+
+def ics_from_list(events, tzs, random_uid=False):
+    """convert an iterable of icalendar.Events to an icalendar.Calendar
 
-    :param random_uid: asign the same random UID to all events
+    :params events: list of events all with the same uid
+    :type events: list(icalendar.cal.Event)
+    :param random_uid: assign random uids to all events
     :type random_uid: bool
+    :param tzs: collection of timezones
+    :type tzs: dict(icalendar.cal.Vtimzone
     """
     calendar = icalendar.Calendar()
     calendar.add('version', '2.0')
     calendar.add('prodid', '-//CALENDARSERVER.ORG//NONSGML Version 1//EN')
+
     if random_uid:
-        new_uid = icalendar.vText(generate_random_uid())
-    for sub_event in vevent:
+        new_uid = generate_random_uid()
+
+    needed_tz, missing_tz = set(), set()
+    for sub_event in events:
         if random_uid:
-            sub_event['uid'] = new_uid
+            sub_event['UID'] = new_uid
+        # icalendar round-trip converts `TZID=a b` to `TZID="a b"` investigate, file bug XXX
+        for prop in ['DTSTART', 'DTEND', 'DUE', 'EXDATE', 'RDATE', 'RECURRENCE-ID', 'DUE']:
+            if isinstance(sub_event.get(prop), list):
+                items = sub_event.get(prop)
+            else:
+                items = [sub_event.get(prop)]
+
+            for item in items:
+                if not (hasattr(item, 'dt') or hasattr(item, 'dts')):
+                    continue
+                # if prop is a list, all items have the same parameters
+                datetime_ = item.dts[0].dt if hasattr(item, 'dts') else item.dt
+
+                if not hasattr(datetime_, 'tzinfo'):
+                    continue
+
+                # check for datetimes' timezones which are not understood by
+                # icalendar
+                if datetime_.tzinfo is None and 'TZID' in item.params and \
+                        item.params['TZID'] not in missing_tz:
+                    logger.warn(
+                        'Cannot find timezone `{}` in .ics file, using default timezone. '
+                        'This can lead to erroneous time shifts'.format(item.params['TZID'])
+                    )
+                    missing_tz.add(item.params['TZID'])
+                elif datetime_.tzinfo != pytz.UTC:
+                    needed_tz.add(datetime_.tzinfo)
+
+    for tzid in needed_tz:
+        if str(tzid) in tzs:
+            calendar.add_component(tzs[str(tzid)])
+        else:
+            logger.warn(
+                'Cannot find timezone `{}` in .ics file, this could be a bug, '
+                'please report this issue at http://github.com/pimutils/khal/.'.format(tzid))
+    for sub_event in events:
         calendar.add_component(sub_event)
-    return calendar
+    return calendar.to_ical().decode('utf-8')
diff --git a/khal/calendar_display.py b/khal/calendar_display.py
index 4442648..8d7f05a 100644
--- a/khal/calendar_display.py
+++ b/khal/calendar_display.py
@@ -76,6 +76,7 @@ def get_event_color(event, default_color):
 def str_highlight_day(day, devents, hmethod, default_color, multiple, color):
     """returns a string with day highlighted according to configuration
     """
+    dcolor = ''
     dstr = str(day.day).rjust(2)
     if color == '':
         dcolors = list(set(map(lambda x: get_event_color(x, default_color), devents)))
diff --git a/khal/controllers.py b/khal/controllers.py
index 9a91fd3..a633d78 100644
--- a/khal/controllers.py
+++ b/khal/controllers.py
@@ -25,8 +25,6 @@ import icalendar
 from click import confirm, echo, style, prompt
 from vdirsyncer.utils.vobject import Item
 
-from collections import defaultdict
-
 import datetime
 import itertools
 import logging
@@ -38,7 +36,6 @@ from khal.compat import to_unicode
 from khal.khalendar.exceptions import ReadOnlyCalendarError, DuplicateUid
 from khal.exceptions import InvalidDate, FatalError
 from khal.khalendar.event import Event
-from khal.khalendar.backend import sort_key
 from khal import __version__, __productname__
 from khal.log import logger
 from .terminal import colored, get_terminal_size, merge_columns
@@ -219,63 +216,55 @@ def import_ics(collection, conf, ics, batch=False, random_uid=False):
                   even when an event with the same uid already exists
     :type batch: bool
     """
-    cal = icalendar.Calendar.from_ical(ics)
-    events = [item for item in cal.walk() if item.name == 'VEVENT']
-    events_grouped = defaultdict(list)
-    for event in events:
-        events_grouped[event['UID']].append(event)
-
-    vevents = list()
-    for uid in events_grouped:
-        vevents.append(sorted(events_grouped[uid], key=sort_key))
+    vevents = aux.split_ics(ics, random_uid)
     for vevent in vevents:
-        import_event(vevent, collection, conf['locale'], batch, random_uid)
+        import_event(vevent, collection, conf['locale'], batch)
 
 
-def import_event(vevent, collection, locale, batch, random_uid):
-    """import one event into collection, let user choose the collection"""
+def import_event(vevent, collection, locale, batch, format=None, env=None):
+    """import one event into collection, let user choose the collection
 
+    :type vevent: list of vevents, which can be more than one VEVENT, i.e., the
+        same UID, i.e., one "master" event and (optionally) 1+ RECURRENCE-ID events
+    :type vevent: list(str)
+    """
     # print all sub-events
-    for sub_event in vevent:
-        if not batch:
-            event = Event.fromVEvents(
-                [sub_event], calendar=collection.default_calendar_name, locale=locale)
-            echo(event.event_description)
+    if not batch:
+        for item in icalendar.Calendar.from_ical(vevent).walk():
+            if item.name == 'VEVENT':
+                event = Event.fromVEvents(
+                    [item], calendar=collection.default_calendar_name, locale=locale)
+                echo(event.event_description)
 
     # get the calendar to insert into
     if batch or len(collection.writable_names) == 1:
         calendar_name = collection.writable_names[0]
     else:
-        choice = list()
-        for num, name in enumerate(collection.writable_names):
-            choice.append(u'{}({})'.format(name, num))
-        choice = u', '.join(choice)
+        calendar_names = sorted(collection.writable_names)
+        choices = ', '.join(
+            ['{}({})'.format(name, num) for num, name in enumerate(calendar_names)])
         while True:
-            value = prompt(u'Which calendar do you want to import to? \n'
-                           u'{}'.format(choice), default=collection.default_calendar_name)
+            value = prompt(
+                u"Which calendar do you want to import to? (unique prefixes are fine)\n"
+                u"{}".format(choices),
+                default=collection.default_calendar_name,
+            )
             try:
-                number = int(value)
-                calendar_name = collection.writable_names[number]
+                calendar_name = calendar_names[int(value)]
                 break
             except (ValueError, IndexError):
-                matches = filter(lambda x: x.startswith(value), collection.writable_names)
+                matches = [x for x in collection.writable_names if x.startswith(value)]
                 if len(matches) == 1:
                     calendar_name = matches[0]
                     break
             echo(u'invalid choice')
 
-    if batch or confirm(u"Do you want to import this event into `{}`?"
-                        u"".format(calendar_name)):
-        ics = aux.ics_from_list(vevent, random_uid)
+    if batch or confirm(u"Do you want to import this event into `{}`?".format(calendar_name)):
         try:
-            collection.new(
-                Item(ics.to_ical().decode('utf-8')),
-                collection=calendar_name)
+            collection.new(Item(vevent), collection=calendar_name)
         except DuplicateUid:
-            if batch or confirm(u"An event with the same UID already exists. "
-                                u"Do you want to update it?"):
-                collection.force_update(
-                    Item(ics.to_ical().decode('utf-8')),
-                    collection=calendar_name)
+            if batch or confirm(
+                    u"An event with the same UID already exists. Do you want to update it?"):
+                collection.force_update(Item(vevent), collection=calendar_name)
             else:
                 logger.warn(u"Not importing event with UID `{}`".format(event.uid))
diff --git a/khal/khalendar/backend.py b/khal/khalendar/backend.py
index 3a85c87..954d961 100644
--- a/khal/khalendar/backend.py
+++ b/khal/khalendar/backend.py
@@ -527,7 +527,7 @@ def check_support(vevent, href, calendar):
             .format(href, calendar)
         )
     rdate = vevent.get('RDATE')
-    if rdate is not None and rdate.params.get('VALUE') == 'PERIOD':
+    if rdate is not None and hasattr(rdate, 'params') and rdate.params.get('VALUE') == 'PERIOD':
         raise UpdateFailed(
             u'`RDATE;VALUE=PERIOD` is currently not supported by khal. '
             u'Therefore event {} from calendar {} will not be shown in khal.\n'
diff --git a/setup.py b/setup.py
index f05cdba..7e2f654 100755
--- a/setup.py
+++ b/setup.py
@@ -9,7 +9,7 @@ requirements = [
     'urwid',
     'pyxdg',
     'pytz',
-    'vdirsyncer',
+    'vdirsyncer<0.13',
     'python-dateutil',
     'configobj',
     'tzlocal>=1.0',


Test patch:
diff --git a/tests/aux_test.py b/tests/aux_test.py
index 409a6ef..37a1414 100644
--- a/tests/aux_test.py
+++ b/tests/aux_test.py
@@ -2,6 +2,8 @@
 """testing functions from the khal.aux"""
 from datetime import date, datetime, time, timedelta
 import textwrap
+import random
+import sys
 
 import icalendar
 import pytz
@@ -10,8 +12,7 @@ from khal.aux import construct_event, guessdatetimefstr
 from khal import aux
 from khal.compat import to_bytes
 
-from .aux import _get_all_vevents_file, _get_text, \
-    normalize_component
+from .aux import _get_text, normalize_component
 
 
 def _now():
@@ -49,6 +50,11 @@ def _replace_uid(event):
     return event
 
 
+def _get_TZIDs(lines):
+    """from a list of strings, get all unique strings that start with TZID"""
+    return sorted((line for line in lines if line.startswith('TZID')))
+
+
 def test_normalize_component():
     assert normalize_component(textwrap.dedent("""
     BEGIN:VEVENT
@@ -342,21 +348,56 @@ def test_description_and_location():
         assert _replace_uid(event).to_ical() == vevent
 
 
-class TestIcsFromList(object):
-
-    def test_ics_from_list(self):
-        vevents = _get_all_vevents_file('event_rrule_recuid')
-        cal = aux.ics_from_list(list(vevents))
-        assert normalize_component(cal.to_ical()) == \
-            normalize_component(_get_text('event_rrule_recuid'))
-
-    def test_ics_from_list_random_uid(self):
-        vevents = _get_all_vevents_file('event_rrule_recuid')
-        cal = aux.ics_from_list(list(vevents), random_uid=True)
-        normalize_component(cal.to_ical())
-        vevents = [item for item in cal.walk() if item.name == 'VEVENT']
-        uids = set()
-        for event in vevents:
-            uids.add(event['UID'])
-        assert len(uids) == 1
-        assert event['UID'] != icalendar.vText('event_rrule_recurrence_id')
+def test_split_ics():
+    cal = _get_text('cal_lots_of_timezones')
+    vevents = aux.split_ics(cal)
+
+    vevents0 = vevents[0].split('\r\n')
+    vevents1 = vevents[1].split('\r\n')
+
+    part0 = _get_text('part0').split('\n')
+    part1 = _get_text('part1').split('\n')
+
+    assert _get_TZIDs(vevents0) == _get_TZIDs(part0)
+    assert _get_TZIDs(vevents1) == _get_TZIDs(part1)
+
+    assert sorted(vevents0) == sorted(part0)
+    assert sorted(vevents1) == sorted(part1)
+
+
+def test_split_ics_random_uid():
+    random.seed(123)
+    cal = _get_text('cal_lots_of_timezones')
+    vevents = aux.split_ics(cal, random_uid=True)
+
+    part0 = _get_text('part0').split('\n')
+    part1 = _get_text('part1').split('\n')
+
+    if sys.version < (3, 0, 0):
+        for item in icalendar.Calendar.from_ical(vevents[0]).walk():
+            if item.name == 'VEVENT':
+                assert item['UID'] == 'BDOD6BTL4FMMIAPDVCLQ6DF2A6UJ41M2HVS3'
+        for item in icalendar.Calendar.from_ical(vevents[1]).walk():
+            if item.name == 'VEVENT':
+                assert item['UID'] == 'LO1SYWX6RYNB1G36XGMOCQUMGDWAMIT06W98'
+    else:
+        for item in icalendar.Calendar.from_ical(vevents[0]).walk():
+            if item.name == 'VEVENT':
+                assert item['UID'] == 'DRF0RGCY89VVDKIV9VPKA1FYEAU2GCFJIBS1'
+        for item in icalendar.Calendar.from_ical(vevents[1]).walk():
+            if item.name == 'VEVENT':
+                assert item['UID'] == '4Q4CTV74N7UAZ618570X6CLF5QKVV9ZE3YVB'
+
+    # after replacing the UIDs, everything should be as above
+    if sys.version < (3, 0, 0):
+        vevents0 = vevents[0].replace('BDOD6BTL4FMMIAPDVCLQ6DF2A6UJ41M2HVS3', '123').split('\r\n')
+        vevents1 = vevents[1].replace('LO1SYWX6RYNB1G36XGMOCQUMGDWAMIT06W98', 'abcde').split('\r\n')
+    else:
+        vevents0 = vevents[0].replace('DRF0RGCY89VVDKIV9VPKA1FYEAU2GCFJIBS1', '123').split('\r\n')
+        vevents1 = vevents[1].replace('4Q4CTV74N7UAZ618570X6CLF5QKVV9ZE3YVB', 'abcde').split('\r\n')
+
+    assert _get_TZIDs(vevents0) == _get_TZIDs(part0)
+    assert _get_TZIDs(vevents1) == _get_TZIDs(part1)
+
+    assert sorted(vevents0) == sorted(part0)
+    assert sorted(vevents1) == sorted(part1)
diff --git a/tests/backend_test.py b/tests/backend_test.py
index 9d11765..0ae296d 100644
--- a/tests/backend_test.py
+++ b/tests/backend_test.py
@@ -569,6 +569,13 @@ def test_check_support():
         [backend.check_support(event, '', '') for event in ical.walk()]
 
 
+def test_check_support_rdate_no_values():
+    """check if `check_support` doesn't choke on events with an RDATE property
+    without a VALUE parameter"""
+    ical = icalendar.Calendar.from_ical(_get_text('event_rdate_no_value'))
+    [backend.check_support(event, '', '') for event in ical.walk()]
+
+
 card = """BEGIN:VCARD
 VERSION:3.0
 FN:Unix
diff --git a/tests/ics/cal_dt_two_tz.ics b/tests/ics/cal_dt_two_tz.ics
index 820bb0b..b27dbe4 100644
--- a/tests/ics/cal_dt_two_tz.ics
+++ b/tests/ics/cal_dt_two_tz.ics
@@ -8,7 +8,7 @@ DTSTART;VALUE=DATE-TIME:20141026T020000
 TZNAME:CET
 TZOFFSETFROM:+0200
 TZOFFSETTO:+0100
-RDATE:20151025T020000
+RDATE:20151025T020000,20161030T020000
 END:STANDARD
 BEGIN:DAYLIGHT
 DTSTART;VALUE=DATE-TIME:20140330T030000
@@ -22,7 +22,7 @@ BEGIN:VTIMEZONE
 TZID:America/New_York
 BEGIN:STANDARD
 DTSTART;VALUE=DATE-TIME:20141102T010000
-RDATE:20151101T010000
+RDATE:20151101T010000,20161106T010000
 TZNAME:EST
 TZOFFSETFROM:-0400
 TZOFFSETTO:-0500
diff --git a/tests/ics/cal_lots_of_timezones.ics b/tests/ics/cal_lots_of_timezones.ics
new file mode 100644
index 0000000..e175f04
--- /dev/null
+++ b/tests/ics/cal_lots_of_timezones.ics
@@ -0,0 +1,128 @@
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VTIMEZONE
+TZID:IndianReunion
+BEGIN:STANDARD
+TZOFFSETFROM:+034152
+TZOFFSETTO:+0400
+TZNAME:RET
+DTSTART:19110601T000000
+RDATE:19110601T000000
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VTIMEZONE
+TZID:Will_not_appear
+BEGIN:STANDARD
+TZOFFSETFROM:+034152
+TZOFFSETTO:+0400
+TZNAME:RET
+DTSTART:19110601T000000
+RDATE:19110601T000000
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VTIMEZONE
+TZID:Europe_Amsterdam
+BEGIN:DAYLIGHT
+TZOFFSETFROM:+0100
+TZOFFSETTO:+0200
+TZNAME:CEST
+DTSTART:19810329T020000
+RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
+END:DAYLIGHT
+BEGIN:STANDARD
+TZOFFSETFROM:+0200
+TZOFFSETTO:+0100
+TZNAME:CET
+DTSTART:19961027T030000
+RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VTIMEZONE
+TZID:Europe_Berlin
+BEGIN:STANDARD
+DTSTART;VALUE=DATE-TIME:20141026T020000
+TZNAME:CET
+TZOFFSETFROM:+0200
+TZOFFSETTO:+0100
+RDATE:20151025T020000
+END:STANDARD
+BEGIN:DAYLIGHT
+DTSTART;VALUE=DATE-TIME:20140330T030000
+RDATE:20150329T030000,20160327T030000
+TZNAME:CEST
+TZOFFSETFROM:+0100
+TZOFFSETTO:+0200
+END:DAYLIGHT
+END:VTIMEZONE
+BEGIN:VTIMEZONE
+TZID:America_New_York
+BEGIN:STANDARD
+DTSTART;VALUE=DATE-TIME:20141102T010000
+RDATE:20151101T010000
+TZNAME:EST
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+END:STANDARD
+BEGIN:DAYLIGHT
+DTSTART;VALUE=DATE-TIME:20140309T030000
+RDATE:20150308T030000,20160313T030000
+TZNAME:EDT
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+END:DAYLIGHT
+END:VTIMEZONE
+BEGIN:VTIMEZONE
+TZID:America_Bogota
+BEGIN:STANDARD
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+TZNAME:COT
+DTSTART:19930404T000000
+RDATE:19930404T000000
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VTIMEZONE
+TZID:Europe_London
+BEGIN:DAYLIGHT
+TZOFFSETFROM:+0000
+TZOFFSETTO:+0100
+TZNAME:BST
+DTSTART:19810329T010000
+RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
+END:DAYLIGHT
+BEGIN:STANDARD
+TZOFFSETFROM:+0100
+TZOFFSETTO:+0000
+TZNAME:GMT
+DTSTART:19961027T020000
+RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VEVENT
+SUMMARY:An Event
+DTSTART;TZID=Europe_Berlin;VALUE=DATE-TIME:20140409T093000
+DTEND;TZID=America_New_York;VALUE=DATE-TIME:20140409T103000
+RDATE;TZID=America_Bogota:20140411T113000,20140413T113000
+RDATE;TZID=America_Bogota:20140415T113000
+RDATE;TZID=IndianReunion:20140418T113000
+RRULE:FREQ=MONTHLY;COUNT=6
+DTSTAMP;VALUE=DATE-TIME:20140401T234817Z
+UID:abcde
+END:VEVENT
+BEGIN:VEVENT
+SUMMARY:An Event
+DTSTART;TZID=Europe_London;VALUE=DATE-TIME:20140509T193000
+DTEND;TZID=Europe_London;VALUE=DATE-TIME:20140509T203000
+DTSTAMP;VALUE=DATE-TIME:20140401T234817Z
+UID:123
+END:VEVENT
+BEGIN:VEVENT
+SUMMARY:An Updated Event
+DTSTART;TZID=Europe_Berlin;VALUE=DATE-TIME:20140409T093000
+DTEND;TZID=America_New_York;VALUE=DATE-TIME:20140409T103000
+DTSTAMP;VALUE=DATE-TIME:20140401T234817Z
+UID:abcde
+RECURRENCE-ID;TZID=Europe_Amsterdam:20140707T070000
+END:VEVENT
+END:VCALENDAR
diff --git a/tests/ics/event_dt_simple_inkl_vtimezone.ics b/tests/ics/event_dt_simple_inkl_vtimezone.ics
index a6eb931..a925241 100644
--- a/tests/ics/event_dt_simple_inkl_vtimezone.ics
+++ b/tests/ics/event_dt_simple_inkl_vtimezone.ics
@@ -4,7 +4,7 @@ PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
 BEGIN:VTIMEZONE
 TZID:Europe/Berlin
 BEGIN:STANDARD
-RDATE:20151025T020000
+RDATE:20151025T020000,20161030T020000
 DTSTART;VALUE=DATE-TIME:20141026T020000
 TZNAME:CET
 TZOFFSETFROM:+0200
diff --git a/tests/ics/event_rdate_no_value.ics b/tests/ics/event_rdate_no_value.ics
new file mode 100644
index 0000000..fed4415
--- /dev/null
+++ b/tests/ics/event_rdate_no_value.ics
@@ -0,0 +1,13 @@
+BEGIN:VCALENDAR
+VERSION:2.0
+BEGIN:VEVENT
+LOCATION:Mumble channel
+RDATE;TZID=Europe/Berlin:20150831T113000
+RDATE;TZID=Europe/Berlin:20150914T113000
+RRULE:FREQ=WEEKLY;UNTIL=20161231;INTERVAL=2;BYDAY=MO
+DTSTART;TZID=Europe/Berlin:20160125T113000
+DTEND;TZID=Europe/Berlin:20160125T123000
+UID:rdatenovalue
+SUMMARY:An event with rdates which have no VALUE
+END:VEVENT
+END:VCALENDAR
diff --git a/tests/ics/part0.ics b/tests/ics/part0.ics
new file mode 100644
index 0000000..b919d48
--- /dev/null
+++ b/tests/ics/part0.ics
@@ -0,0 +1,28 @@
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VTIMEZONE
+TZID:Europe_London
+BEGIN:DAYLIGHT
+TZOFFSETFROM:+0000
+TZOFFSETTO:+0100
+TZNAME:BST
+DTSTART:19810329T010000
+RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3
+END:DAYLIGHT
+BEGIN:STANDARD
+TZOFFSETFROM:+0100
+TZOFFSETTO:+0000
+TZNAME:GMT
+DTSTART:19961027T020000
+RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VEVENT
+SUMMARY:An Event
+DTSTART;TZID=Europe_London;VALUE=DATE-TIME:20140509T193000
+DTEND;TZID=Europe_London;VALUE=DATE-TIME:20140509T203000
+DTSTAMP;VALUE=DATE-TIME:20140401T234817Z
+UID:123
+END:VEVENT
+END:VCALENDAR
diff --git a/tests/ics/part1.ics b/tests/ics/part1.ics
new file mode 100644
index 0000000..1a449b0
--- /dev/null
+++ b/tests/ics/part1.ics
@@ -0,0 +1,94 @@
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VTIMEZONE
+TZID:IndianReunion
+BEGIN:STANDARD
+TZOFFSETFROM:+034152
+TZOFFSETTO:+0400
+TZNAME:RET
+DTSTART:19110601T000000
+RDATE:19110601T000000
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VTIMEZONE
+TZID:Europe_Amsterdam
+BEGIN:DAYLIGHT
+TZOFFSETFROM:+0100
+TZOFFSETTO:+0200
+TZNAME:CEST
+DTSTART:19810329T020000
+RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3
+END:DAYLIGHT
+BEGIN:STANDARD
+TZOFFSETFROM:+0200
+TZOFFSETTO:+0100
+TZNAME:CET
+DTSTART:19961027T030000
+RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VTIMEZONE
+TZID:Europe_Berlin
+BEGIN:STANDARD
+DTSTART;VALUE=DATE-TIME:20141026T020000
+TZNAME:CET
+TZOFFSETFROM:+0200
+TZOFFSETTO:+0100
+RDATE:20151025T020000
+END:STANDARD
+BEGIN:DAYLIGHT
+DTSTART;VALUE=DATE-TIME:20140330T030000
+RDATE:20150329T030000,20160327T030000
+TZNAME:CEST
+TZOFFSETFROM:+0100
+TZOFFSETTO:+0200
+END:DAYLIGHT
+END:VTIMEZONE
+BEGIN:VTIMEZONE
+TZID:America_New_York
+BEGIN:STANDARD
+DTSTART;VALUE=DATE-TIME:20141102T010000
+RDATE:20151101T010000
+TZNAME:EST
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+END:STANDARD
+BEGIN:DAYLIGHT
+DTSTART;VALUE=DATE-TIME:20140309T030000
+RDATE:20150308T030000,20160313T030000
+TZNAME:EDT
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+END:DAYLIGHT
+END:VTIMEZONE
+BEGIN:VTIMEZONE
+TZID:America_Bogota
+BEGIN:STANDARD
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+TZNAME:COT
+DTSTART:19930404T000000
+RDATE:19930404T000000
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VEVENT
+SUMMARY:An Event
+DTSTART;TZID=Europe_Berlin;VALUE=DATE-TIME:20140409T093000
+DTEND;TZID=America_New_York;VALUE=DATE-TIME:20140409T103000
+RDATE;TZID=IndianReunion:20140418T113000
+RDATE;TZID=America_Bogota:20140411T113000,20140413T113000
+RDATE;TZID=America_Bogota:20140415T113000
+RRULE:FREQ=MONTHLY;COUNT=6
+DTSTAMP;VALUE=DATE-TIME:20140401T234817Z
+UID:abcde
+END:VEVENT
+BEGIN:VEVENT
+SUMMARY:An Updated Event
+DTSTART;TZID=Europe_Berlin;VALUE=DATE-TIME:20140409T093000
+DTEND;TZID=America_New_York;VALUE=DATE-TIME:20140409T103000
+DTSTAMP;VALUE=DATE-TIME:20140401T234817Z
+UID:abcde
+RECURRENCE-ID;TZID=Europe_Amsterdam:20140707T070000
+END:VEVENT
+END:VCALENDAR