]> git.neil.brown.name Git - plato.git/commitdiff
ical.py: ical RRULE parsing.
authorNeilBrown <neilb@suse.de>
Mon, 30 Dec 2013 02:19:22 +0000 (13:19 +1100)
committerNeilBrown <neilb@suse.de>
Mon, 30 Dec 2013 02:19:22 +0000 (13:19 +1100)
This is still preliminary code, not really usable as a library.

The main purpose is to produce a list of date/times from an ical
recurrence rule.

lib/ical.py [new file with mode: 0644]

diff --git a/lib/ical.py b/lib/ical.py
new file mode 100644 (file)
index 0000000..c4afe36
--- /dev/null
@@ -0,0 +1,740 @@
+#
+#
+# Ical manipulation
+# Particularly date recurrance rules.
+# But full VEVENTs really to provide full picture.
+#
+# A VEVENT has:
+#  DTSTART     When event starts
+#  DTEND       When event ends, if it has duration
+#  DURATION    alternate form
+#  RRULE       recurrance rule
+#  EXDATE      dates that would be in the list, but are excluded
+#
+#
+# RRULE can contain:
+#   COUNT   - number of events
+#   UNTIL   - last date to consider  (not if COUNT is given)
+#   INTERVAL - number of units between events
+#   FREQ    - YEARLY MONTHLY WEEKLY DAILY HOURLY MINUTELY SECONDLY
+#                 The unit
+#
+#   BYMONTH  BYWEEKNO  BYYEARDAY  BYMONTHDAY BYDAY BYHOUR BYMINUTE BYSECOND
+#          give specific times in the unit.  comma sep list
+#          BYDAY can be +5TU  meaning first TUesday.  -1TU is last
+#          BYMONTHDAY can be -1 for last day
+#   BYSETPOS  1 or more indexs into the current set of dates.
+#   WKST   chose the day to start weeks  (default MO)
+#            week 1 contains at least 4 days
+#
+# An RRULE produces a list of moments: date or date+time
+#  We start with the DTSTART which accompanies the RRULE and add
+#  moments based on FREQ (must be present) and INTERVAL (defaults to 1).
+#  Then for each BY* rule (in order given) we either discard
+#  moments that don't match, or split each into smaller-units.
+#  If a BY* is followed by BYSETPOS we filter the split through that list.
+#  We never keep more than COUNT or any date after UNTIL.
+#  Each 'by*' produces an object with an 'interval' and the
+#  ability to 'expand' or 'filter' a date
+#  If the current 'freq' if longer than the by* interval, we expand
+#  and update the 'freq' for subsequent 'by*'.
+#  If the current 'freq' is smaller or equal to the by* interval
+#  we keep or discard depending on the filter
+#
+# If there is a COUNT, we need to expand from DTSTART.  If there isn't
+# a COUNT we can expand from any date after rounding to FREQ*INTERVAL
+#
+#
+# ARGH - this ical rrule stuff always confuses me.  I need to get it
+# straight.
+# We start with a moment (DTSTART), a FREQ, and an interval (which is
+# a multiplier of freq to some extent).
+# The elements of DTSTART which match or a longer than the FREQ determine
+# the first instance.  The FREQ and INTERVAL determine subsequent instances.
+# The elements of DSTTART with smaller granularity determine the moment
+# within each instance.
+#
+# Each BY* can do one of two things.
+# If its FREQ is the same or larger than the current FREQ, it acts as
+# a filter and discards instances which don't match.  If its FREQ is shorter
+# than the current FREQ, then each interval is divided according to the new
+# freq and the BY* acts as a filter on those.
+#
+# BYSETPOS is combined with the previous BY* and further filters the set of instances.
+#
+# So we start with a moment and generate a list of moments of the given
+# frequency and interval.  We also have a list of BY* objects.
+# We pass a moment to a BY* and get back a (possibly empty) list of moments
+# which must then be passed to the next BY*.  These may be before the given
+# moment so we need to check everything against DTSTART.
+
+import time,copy
+
+by={}
+
+month_days =  [31,28,31,30,31,30,31,31,30,31,30,31]
+daynames=['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday']
+shortdays=['SU','MO','TU','WE','TH','FR','SA']
+
+def daysin(mon, yr):
+    if mon != 2:
+        return month_days[mon-1]
+    if yr % 4 > 0:
+        return 28
+    if yr % 400 > 0:
+        return 29
+    return 28
+
+class moment:
+    def __init__(self, tm):
+        self.yr = tm.tm_year
+        self.mon = tm.tm_mon
+        self.day = tm.tm_mday
+        self.hr = tm.tm_hour
+        self.min = tm.tm_min
+        self.sec = tm.tm_sec
+        self.wday = (tm.tm_wday+1) % 7
+        self.yday = tm.tm_yday
+        self.zone = None
+
+    def norm_sec(self):
+        if self.sec >= 0 and self.sec < 60:
+            return
+        while self.sec >= 60:
+            self.sec -= 60
+            self.min += 1
+        while self.sec < 0:
+            self.sec += 60
+            self.min -= 1
+        self.norm_min()
+
+    def norm_min(self):
+        if self.min >= 0 and self.min < 60:
+            return
+        while self.min >= 60:
+            self.min -= 60
+            self.hr += 1
+        while self.min < 0:
+            self.min += 60
+            self.hr -= 1
+        self.norm_hr()
+
+    def norm_hr(self):
+        if self.hr >= 0 and self.hr < 24:
+            return
+        while self.hr >= 24:
+            self.hr -= 24
+            self.day += 1
+            self.wday += 1
+            self.yday += 1
+        while self.hr < 0:
+            self.hr += 24
+            self.day -= 1
+            self.wday -= 1
+            self.yday -= 1
+        self.norm_day()
+
+    def norm_day(self):
+        while self.wday < 0:
+            self.wday += 7
+        while self.wday > 6:
+            self.wday -= 7
+
+        if self.day >= 1 and self.day <= daysin(self.mon, self.yr):
+            return
+        while self.day < 1:
+            self.mon -= 1
+            self.norm_mon()
+            self.day += daysin(self.mon, self.yr)
+        while self.day > daysin(self.mon, self.yr):
+            self.day -= daysin(self.mon, self.yr)
+            self.mon += 1
+            self.norm_mon()
+
+    def norm_mon(self):
+        while self.mon < 1:
+            self.mon += 12
+            self.yr -= 1
+        while self.mon > 12:
+            self.mon -= 12
+            self.yr += 1
+
+    def set_yday(self):
+        yday = self.day
+        for m in range(1, self.mon):
+            yday += daysin(m, self.yr)
+        self.yday = yday
+
+    def set_wday(self):
+        self.set_yday()
+        yr = self.yr - 1
+        wday = yr + yr/4 - yr/400 + self.yday + 4
+        self.wday = wday % 7
+
+    def fmt(self):
+        if self.hr >= 0:
+            return "%4d/%02d/%02d %02d:%02d:%02d %s" %  (self.yr,self.mon,self.day,self.hr,self.min,self.sec, daynames[self.wday])
+        else:
+            return "%4d/%02d/%02d %s" %  (self.yr,self.mon,self.day, daynames[self.wday])
+
+    def tonum(self):
+        if self.hr < 0:
+            return 60 * (60 * (24 * (self.day + 31 * (self.mon + 12 * self.yr))))
+        else:
+            return self.sec + 60 * (self.min + 60 * (self.hr + 24 * (self.day + 31 * (self.mon + 12 * self.yr))))
+
+    def before(self, other):
+        return self.tonum() < other.tonum()
+
+class date_seq:
+    def __init__(self, start, freq, interval = 1):
+        self.now = copy.copy(start)
+        self.interval = interval
+        self.freq = freq
+
+    def next(self):
+        if self.freq == 'SECONDLY':
+            self.now.sec += self.interval
+            self.now.norm_sec()
+        elif self.freq == 'MINUTELY':
+            self.now.min += self.interval
+            self.now.norm_min()
+        elif self.freq == 'HOURLY':
+            self.now.hr += self.interval
+            self.now.norm_hr()
+        elif self.freq == 'DAILY':
+            self.now.day += self.interval
+            self.now.norm_day()
+        elif self.freq == 'WEEKLY':
+            self.now.day += 7 * self.interval
+            self.now.norm_day()
+        elif self.freq == 'MONTHLY':
+            self.now.mon += self.interval
+            self.now.norm_mon()
+        elif self.freq == 'YEARLY':
+            self.now.yr += self.interval
+        else:
+            return None
+        self.now.set_wday()
+        return self.now
+
+# Each BY* is filter which will discards moments, or generates sub-moments.
+# e.g. BYMONTH will discard non-matching MONTHLY, WEEKLY etc moments, and
+# will convert a YEARLY moment to some MONTHLY moments
+class bysimple:
+    def __init__(self, list, min, max):
+        self.vals = []
+        for v in list:
+            try:
+                val = int(v)
+                if val >= min and val <= max:
+                    self.vals.append(val)
+            except:
+                pass
+    def expand(self, moment, xtype, wkst):
+        rv = []
+        for v in self.vals:
+            mm = copy.copy(moment)
+            if self.set(mm, v):
+                rv.append(mm)
+        return  rv
+
+    def filter(self, moment):
+        return self.val(moment) in self.vals
+
+class bymonth(bysimple):
+    def __init__(self, list):
+        bysimple.__init__(self, list, 1, 12)
+    def val(self, moment):
+        return moment.mon
+    def set(self, moment, v):
+        if moment.day > daysin(v, moment.yr):
+            return False
+        moment.mon = v
+        moment.set_wday()
+        return True
+by['BYMONTH'] = bymonth
+
+class bysecond(bysimple):
+    def __init__(self, list):
+        bysimple.__init__(self, list, 0, 59)
+    def val(self, moment):
+        return moment.sec
+    def set(self, moment, v):
+        moment.sec = v
+        return True
+by['BYSECOND'] = bysecond
+
+class byminute(bysimple):
+    def __init__(self, list):
+        bysimple.__init__(self, list, 0, 59)
+    def val(self, moment):
+        return moment.min
+    def set(self, moment, v):
+        moment.min = v
+        return True
+by['BYMINUTE'] = byminute
+
+class byhour(bysimple):
+    def __init__(self, list):
+        bysimple.__init__(self, list, 0, 59)
+    def val(self, moment):
+        return moment.hr
+    def set(self, moment, v):
+        moment.hr = v
+        return True
+by['BYHOUR'] = byhour
+
+class byday:
+    """ SU-SA or 1TU or -1TU
+    Numbers only allowed if xtype is MONTHLY or YEARLY
+    """
+    def __init__(self, list):
+        self.list = {}
+        for d in shortdays:
+            self.list[d] = []
+        for d in list:
+            if len(d) < 2 or d[-2:] not in shortdays:
+                continue
+            n = d[:-2]
+            if n == '':
+                self.list[d[-2:]].append(0)
+            else:
+                self.list[d[-2:]].append(int(n))
+    def filter(self, moment):
+        l = self.list[shortdays[moment.wday]]
+        if len(l) == 0:
+            return False
+        if 0 in l:
+            return True
+
+        # ignore number prefixes for filtering
+        return False
+
+    def expand(self, moment, xtype, wkst):
+        rv = []
+        start = copy.copy(moment)
+        if xtype == 'YEARLY':
+            # every relevant day in this year
+            start.mon = 1
+            start.day = 1
+            days = daysin(2, start.yr)-28+365
+        elif xtype == 'MONTHLY':
+            # every relevany day in this month
+            start.day = 1
+            days = daysin(start.mon, start.yr)
+        else:
+            # every relevant day in this week
+            while start.wday != wkst:
+                start.day -= 1
+                start.wday -= 1
+                start.norm_day()
+            days = 7
+        start.set_wday()
+        for d in range(days):
+            pwk = d/7 + 1
+            mwk = (days-d)/7 + 1
+            dl = self.list[shortdays[start.wday]]
+            if 0 in dl or pwk in dl or -mwk in dl:
+                rv.append(copy.copy(start))
+            start.day += 1
+            start.norm_day()
+            start.set_wday()
+        return rv
+by['BYDAY'] = byday
+
+class bymonthday:
+    def __init__(self, list):
+        self.from_start = []
+        self.from_end = []
+        for v in list:
+            try:
+                val = int(v)
+                if val >= 1 and val <= 31:
+                    self.from_start.append(val)
+                if -val >= 1 and -val <= 31:
+                    self.from_end.append(val)
+            except:
+                pass
+    def filter(self, moment):
+        days = daysin(moment.mon, moment.yr)
+        return mm.day in self.from_start or (mm.day - days - 1) in self.from_end
+
+    def expand(self, moment, xtype, wkst):
+        rv = []
+        days = daysin(moment.mon, moment.yr)
+        for v in self.from_start:
+            if v <= days:
+                mm = copy.copy(moment)
+                mm.day = v
+                mm.set_wday()
+                rv.append(mm)
+        for v1 in self.from_end:
+            v = days + 1 + v1
+            if v >= 1 and v not in self.from_start:
+                mm = copy.copy(moment)
+                mm.day = v
+                mm.set_wday()
+                rv.append(mm)
+        return rv
+
+by['BYMONTHDAY'] = bymonthday
+
+class byyearday:
+    def __init__(self, list):
+        self.from_start = []
+        self.from_end = []
+        for v in list:
+            try:
+                val = int(v)
+                if val >= 1 and val <= 366:
+                    self.from_start.append(val)
+                if -val >= 1 and -val <= 366:
+                    self.from_end.append(val)
+            except:
+                pass
+    def expand(self, moment, xtype, wkst):
+        rv = []
+        days = daysin(2, moment.yr)-28+365
+        for v in self.from_start:
+            if v <= days:
+                mm = copy.copy(moment)
+                for m in range(1,13):
+                    d = daysin(m, moment.yr)
+                    if v <= d:
+                        break
+                    v -= d
+                mm.day = v
+                mm.mon = m
+                mm.set_wday()
+                rv.append(mm)
+        for v1 in self.from_end:
+            v = days + 1 + v1
+            if v >= 1 and v not in self.from_start:
+                mm = copy.copy(moment)
+                for m in range(1,13):
+                    d = daysin(m, moment.yr)
+                    if v <= d:
+                        break
+                    v -= d
+                mm.day = v
+                mm.mon = m
+                mm.set_wday()
+                rv.append(mm)
+        return rv
+
+    def filter(self, moment):
+        days = daysin(2, moment.yr)-28+365
+        return mm.yday in self.from_start or (mm.yday - days - 1) in self.from_end
+
+by['BYYEARDAY'] = byyearday
+
+class byweekno:
+    '''
+    WEEKNO 0 isn't allowed, and negatives aren't defined in the RFC.
+    We allow WEEKNO=0 and -1 to be last week with at least 4 days
+    '''
+    def __init__(self, list):
+        self.weeks = []
+        for v in list:
+            try:
+                val = int(v)
+                if val >= -53 and val <= 53:
+                    self.weeks.append(val)
+            except:
+                pass
+    def expand(self, moment, xtype, wkst):
+        rv = []
+        days = daysin(2, moment.yr)-28+365
+        start = copy.copy(moment)
+        start.mon = 1
+        start.day = -2
+        start.norm_day()
+        start.set_wday()
+        while start.wday != wkst:
+            start.day += 1
+            start.norm_day()
+            start.set_wday()
+        end = copy.copy(moment)
+        end.mon = 12
+        end.day = 28
+        end.set_wday()
+        while end.wday != wkst:
+            end.day -= 1
+            end.set_wday()
+        # 'start' starts week 1,
+        # 'end' starts week -1
+        for wn in self.weeks:
+            if wn >= 0:
+                st = copy.copy(start)
+                st.day += (wn-1)*7
+                st.norm_day()
+                st.set_wday()
+            else:
+                st = copy.copy(end)
+                st.day -= (-1-wn)*7
+                st.norm_day()
+                st.set_wday()
+        for d in range(7):
+            if st.yr == moment.yr:
+                rv.append(copy.copy(st))
+            st.day += 1
+            st.norm_day()
+            st.set_wday()
+        return rv
+by['BYWEEKNO'] = byweekno
+
+class rrule:
+    def __init__(self, str, tz):
+        self.step = 1
+        self.bylist = {}
+        self.count = None
+        self.end = None
+        self.interval = None
+        self.byset = None
+        self.wkst = shortdays.index('MO')
+        for w in str.split(';'):
+            x = w.split('=',1)
+            field = x[0].upper()
+            if field == "FREQ":
+                self.set_freq(x[1])
+            if field == "INTERVAL":
+                self.set_step(x[1])
+            if field == "COUNT":
+                self.set_count(x[1])
+            if field == "UNTIL":
+                self.set_end(x[1])
+            if field == "BYSETPOS":
+                self.add_set(x[1])
+            if field == "WKST":
+                self.wkst = shortdays.index(x[1].upper())
+            if field in by:
+                self.add_by(field, by[field](x[1].split(',')))
+
+    def set_freq(self, f):
+        if f.upper() in intervals:
+            self.interval = f
+    def set_step(self, st):
+        self.step = int(st)
+    def set_count(self, c):
+        self.count = int(c)
+    def set_end(self, e):
+        self.end = ical_date(e, tz)
+    def add_by(self, field, byx):
+        self.bylist[field] = byx
+    def add_set(self, set):
+        self.byset = map(int, set.split(','))
+
+
+def sort_and_trim(list, start = None, select = None):
+    list.sort(key = lambda x:x.tonum())
+    n = 1
+    while n < len(list):
+        if list[n-1].tonum() == list[n].tonum():
+            del(list[n])
+        else:
+            n += 1
+    if select:
+        l = []
+        for n in select:
+            if n > 0 and n <= len(list):
+                l.append(list[n-1])
+            if n < 0 and n >= -len(list):
+                l.append(list[n])
+        list = l
+    while start and list and list[0].tonum() < start.tonum():
+        list.pop(0)
+    return list
+
+def sub_dates(l, togo):
+    nums = map(lambda x:x.tonum(), togo)
+    n = 0
+    while n < len(l):
+        if l[n].tonum() in nums:
+            del(l[n])
+        else:
+            n += 1
+
+'''
+From RFC5545
+
+      If multiple BYxxx rule parts are specified, then after evaluating
+      the specified FREQ and INTERVAL rule parts, the BYxxx rule parts
+      are applied to the current set of evaluated occurrences in the
+      following order: BYMONTH, BYWEEKNO, BYYEARDAY, BYMONTHDAY, BYDAY,
+      BYHOUR, BYMINUTE, BYSECOND and BYSETPOS; then COUNT and UNTIL are
+      evaluated.
+
+and
+
+   +----------+--------+--------+-------+-------+------+-------+------+
+   |          |SECONDLY|MINUTELY|HOURLY |DAILY  |WEEKLY|MONTHLY|YEARLY|
+   +----------+--------+--------+-------+-------+------+-------+------+
+   |BYMONTH   |Limit   |Limit   |Limit  |Limit  |Limit |Limit  |Expand|
+   +----------+--------+--------+-------+-------+------+-------+------+
+   |BYWEEKNO  |N/A     |N/A     |N/A    |N/A    |N/A   |N/A    |Expand|
+   +----------+--------+--------+-------+-------+------+-------+------+
+   |BYYEARDAY |Limit   |Limit   |Limit  |N/A    |N/A   |N/A    |Expand|
+   +----------+--------+--------+-------+-------+------+-------+------+
+   |BYMONTHDAY|Limit   |Limit   |Limit  |Limit  |N/A   |Expand |Expand|
+   +----------+--------+--------+-------+-------+------+-------+------+
+   |BYDAY     |Limit   |Limit   |Limit  |Limit  |Expand|Note 1 |Note 2|
+   +----------+--------+--------+-------+-------+------+-------+------+
+   |BYHOUR    |Limit   |Limit   |Limit  |Expand |Expand|Expand |Expand|
+   +----------+--------+--------+-------+-------+------+-------+------+
+   |BYMINUTE  |Limit   |Limit   |Expand |Expand |Expand|Expand |Expand|
+   +----------+--------+--------+-------+-------+------+-------+------+
+   |BYSECOND  |Limit   |Expand  |Expand |Expand |Expand|Expand |Expand|
+   +----------+--------+--------+-------+-------+------+-------+------+
+   |BYSETPOS  |Limit   |Limit   |Limit  |Limit  |Limit |Limit  |Limit |
+   +----------+--------+--------+-------+-------+------+-------+------+
+
+      Note 1:  Limit if BYMONTHDAY is present; otherwise, special expand
+               for MONTHLY.
+
+      Note 2:  Limit if BYYEARDAY or BYMONTHDAY is present; otherwise,
+               special expand for WEEKLY if BYWEEKNO present; otherwise,
+               special expand for MONTHLY if BYMONTH present; otherwise,
+               special expand for YEARLY.
+'''
+byorder=['BYMONTH','BYWEEKNO','BYYEARDAY','BYMONTHDAY','BYDAY','BYHOUR','BYMINUTE','BYSECOND']
+intervals = ['SECONDLY','MINUTELY','HOURLY','DAILY','WEEKLY','MONTHLY','YEARLY']
+byaction = {
+    'BYMONTH' : [ -1,     -1,      -1,     -1,    -1,    -1,      1],
+    'BYWEEKNO': [  0,      0,       0,      0,     0,     0,      1],
+    'BYYEARDAY':[ -1,     -1,      -1,      0,     0,     0,      1],
+    'BYMONTHDAY':[-1,     -1,      -1,     -1,     0,     1,      1],
+    'BYDAY':    [ -1,     -1,      -1,     -1,     1,     2,      3],
+    'BYHOUR':   [ -1,     -1,      -1,      1,     1,     1,      1],
+    'BYMINUTE': [ -1,     -1,       1,      1,     1,     1,      1],
+    'BYSECOND': [ -1,      1,       1,      1,     1,     1,      1],
+}
+
+def make_dates(start, rr):
+    ret = []
+    last = start
+    s = date_seq(start, rr.interval, rr.step)
+    while (rr.count != None and len(ret) < rr.count) or (rr.end != None and last.before(rr.end)):
+        n1 = [ copy.copy(last) ]
+        for bn in byorder:
+            if bn not in rr.bylist:
+                continue
+            b = rr.bylist[bn]
+            n2 = []
+            act = byaction[bn][intervals.index(rr.interval)]
+            xtype = None
+            if act == 2:
+                if 'BYMONTHDAY' in rr.bylist:
+                    act = -1
+                else:
+                    xtype = 'MONTHLY'
+            elif act == 3:
+                if 'BYYEARDAY' in rr.bylist or 'BYMONTHDAY' in rr.bylist:
+                    act = -1
+                elif 'BYWEEKNO' in rr.bylist:
+                    act = -1
+                elif 'BYMONTH' in rr.bylist:
+                    xtype = 'MONTHLY'
+                else:
+                    xtype = 'YEARLY'
+            if act < 0:
+                for n in n1:
+                    if b.filter(n):
+                        n2.append(n)
+            elif act > 0:
+                for n in n1:
+                    n2.extend(b.expand(n, xtype, rr.wkst))
+            else:
+                raise ValueError('%s not permitted with %s' % (bn, rr.interval))
+            n1 = n2
+        if n1:
+            n1 = sort_and_trim(n1, start, rr.byset)
+        if n1:
+            last = n1[-1]
+            ret.extend(n1)
+        last = s.next()
+    if rr.count:
+        ret = ret[:rr.count]
+    if rr.end:
+        n1 = []
+        for n in ret:
+            if n.before(rr.end):
+                n1 += [n]
+        ret = n1
+    return ret
+
+'''
+FIXME should handle
+  start/stop
+and
+  start/period
+where 'period' is
+ P [nW][nD][T[nH][nM][nS]]
+'''
+def ical_date(s, tz):
+    m = moment(time.localtime())
+    if len(s) < 8 or not s[0:8].isdigit():
+        raise ValueError('Bad date/time %s' % s)
+    m.yr = int(s[0:4])
+    m.mon = int(s[4:6])
+    m.day = int(s[6:8])
+    if len(s) == 8:
+        m.hr = -1
+        return m
+    if (not (len(s) == 15  or (len(s) == 16 and s[15] == 'Z'))
+        or s[8] != 'T' or not s[9:15].isdigit()):
+        raise ValueError('Bad data/time %s' % s)
+    m.hr = int(s[9:11])
+    m.min = int(s[11:13])
+    m.sec = int(s[13:15])
+    if m.sec == 60:
+        # leap second...
+        m.sec = 59
+    if len(s) == 16:
+        m.zone = "UTC"
+    else:
+        m.zone = tz
+    m.set_wday()
+    return m
+
+def timelist(str, tz):
+    rv = []
+    for d in str.split(','):
+        rv.append(ical_date(d, tz))
+    return rv
+
+import sys
+start = None
+rr = None
+rdate = None
+xdate =  None
+for arg in sys.argv[1:]:
+    l = arg.split(':', 1)
+    head = l[0].split(';')
+    tz = None
+    if len(head) > 1 and head[1][:5] == "TZID=":
+        tz = head[1][5:]
+    tag = head[0].upper()
+    if tag == 'DTSTART':
+        start = ical_date(l[1], tz)
+    elif tag == 'RRULE':
+        rr = rrule(l[1], tz)
+    elif tag == 'RDATE':
+        rdate = timelist(l[1], tz)
+    elif tag == 'EXDATE':
+        xdate = timelist(l[1], tz)
+    else:
+        raise ValueError('Bad tag: %s' % tag)
+
+m = []
+if start and rr:
+    m = make_dates(start, rr)
+if rdate:
+    m.extend(rdate)
+    m = sort_and_trim(m)
+
+if xdate:
+    sub_dates(m, xdate)
+
+for m1 in m:
+    print m1.fmt()