ics-py / ics-py

Pythonic and easy iCalendar library (rfc5545)

Home Page:http://icspy.readthedocs.org/en/stable/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Virtual time zones (TZID properties) are ignored in DTSTART/DTEND

neuron-whisperer opened this issue · comments

Version of ics.py: 0.7
Version of Python: 3.10.4
OS: MacOS 12.4

I have an .ics that is exported from Outlook. This .ics includes DTSTART and DTEND fields with time zones encoded not as trailing UTC offsets, but as virtual time zones. The .ics file begins with some definitions:

BEGIN:VTIMEZONE
TZID:Pacific Standard Time
BEGIN:STANDARD
DTSTART:16011104T020000
RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11
TZOFFSETFROM:-0700
TZOFFSETTO:-0800
END:STANDARD
BEGIN:DAYLIGHT
DTSTART:16010311T020000
RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3
TZOFFSETFROM:-0800
TZOFFSETTO:-0700
END:DAYLIGHT
END:VTIMEZONE

BEGIN:VTIMEZONE
TZID:Eastern Standard Time
BEGIN:STANDARD
DTSTART:16011104T020000
RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11
TZOFFSETFROM:-0400
TZOFFSETTO:-0500
END:STANDARD
BEGIN:DAYLIGHT
DTSTART:16010311T020000
RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3
TZOFFSETFROM:-0500
TZOFFSETTO:-0400
END:DAYLIGHT
END:VTIMEZONE

...which, I presume, is standard and maybe even template code from Outlook.

The DSTARTs and DTENDs are encoded like this:

DTSTART;TZID="Pacific Standard Time":20210526T130000
DTSTART;TZID="Eastern Standard Time":20211006T110000

When I load this .ics as a Calendar, these events have the following begin dates:

2021-05-26T13:00:00+00:00
2021-10-06T11:00:00+00:00

...that is, the TZID fields are being completely ignored. Since May 26th should be Pacific Daylight Time (UTC-7) and October 6th should also be Eastern Daylight Time (UTC-4), the correctly formatted dates and times would be:

2021-05-26T13:00:00-07:00
2021-10-06T11:00:00-04:00

This problem also applies to DTEND.

Worse, this error is not fixable after loading the calendar, because the TZID part of this field is being discarded - it is not preserved in [event].extra. I presume that since ics.py recognizes the DTSTART and DTEND fields, it proceeds to process the value, but it does not also recognize the TZID part of the field nor account for it in the generated dates and times.

For those looking for a workaround, here is some code to preprocess an .ics and replace all of the DTSTART / DTEND TZINFO fields with UTC-encoded fields.

This code uses datetime, zoneinfo, and re (all of which are Python builtins).

def ics_replace_vtz(ics_text):

  # find all matching DSTART / DTEND fields
  vtz_fields = re.finditer('DT(START|END);TZID=(.*):(\S+)', ics_text)
  for vtz_field in vtz_fields:

    # generate corresponding ZoneInfo object
    timezone_lookup = {'"Pacific Standard Time"':'PST8PDT', '"Mountain Standard Time"':'MST7MDT', '"Central Standard Time"':'CST6CDT', '"Eastern Standard Time"':'EST5EDT'}
    if vtz_field.group(2) in timezone_lookup:
      tz = zoneinfo.ZoneInfo(timezone_lookup[vtz_field.group(2)])
    else:
      try:
        tz = zoneinfo.ZoneInfo(vtz_field.group(2))
      except Exception as e:
        raise Exception(f'No ZoneInfo time zone for {vtz_field.group(2)}')
        
    # generate datetime from field, convert to UTC datetime, and encode as UTC string
    d = vtz_field.group(3)
    dt = datetime.datetime.strptime(f'{d[0:8]}{d[9:15]}', '%Y%m%d%H%M%S').replace(tzinfo=tz)
    utcdt = dt.astimezone(zoneinfo.ZoneInfo('etc/UTC'))
    utcdt_string = utcdt.strftime('%Y%m%dT%H%M%SZ')
    updated_field = f'DT{vtz_field.group(1)}:{utcdt_string}'
    text = text.replace(vtz_field.group(0), updated_field)

  return text

Please try the upcoming version 0.8 from the current main branch, it should fix that behaviour and handle timezones (very) properly:

pip install -U git+https://github.com/ics-py/ics-py.git

Please note that we now longer use Arrow and use plain datetimes instead, as those also handle floating events' times properly. Furthermore, serialisation to ics should be done via a call to .serialize() instead of using the built-in to string mechanism.

commented

Hi 👋
This issue seems stale as nothing moved since a long time. I'm closing it now but feel free to reopen if needed :)