Skip to content

Instantly share code, notes, and snippets.

@rmasters
Created October 21, 2012 02:18
Show Gist options
  • Save rmasters/3925486 to your computer and use it in GitHub Desktop.
Save rmasters/3925486 to your computer and use it in GitHub Desktop.
Finding occurrences of repeated events within a given datetime range
"""
Find all events from an iCalendar file, with some support for recurrent events
Implemented:
- Normal events (with DTSTART and DTEND)
- Daily, weekly, monthly, yearly, hourly and minutely frequencies (FREQ)
- Intervals in frequencies (INTERVAL)
- Until date (UNTIL)
- Maximum occurrences (COUNT)
- Excluded dates (EXDATE)
Not implemented:
- BYDAY (generally weekly works if the DTSTART uses the desired day)
- BYMONTH
- BYMONTHDAY
- BYSETPOS
- BYHOUR
- BYMINUTE
- WKST
Timezone support is also week (icalendar does not report the timezone used on
DTSTART/DTEND so this is difficult to tell).
"""
import datetime
import copy
import pytz
from icalendar import Calendar, Event
TIMEZONE = pytz.timezone('Europe/London')
"""
Make a datetime timezone aware, and convert datetime.dates to datetime.datetimes
icalendar returns naive datetimes, so this attempts to cooerce them to a
timezone in a dubious manner
"""
def fixtz(dt):
if isinstance(dt, datetime.datetime):
if dt.tzinfo is None:
dt = TIMEZONE.localize(dt)
elif dt.tzinfo is not TIMEZONE:
dt = dt.astimezone(TIMEZONE)
else:
dt = datetime.datetime.combine(dt, datetime.time(0,0,0))
dt = TIMEZONE.localize(dt)
return dt
"""
Get events within a given datetime range
calendar: a icalendar.Calendar() instance
start, end: datetime.datetime instances
"""
def get_events(calendar, start, end):
start = fixtz(start)
end = fixtz(end)
# Collected events
events = []
# Walk each event in the calendar
for evt in calendar.walk('VEVENT'):
# Normalise start and end times
evt['DTSTART'].dt = fixtz(evt['DTSTART'].dt)
evt['DTEND'].dt = fixtz(evt['DTEND'].dt)
# If the event is not recurrent, simply check if it's within our desired
# range
if 'RRULE' not in evt:
if evt['DTSTART'].dt >= start and evt['DTEND'].dt <= end:
events.append(evt)
continue
# The event is recurrent - find occurrences
occurences = []
# For use with RRULE:COUNT=n
counter = 1
# A tuple of the current/last start and end date for incrementing
cur_date = (evt['DTSTART'].dt, evt['DTEND'].dt)
# Increment a start/end datetime pair
def increment(start, end):
interval = evt['RRULE']['INTERVAL'][0] if 'INTERVAL' in evt['RRULE'] else 1
freq = evt['RRULE']['FREQ'][0]
if freq == 'DAILY':
# Add n days
delta = datetime.timedelta(days=interval)
return (start+delta, end+delta)
elif freq == 'WEEKLY':
# Add n weeks
delta = datetime.timedelta(weeks=interval)
return (start+delta, end+delta)
elif freq == 'MONTHLY':
# Add n months
month, year = divmod(interval, 12)
return (start.replace(month=start.month+month, year=start.year+year), \
end.replace(month=end.month+month, year=end.year+year))
elif freq == 'YEARLY':
# Add n years
return (start.replace(year=start.year+interval), \
end.replace(year=end.year+interval))
elif freq == 'HOURLY':
# Add n hours
delta = datetime.timedelta(hours=interval)
return (start+delta, end+delta)
elif freq == 'MINUTELY':
# Add n minutes
delta = datetime.timedelta(minutes=interval)
return (start+delta, end+delta)
# Not incremented - should really be reported somehow
return (start, end)
# Find all occurences
while True:
# If we have reached the max occurences required, bail out
if 'COUNT' in evt['RRULE'] and counter > evt['RRULE']['COUNT'][0]: break
# Make a shallow copy of the event
occ = copy.deepcopy(evt)
occ['RRULE'] = None
if counter > 1: cur_date = increment(cur_date[0], cur_date[1])
occ['DTSTART'].dt = cur_date[0]
occ['DTEND'].dt = cur_date[1]
# If we have passed the maximum date, exit
if 'UNTIL' in evt['RRULE'] and cur_date > evt['RRULE']['UNTIL'].dt: break
# If the event ends after the end date, exit
if occ['DTEND'].dt > end: break
# Ensure the date is within the range from the start
if occ['DTSTART'].dt >= start:
# Ensure date is not in the exclusion list
if 'EXDATE' not in evt or \
occ['DTSTART'].dt not in [fixtz(ex.dt) for ex in evt['EXDATE'].dts]:
occurences.append(occ)
dump_evt(occ)
counter += 1
events += occurences
return events
# Sample usage
if __name__ == "__main__":
with open('calendar.ics') as f:
calendar = Calendar().from_ical(f.read())
start = datetime.datetime.today()
end = datetime.datetime.today()
events = get_events(calendar, start, end)
for evt in events:
print evt['DTSTART'].dt, evt['DTEND'].dt, evt['SUMMARY']
@rmasters
Copy link
Author

5765ab Fixes an issue where an occurrence is modified by the next run (most assignments in Python create references).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment