diff env/lib/python3.7/site-packages/dateutil/rrule.py @ 0:26e78fe6e8c4 draft

"planemo upload commit c699937486c35866861690329de38ec1a5d9f783"
author shellac
date Sat, 02 May 2020 07:14:21 -0400
parents
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/env/lib/python3.7/site-packages/dateutil/rrule.py	Sat May 02 07:14:21 2020 -0400
@@ -0,0 +1,1735 @@
+# -*- coding: utf-8 -*-
+"""
+The rrule module offers a small, complete, and very fast, implementation of
+the recurrence rules documented in the
+`iCalendar RFC <https://tools.ietf.org/html/rfc5545>`_,
+including support for caching of results.
+"""
+import itertools
+import datetime
+import calendar
+import re
+import sys
+
+try:
+    from math import gcd
+except ImportError:
+    from fractions import gcd
+
+from six import advance_iterator, integer_types
+from six.moves import _thread, range
+import heapq
+
+from ._common import weekday as weekdaybase
+
+# For warning about deprecation of until and count
+from warnings import warn
+
+__all__ = ["rrule", "rruleset", "rrulestr",
+           "YEARLY", "MONTHLY", "WEEKLY", "DAILY",
+           "HOURLY", "MINUTELY", "SECONDLY",
+           "MO", "TU", "WE", "TH", "FR", "SA", "SU"]
+
+# Every mask is 7 days longer to handle cross-year weekly periods.
+M366MASK = tuple([1]*31+[2]*29+[3]*31+[4]*30+[5]*31+[6]*30 +
+                 [7]*31+[8]*31+[9]*30+[10]*31+[11]*30+[12]*31+[1]*7)
+M365MASK = list(M366MASK)
+M29, M30, M31 = list(range(1, 30)), list(range(1, 31)), list(range(1, 32))
+MDAY366MASK = tuple(M31+M29+M31+M30+M31+M30+M31+M31+M30+M31+M30+M31+M31[:7])
+MDAY365MASK = list(MDAY366MASK)
+M29, M30, M31 = list(range(-29, 0)), list(range(-30, 0)), list(range(-31, 0))
+NMDAY366MASK = tuple(M31+M29+M31+M30+M31+M30+M31+M31+M30+M31+M30+M31+M31[:7])
+NMDAY365MASK = list(NMDAY366MASK)
+M366RANGE = (0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366)
+M365RANGE = (0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365)
+WDAYMASK = [0, 1, 2, 3, 4, 5, 6]*55
+del M29, M30, M31, M365MASK[59], MDAY365MASK[59], NMDAY365MASK[31]
+MDAY365MASK = tuple(MDAY365MASK)
+M365MASK = tuple(M365MASK)
+
+FREQNAMES = ['YEARLY', 'MONTHLY', 'WEEKLY', 'DAILY', 'HOURLY', 'MINUTELY', 'SECONDLY']
+
+(YEARLY,
+ MONTHLY,
+ WEEKLY,
+ DAILY,
+ HOURLY,
+ MINUTELY,
+ SECONDLY) = list(range(7))
+
+# Imported on demand.
+easter = None
+parser = None
+
+
+class weekday(weekdaybase):
+    """
+    This version of weekday does not allow n = 0.
+    """
+    def __init__(self, wkday, n=None):
+        if n == 0:
+            raise ValueError("Can't create weekday with n==0")
+
+        super(weekday, self).__init__(wkday, n)
+
+
+MO, TU, WE, TH, FR, SA, SU = weekdays = tuple(weekday(x) for x in range(7))
+
+
+def _invalidates_cache(f):
+    """
+    Decorator for rruleset methods which may invalidate the
+    cached length.
+    """
+    def inner_func(self, *args, **kwargs):
+        rv = f(self, *args, **kwargs)
+        self._invalidate_cache()
+        return rv
+
+    return inner_func
+
+
+class rrulebase(object):
+    def __init__(self, cache=False):
+        if cache:
+            self._cache = []
+            self._cache_lock = _thread.allocate_lock()
+            self._invalidate_cache()
+        else:
+            self._cache = None
+            self._cache_complete = False
+            self._len = None
+
+    def __iter__(self):
+        if self._cache_complete:
+            return iter(self._cache)
+        elif self._cache is None:
+            return self._iter()
+        else:
+            return self._iter_cached()
+
+    def _invalidate_cache(self):
+        if self._cache is not None:
+            self._cache = []
+            self._cache_complete = False
+            self._cache_gen = self._iter()
+
+            if self._cache_lock.locked():
+                self._cache_lock.release()
+
+        self._len = None
+
+    def _iter_cached(self):
+        i = 0
+        gen = self._cache_gen
+        cache = self._cache
+        acquire = self._cache_lock.acquire
+        release = self._cache_lock.release
+        while gen:
+            if i == len(cache):
+                acquire()
+                if self._cache_complete:
+                    break
+                try:
+                    for j in range(10):
+                        cache.append(advance_iterator(gen))
+                except StopIteration:
+                    self._cache_gen = gen = None
+                    self._cache_complete = True
+                    break
+                release()
+            yield cache[i]
+            i += 1
+        while i < self._len:
+            yield cache[i]
+            i += 1
+
+    def __getitem__(self, item):
+        if self._cache_complete:
+            return self._cache[item]
+        elif isinstance(item, slice):
+            if item.step and item.step < 0:
+                return list(iter(self))[item]
+            else:
+                return list(itertools.islice(self,
+                                             item.start or 0,
+                                             item.stop or sys.maxsize,
+                                             item.step or 1))
+        elif item >= 0:
+            gen = iter(self)
+            try:
+                for i in range(item+1):
+                    res = advance_iterator(gen)
+            except StopIteration:
+                raise IndexError
+            return res
+        else:
+            return list(iter(self))[item]
+
+    def __contains__(self, item):
+        if self._cache_complete:
+            return item in self._cache
+        else:
+            for i in self:
+                if i == item:
+                    return True
+                elif i > item:
+                    return False
+        return False
+
+    # __len__() introduces a large performance penalty.
+    def count(self):
+        """ Returns the number of recurrences in this set. It will have go
+            trough the whole recurrence, if this hasn't been done before. """
+        if self._len is None:
+            for x in self:
+                pass
+        return self._len
+
+    def before(self, dt, inc=False):
+        """ Returns the last recurrence before the given datetime instance. The
+            inc keyword defines what happens if dt is an occurrence. With
+            inc=True, if dt itself is an occurrence, it will be returned. """
+        if self._cache_complete:
+            gen = self._cache
+        else:
+            gen = self
+        last = None
+        if inc:
+            for i in gen:
+                if i > dt:
+                    break
+                last = i
+        else:
+            for i in gen:
+                if i >= dt:
+                    break
+                last = i
+        return last
+
+    def after(self, dt, inc=False):
+        """ Returns the first recurrence after the given datetime instance. The
+            inc keyword defines what happens if dt is an occurrence. With
+            inc=True, if dt itself is an occurrence, it will be returned.  """
+        if self._cache_complete:
+            gen = self._cache
+        else:
+            gen = self
+        if inc:
+            for i in gen:
+                if i >= dt:
+                    return i
+        else:
+            for i in gen:
+                if i > dt:
+                    return i
+        return None
+
+    def xafter(self, dt, count=None, inc=False):
+        """
+        Generator which yields up to `count` recurrences after the given
+        datetime instance, equivalent to `after`.
+
+        :param dt:
+            The datetime at which to start generating recurrences.
+
+        :param count:
+            The maximum number of recurrences to generate. If `None` (default),
+            dates are generated until the recurrence rule is exhausted.
+
+        :param inc:
+            If `dt` is an instance of the rule and `inc` is `True`, it is
+            included in the output.
+
+        :yields: Yields a sequence of `datetime` objects.
+        """
+
+        if self._cache_complete:
+            gen = self._cache
+        else:
+            gen = self
+
+        # Select the comparison function
+        if inc:
+            comp = lambda dc, dtc: dc >= dtc
+        else:
+            comp = lambda dc, dtc: dc > dtc
+
+        # Generate dates
+        n = 0
+        for d in gen:
+            if comp(d, dt):
+                if count is not None:
+                    n += 1
+                    if n > count:
+                        break
+
+                yield d
+
+    def between(self, after, before, inc=False, count=1):
+        """ Returns all the occurrences of the rrule between after and before.
+        The inc keyword defines what happens if after and/or before are
+        themselves occurrences. With inc=True, they will be included in the
+        list, if they are found in the recurrence set. """
+        if self._cache_complete:
+            gen = self._cache
+        else:
+            gen = self
+        started = False
+        l = []
+        if inc:
+            for i in gen:
+                if i > before:
+                    break
+                elif not started:
+                    if i >= after:
+                        started = True
+                        l.append(i)
+                else:
+                    l.append(i)
+        else:
+            for i in gen:
+                if i >= before:
+                    break
+                elif not started:
+                    if i > after:
+                        started = True
+                        l.append(i)
+                else:
+                    l.append(i)
+        return l
+
+
+class rrule(rrulebase):
+    """
+    That's the base of the rrule operation. It accepts all the keywords
+    defined in the RFC as its constructor parameters (except byday,
+    which was renamed to byweekday) and more. The constructor prototype is::
+
+            rrule(freq)
+
+    Where freq must be one of YEARLY, MONTHLY, WEEKLY, DAILY, HOURLY, MINUTELY,
+    or SECONDLY.
+
+    .. note::
+        Per RFC section 3.3.10, recurrence instances falling on invalid dates
+        and times are ignored rather than coerced:
+
+            Recurrence rules may generate recurrence instances with an invalid
+            date (e.g., February 30) or nonexistent local time (e.g., 1:30 AM
+            on a day where the local time is moved forward by an hour at 1:00
+            AM).  Such recurrence instances MUST be ignored and MUST NOT be
+            counted as part of the recurrence set.
+
+        This can lead to possibly surprising behavior when, for example, the
+        start date occurs at the end of the month:
+
+        >>> from dateutil.rrule import rrule, MONTHLY
+        >>> from datetime import datetime
+        >>> start_date = datetime(2014, 12, 31)
+        >>> list(rrule(freq=MONTHLY, count=4, dtstart=start_date))
+        ... # doctest: +NORMALIZE_WHITESPACE
+        [datetime.datetime(2014, 12, 31, 0, 0),
+         datetime.datetime(2015, 1, 31, 0, 0),
+         datetime.datetime(2015, 3, 31, 0, 0),
+         datetime.datetime(2015, 5, 31, 0, 0)]
+
+    Additionally, it supports the following keyword arguments:
+
+    :param dtstart:
+        The recurrence start. Besides being the base for the recurrence,
+        missing parameters in the final recurrence instances will also be
+        extracted from this date. If not given, datetime.now() will be used
+        instead.
+    :param interval:
+        The interval between each freq iteration. For example, when using
+        YEARLY, an interval of 2 means once every two years, but with HOURLY,
+        it means once every two hours. The default interval is 1.
+    :param wkst:
+        The week start day. Must be one of the MO, TU, WE constants, or an
+        integer, specifying the first day of the week. This will affect
+        recurrences based on weekly periods. The default week start is got
+        from calendar.firstweekday(), and may be modified by
+        calendar.setfirstweekday().
+    :param count:
+        If given, this determines how many occurrences will be generated.
+
+        .. note::
+            As of version 2.5.0, the use of the keyword ``until`` in conjunction
+            with ``count`` is deprecated, to make sure ``dateutil`` is fully
+            compliant with `RFC-5545 Sec. 3.3.10 <https://tools.ietf.org/
+            html/rfc5545#section-3.3.10>`_. Therefore, ``until`` and ``count``
+            **must not** occur in the same call to ``rrule``.
+    :param until:
+        If given, this must be a datetime instance specifying the upper-bound
+        limit of the recurrence. The last recurrence in the rule is the greatest
+        datetime that is less than or equal to the value specified in the
+        ``until`` parameter.
+
+        .. note::
+            As of version 2.5.0, the use of the keyword ``until`` in conjunction
+            with ``count`` is deprecated, to make sure ``dateutil`` is fully
+            compliant with `RFC-5545 Sec. 3.3.10 <https://tools.ietf.org/
+            html/rfc5545#section-3.3.10>`_. Therefore, ``until`` and ``count``
+            **must not** occur in the same call to ``rrule``.
+    :param bysetpos:
+        If given, it must be either an integer, or a sequence of integers,
+        positive or negative. Each given integer will specify an occurrence
+        number, corresponding to the nth occurrence of the rule inside the
+        frequency period. For example, a bysetpos of -1 if combined with a
+        MONTHLY frequency, and a byweekday of (MO, TU, WE, TH, FR), will
+        result in the last work day of every month.
+    :param bymonth:
+        If given, it must be either an integer, or a sequence of integers,
+        meaning the months to apply the recurrence to.
+    :param bymonthday:
+        If given, it must be either an integer, or a sequence of integers,
+        meaning the month days to apply the recurrence to.
+    :param byyearday:
+        If given, it must be either an integer, or a sequence of integers,
+        meaning the year days to apply the recurrence to.
+    :param byeaster:
+        If given, it must be either an integer, or a sequence of integers,
+        positive or negative. Each integer will define an offset from the
+        Easter Sunday. Passing the offset 0 to byeaster will yield the Easter
+        Sunday itself. This is an extension to the RFC specification.
+    :param byweekno:
+        If given, it must be either an integer, or a sequence of integers,
+        meaning the week numbers to apply the recurrence to. Week numbers
+        have the meaning described in ISO8601, that is, the first week of
+        the year is that containing at least four days of the new year.
+    :param byweekday:
+        If given, it must be either an integer (0 == MO), a sequence of
+        integers, one of the weekday constants (MO, TU, etc), or a sequence
+        of these constants. When given, these variables will define the
+        weekdays where the recurrence will be applied. It's also possible to
+        use an argument n for the weekday instances, which will mean the nth
+        occurrence of this weekday in the period. For example, with MONTHLY,
+        or with YEARLY and BYMONTH, using FR(+1) in byweekday will specify the
+        first friday of the month where the recurrence happens. Notice that in
+        the RFC documentation, this is specified as BYDAY, but was renamed to
+        avoid the ambiguity of that keyword.
+    :param byhour:
+        If given, it must be either an integer, or a sequence of integers,
+        meaning the hours to apply the recurrence to.
+    :param byminute:
+        If given, it must be either an integer, or a sequence of integers,
+        meaning the minutes to apply the recurrence to.
+    :param bysecond:
+        If given, it must be either an integer, or a sequence of integers,
+        meaning the seconds to apply the recurrence to.
+    :param cache:
+        If given, it must be a boolean value specifying to enable or disable
+        caching of results. If you will use the same rrule instance multiple
+        times, enabling caching will improve the performance considerably.
+     """
+    def __init__(self, freq, dtstart=None,
+                 interval=1, wkst=None, count=None, until=None, bysetpos=None,
+                 bymonth=None, bymonthday=None, byyearday=None, byeaster=None,
+                 byweekno=None, byweekday=None,
+                 byhour=None, byminute=None, bysecond=None,
+                 cache=False):
+        super(rrule, self).__init__(cache)
+        global easter
+        if not dtstart:
+            if until and until.tzinfo:
+                dtstart = datetime.datetime.now(tz=until.tzinfo).replace(microsecond=0)
+            else:
+                dtstart = datetime.datetime.now().replace(microsecond=0)
+        elif not isinstance(dtstart, datetime.datetime):
+            dtstart = datetime.datetime.fromordinal(dtstart.toordinal())
+        else:
+            dtstart = dtstart.replace(microsecond=0)
+        self._dtstart = dtstart
+        self._tzinfo = dtstart.tzinfo
+        self._freq = freq
+        self._interval = interval
+        self._count = count
+
+        # Cache the original byxxx rules, if they are provided, as the _byxxx
+        # attributes do not necessarily map to the inputs, and this can be
+        # a problem in generating the strings. Only store things if they've
+        # been supplied (the string retrieval will just use .get())
+        self._original_rule = {}
+
+        if until and not isinstance(until, datetime.datetime):
+            until = datetime.datetime.fromordinal(until.toordinal())
+        self._until = until
+
+        if self._dtstart and self._until:
+            if (self._dtstart.tzinfo is not None) != (self._until.tzinfo is not None):
+                # According to RFC5545 Section 3.3.10:
+                # https://tools.ietf.org/html/rfc5545#section-3.3.10
+                #
+                # > If the "DTSTART" property is specified as a date with UTC
+                # > time or a date with local time and time zone reference,
+                # > then the UNTIL rule part MUST be specified as a date with
+                # > UTC time.
+                raise ValueError(
+                    'RRULE UNTIL values must be specified in UTC when DTSTART '
+                    'is timezone-aware'
+                )
+
+        if count is not None and until:
+            warn("Using both 'count' and 'until' is inconsistent with RFC 5545"
+                 " and has been deprecated in dateutil. Future versions will "
+                 "raise an error.", DeprecationWarning)
+
+        if wkst is None:
+            self._wkst = calendar.firstweekday()
+        elif isinstance(wkst, integer_types):
+            self._wkst = wkst
+        else:
+            self._wkst = wkst.weekday
+
+        if bysetpos is None:
+            self._bysetpos = None
+        elif isinstance(bysetpos, integer_types):
+            if bysetpos == 0 or not (-366 <= bysetpos <= 366):
+                raise ValueError("bysetpos must be between 1 and 366, "
+                                 "or between -366 and -1")
+            self._bysetpos = (bysetpos,)
+        else:
+            self._bysetpos = tuple(bysetpos)
+            for pos in self._bysetpos:
+                if pos == 0 or not (-366 <= pos <= 366):
+                    raise ValueError("bysetpos must be between 1 and 366, "
+                                     "or between -366 and -1")
+
+        if self._bysetpos:
+            self._original_rule['bysetpos'] = self._bysetpos
+
+        if (byweekno is None and byyearday is None and bymonthday is None and
+                byweekday is None and byeaster is None):
+            if freq == YEARLY:
+                if bymonth is None:
+                    bymonth = dtstart.month
+                    self._original_rule['bymonth'] = None
+                bymonthday = dtstart.day
+                self._original_rule['bymonthday'] = None
+            elif freq == MONTHLY:
+                bymonthday = dtstart.day
+                self._original_rule['bymonthday'] = None
+            elif freq == WEEKLY:
+                byweekday = dtstart.weekday()
+                self._original_rule['byweekday'] = None
+
+        # bymonth
+        if bymonth is None:
+            self._bymonth = None
+        else:
+            if isinstance(bymonth, integer_types):
+                bymonth = (bymonth,)
+
+            self._bymonth = tuple(sorted(set(bymonth)))
+
+            if 'bymonth' not in self._original_rule:
+                self._original_rule['bymonth'] = self._bymonth
+
+        # byyearday
+        if byyearday is None:
+            self._byyearday = None
+        else:
+            if isinstance(byyearday, integer_types):
+                byyearday = (byyearday,)
+
+            self._byyearday = tuple(sorted(set(byyearday)))
+            self._original_rule['byyearday'] = self._byyearday
+
+        # byeaster
+        if byeaster is not None:
+            if not easter:
+                from dateutil import easter
+            if isinstance(byeaster, integer_types):
+                self._byeaster = (byeaster,)
+            else:
+                self._byeaster = tuple(sorted(byeaster))
+
+            self._original_rule['byeaster'] = self._byeaster
+        else:
+            self._byeaster = None
+
+        # bymonthday
+        if bymonthday is None:
+            self._bymonthday = ()
+            self._bynmonthday = ()
+        else:
+            if isinstance(bymonthday, integer_types):
+                bymonthday = (bymonthday,)
+
+            bymonthday = set(bymonthday)            # Ensure it's unique
+
+            self._bymonthday = tuple(sorted(x for x in bymonthday if x > 0))
+            self._bynmonthday = tuple(sorted(x for x in bymonthday if x < 0))
+
+            # Storing positive numbers first, then negative numbers
+            if 'bymonthday' not in self._original_rule:
+                self._original_rule['bymonthday'] = tuple(
+                    itertools.chain(self._bymonthday, self._bynmonthday))
+
+        # byweekno
+        if byweekno is None:
+            self._byweekno = None
+        else:
+            if isinstance(byweekno, integer_types):
+                byweekno = (byweekno,)
+
+            self._byweekno = tuple(sorted(set(byweekno)))
+
+            self._original_rule['byweekno'] = self._byweekno
+
+        # byweekday / bynweekday
+        if byweekday is None:
+            self._byweekday = None
+            self._bynweekday = None
+        else:
+            # If it's one of the valid non-sequence types, convert to a
+            # single-element sequence before the iterator that builds the
+            # byweekday set.
+            if isinstance(byweekday, integer_types) or hasattr(byweekday, "n"):
+                byweekday = (byweekday,)
+
+            self._byweekday = set()
+            self._bynweekday = set()
+            for wday in byweekday:
+                if isinstance(wday, integer_types):
+                    self._byweekday.add(wday)
+                elif not wday.n or freq > MONTHLY:
+                    self._byweekday.add(wday.weekday)
+                else:
+                    self._bynweekday.add((wday.weekday, wday.n))
+
+            if not self._byweekday:
+                self._byweekday = None
+            elif not self._bynweekday:
+                self._bynweekday = None
+
+            if self._byweekday is not None:
+                self._byweekday = tuple(sorted(self._byweekday))
+                orig_byweekday = [weekday(x) for x in self._byweekday]
+            else:
+                orig_byweekday = ()
+
+            if self._bynweekday is not None:
+                self._bynweekday = tuple(sorted(self._bynweekday))
+                orig_bynweekday = [weekday(*x) for x in self._bynweekday]
+            else:
+                orig_bynweekday = ()
+
+            if 'byweekday' not in self._original_rule:
+                self._original_rule['byweekday'] = tuple(itertools.chain(
+                    orig_byweekday, orig_bynweekday))
+
+        # byhour
+        if byhour is None:
+            if freq < HOURLY:
+                self._byhour = {dtstart.hour}
+            else:
+                self._byhour = None
+        else:
+            if isinstance(byhour, integer_types):
+                byhour = (byhour,)
+
+            if freq == HOURLY:
+                self._byhour = self.__construct_byset(start=dtstart.hour,
+                                                      byxxx=byhour,
+                                                      base=24)
+            else:
+                self._byhour = set(byhour)
+
+            self._byhour = tuple(sorted(self._byhour))
+            self._original_rule['byhour'] = self._byhour
+
+        # byminute
+        if byminute is None:
+            if freq < MINUTELY:
+                self._byminute = {dtstart.minute}
+            else:
+                self._byminute = None
+        else:
+            if isinstance(byminute, integer_types):
+                byminute = (byminute,)
+
+            if freq == MINUTELY:
+                self._byminute = self.__construct_byset(start=dtstart.minute,
+                                                        byxxx=byminute,
+                                                        base=60)
+            else:
+                self._byminute = set(byminute)
+
+            self._byminute = tuple(sorted(self._byminute))
+            self._original_rule['byminute'] = self._byminute
+
+        # bysecond
+        if bysecond is None:
+            if freq < SECONDLY:
+                self._bysecond = ((dtstart.second,))
+            else:
+                self._bysecond = None
+        else:
+            if isinstance(bysecond, integer_types):
+                bysecond = (bysecond,)
+
+            self._bysecond = set(bysecond)
+
+            if freq == SECONDLY:
+                self._bysecond = self.__construct_byset(start=dtstart.second,
+                                                        byxxx=bysecond,
+                                                        base=60)
+            else:
+                self._bysecond = set(bysecond)
+
+            self._bysecond = tuple(sorted(self._bysecond))
+            self._original_rule['bysecond'] = self._bysecond
+
+        if self._freq >= HOURLY:
+            self._timeset = None
+        else:
+            self._timeset = []
+            for hour in self._byhour:
+                for minute in self._byminute:
+                    for second in self._bysecond:
+                        self._timeset.append(
+                            datetime.time(hour, minute, second,
+                                          tzinfo=self._tzinfo))
+            self._timeset.sort()
+            self._timeset = tuple(self._timeset)
+
+    def __str__(self):
+        """
+        Output a string that would generate this RRULE if passed to rrulestr.
+        This is mostly compatible with RFC5545, except for the
+        dateutil-specific extension BYEASTER.
+        """
+
+        output = []
+        h, m, s = [None] * 3
+        if self._dtstart:
+            output.append(self._dtstart.strftime('DTSTART:%Y%m%dT%H%M%S'))
+            h, m, s = self._dtstart.timetuple()[3:6]
+
+        parts = ['FREQ=' + FREQNAMES[self._freq]]
+        if self._interval != 1:
+            parts.append('INTERVAL=' + str(self._interval))
+
+        if self._wkst:
+            parts.append('WKST=' + repr(weekday(self._wkst))[0:2])
+
+        if self._count is not None:
+            parts.append('COUNT=' + str(self._count))
+
+        if self._until:
+            parts.append(self._until.strftime('UNTIL=%Y%m%dT%H%M%S'))
+
+        if self._original_rule.get('byweekday') is not None:
+            # The str() method on weekday objects doesn't generate
+            # RFC5545-compliant strings, so we should modify that.
+            original_rule = dict(self._original_rule)
+            wday_strings = []
+            for wday in original_rule['byweekday']:
+                if wday.n:
+                    wday_strings.append('{n:+d}{wday}'.format(
+                        n=wday.n,
+                        wday=repr(wday)[0:2]))
+                else:
+                    wday_strings.append(repr(wday))
+
+            original_rule['byweekday'] = wday_strings
+        else:
+            original_rule = self._original_rule
+
+        partfmt = '{name}={vals}'
+        for name, key in [('BYSETPOS', 'bysetpos'),
+                          ('BYMONTH', 'bymonth'),
+                          ('BYMONTHDAY', 'bymonthday'),
+                          ('BYYEARDAY', 'byyearday'),
+                          ('BYWEEKNO', 'byweekno'),
+                          ('BYDAY', 'byweekday'),
+                          ('BYHOUR', 'byhour'),
+                          ('BYMINUTE', 'byminute'),
+                          ('BYSECOND', 'bysecond'),
+                          ('BYEASTER', 'byeaster')]:
+            value = original_rule.get(key)
+            if value:
+                parts.append(partfmt.format(name=name, vals=(','.join(str(v)
+                                                             for v in value))))
+
+        output.append('RRULE:' + ';'.join(parts))
+        return '\n'.join(output)
+
+    def replace(self, **kwargs):
+        """Return new rrule with same attributes except for those attributes given new
+           values by whichever keyword arguments are specified."""
+        new_kwargs = {"interval": self._interval,
+                      "count": self._count,
+                      "dtstart": self._dtstart,
+                      "freq": self._freq,
+                      "until": self._until,
+                      "wkst": self._wkst,
+                      "cache": False if self._cache is None else True }
+        new_kwargs.update(self._original_rule)
+        new_kwargs.update(kwargs)
+        return rrule(**new_kwargs)
+
+    def _iter(self):
+        year, month, day, hour, minute, second, weekday, yearday, _ = \
+            self._dtstart.timetuple()
+
+        # Some local variables to speed things up a bit
+        freq = self._freq
+        interval = self._interval
+        wkst = self._wkst
+        until = self._until
+        bymonth = self._bymonth
+        byweekno = self._byweekno
+        byyearday = self._byyearday
+        byweekday = self._byweekday
+        byeaster = self._byeaster
+        bymonthday = self._bymonthday
+        bynmonthday = self._bynmonthday
+        bysetpos = self._bysetpos
+        byhour = self._byhour
+        byminute = self._byminute
+        bysecond = self._bysecond
+
+        ii = _iterinfo(self)
+        ii.rebuild(year, month)
+
+        getdayset = {YEARLY: ii.ydayset,
+                     MONTHLY: ii.mdayset,
+                     WEEKLY: ii.wdayset,
+                     DAILY: ii.ddayset,
+                     HOURLY: ii.ddayset,
+                     MINUTELY: ii.ddayset,
+                     SECONDLY: ii.ddayset}[freq]
+
+        if freq < HOURLY:
+            timeset = self._timeset
+        else:
+            gettimeset = {HOURLY: ii.htimeset,
+                          MINUTELY: ii.mtimeset,
+                          SECONDLY: ii.stimeset}[freq]
+            if ((freq >= HOURLY and
+                 self._byhour and hour not in self._byhour) or
+                (freq >= MINUTELY and
+                 self._byminute and minute not in self._byminute) or
+                (freq >= SECONDLY and
+                 self._bysecond and second not in self._bysecond)):
+                timeset = ()
+            else:
+                timeset = gettimeset(hour, minute, second)
+
+        total = 0
+        count = self._count
+        while True:
+            # Get dayset with the right frequency
+            dayset, start, end = getdayset(year, month, day)
+
+            # Do the "hard" work ;-)
+            filtered = False
+            for i in dayset[start:end]:
+                if ((bymonth and ii.mmask[i] not in bymonth) or
+                    (byweekno and not ii.wnomask[i]) or
+                    (byweekday and ii.wdaymask[i] not in byweekday) or
+                    (ii.nwdaymask and not ii.nwdaymask[i]) or
+                    (byeaster and not ii.eastermask[i]) or
+                    ((bymonthday or bynmonthday) and
+                     ii.mdaymask[i] not in bymonthday and
+                     ii.nmdaymask[i] not in bynmonthday) or
+                    (byyearday and
+                     ((i < ii.yearlen and i+1 not in byyearday and
+                       -ii.yearlen+i not in byyearday) or
+                      (i >= ii.yearlen and i+1-ii.yearlen not in byyearday and
+                       -ii.nextyearlen+i-ii.yearlen not in byyearday)))):
+                    dayset[i] = None
+                    filtered = True
+
+            # Output results
+            if bysetpos and timeset:
+                poslist = []
+                for pos in bysetpos:
+                    if pos < 0:
+                        daypos, timepos = divmod(pos, len(timeset))
+                    else:
+                        daypos, timepos = divmod(pos-1, len(timeset))
+                    try:
+                        i = [x for x in dayset[start:end]
+                             if x is not None][daypos]
+                        time = timeset[timepos]
+                    except IndexError:
+                        pass
+                    else:
+                        date = datetime.date.fromordinal(ii.yearordinal+i)
+                        res = datetime.datetime.combine(date, time)
+                        if res not in poslist:
+                            poslist.append(res)
+                poslist.sort()
+                for res in poslist:
+                    if until and res > until:
+                        self._len = total
+                        return
+                    elif res >= self._dtstart:
+                        if count is not None:
+                            count -= 1
+                            if count < 0:
+                                self._len = total
+                                return
+                        total += 1
+                        yield res
+            else:
+                for i in dayset[start:end]:
+                    if i is not None:
+                        date = datetime.date.fromordinal(ii.yearordinal + i)
+                        for time in timeset:
+                            res = datetime.datetime.combine(date, time)
+                            if until and res > until:
+                                self._len = total
+                                return
+                            elif res >= self._dtstart:
+                                if count is not None:
+                                    count -= 1
+                                    if count < 0:
+                                        self._len = total
+                                        return
+
+                                total += 1
+                                yield res
+
+            # Handle frequency and interval
+            fixday = False
+            if freq == YEARLY:
+                year += interval
+                if year > datetime.MAXYEAR:
+                    self._len = total
+                    return
+                ii.rebuild(year, month)
+            elif freq == MONTHLY:
+                month += interval
+                if month > 12:
+                    div, mod = divmod(month, 12)
+                    month = mod
+                    year += div
+                    if month == 0:
+                        month = 12
+                        year -= 1
+                    if year > datetime.MAXYEAR:
+                        self._len = total
+                        return
+                ii.rebuild(year, month)
+            elif freq == WEEKLY:
+                if wkst > weekday:
+                    day += -(weekday+1+(6-wkst))+self._interval*7
+                else:
+                    day += -(weekday-wkst)+self._interval*7
+                weekday = wkst
+                fixday = True
+            elif freq == DAILY:
+                day += interval
+                fixday = True
+            elif freq == HOURLY:
+                if filtered:
+                    # Jump to one iteration before next day
+                    hour += ((23-hour)//interval)*interval
+
+                if byhour:
+                    ndays, hour = self.__mod_distance(value=hour,
+                                                      byxxx=self._byhour,
+                                                      base=24)
+                else:
+                    ndays, hour = divmod(hour+interval, 24)
+
+                if ndays:
+                    day += ndays
+                    fixday = True
+
+                timeset = gettimeset(hour, minute, second)
+            elif freq == MINUTELY:
+                if filtered:
+                    # Jump to one iteration before next day
+                    minute += ((1439-(hour*60+minute))//interval)*interval
+
+                valid = False
+                rep_rate = (24*60)
+                for j in range(rep_rate // gcd(interval, rep_rate)):
+                    if byminute:
+                        nhours, minute = \
+                            self.__mod_distance(value=minute,
+                                                byxxx=self._byminute,
+                                                base=60)
+                    else:
+                        nhours, minute = divmod(minute+interval, 60)
+
+                    div, hour = divmod(hour+nhours, 24)
+                    if div:
+                        day += div
+                        fixday = True
+                        filtered = False
+
+                    if not byhour or hour in byhour:
+                        valid = True
+                        break
+
+                if not valid:
+                    raise ValueError('Invalid combination of interval and ' +
+                                     'byhour resulting in empty rule.')
+
+                timeset = gettimeset(hour, minute, second)
+            elif freq == SECONDLY:
+                if filtered:
+                    # Jump to one iteration before next day
+                    second += (((86399 - (hour * 3600 + minute * 60 + second))
+                                // interval) * interval)
+
+                rep_rate = (24 * 3600)
+                valid = False
+                for j in range(0, rep_rate // gcd(interval, rep_rate)):
+                    if bysecond:
+                        nminutes, second = \
+                            self.__mod_distance(value=second,
+                                                byxxx=self._bysecond,
+                                                base=60)
+                    else:
+                        nminutes, second = divmod(second+interval, 60)
+
+                    div, minute = divmod(minute+nminutes, 60)
+                    if div:
+                        hour += div
+                        div, hour = divmod(hour, 24)
+                        if div:
+                            day += div
+                            fixday = True
+
+                    if ((not byhour or hour in byhour) and
+                            (not byminute or minute in byminute) and
+                            (not bysecond or second in bysecond)):
+                        valid = True
+                        break
+
+                if not valid:
+                    raise ValueError('Invalid combination of interval, ' +
+                                     'byhour and byminute resulting in empty' +
+                                     ' rule.')
+
+                timeset = gettimeset(hour, minute, second)
+
+            if fixday and day > 28:
+                daysinmonth = calendar.monthrange(year, month)[1]
+                if day > daysinmonth:
+                    while day > daysinmonth:
+                        day -= daysinmonth
+                        month += 1
+                        if month == 13:
+                            month = 1
+                            year += 1
+                            if year > datetime.MAXYEAR:
+                                self._len = total
+                                return
+                        daysinmonth = calendar.monthrange(year, month)[1]
+                    ii.rebuild(year, month)
+
+    def __construct_byset(self, start, byxxx, base):
+        """
+        If a `BYXXX` sequence is passed to the constructor at the same level as
+        `FREQ` (e.g. `FREQ=HOURLY,BYHOUR={2,4,7},INTERVAL=3`), there are some
+        specifications which cannot be reached given some starting conditions.
+
+        This occurs whenever the interval is not coprime with the base of a
+        given unit and the difference between the starting position and the
+        ending position is not coprime with the greatest common denominator
+        between the interval and the base. For example, with a FREQ of hourly
+        starting at 17:00 and an interval of 4, the only valid values for
+        BYHOUR would be {21, 1, 5, 9, 13, 17}, because 4 and 24 are not
+        coprime.
+
+        :param start:
+            Specifies the starting position.
+        :param byxxx:
+            An iterable containing the list of allowed values.
+        :param base:
+            The largest allowable value for the specified frequency (e.g.
+            24 hours, 60 minutes).
+
+        This does not preserve the type of the iterable, returning a set, since
+        the values should be unique and the order is irrelevant, this will
+        speed up later lookups.
+
+        In the event of an empty set, raises a :exception:`ValueError`, as this
+        results in an empty rrule.
+        """
+
+        cset = set()
+
+        # Support a single byxxx value.
+        if isinstance(byxxx, integer_types):
+            byxxx = (byxxx, )
+
+        for num in byxxx:
+            i_gcd = gcd(self._interval, base)
+            # Use divmod rather than % because we need to wrap negative nums.
+            if i_gcd == 1 or divmod(num - start, i_gcd)[1] == 0:
+                cset.add(num)
+
+        if len(cset) == 0:
+            raise ValueError("Invalid rrule byxxx generates an empty set.")
+
+        return cset
+
+    def __mod_distance(self, value, byxxx, base):
+        """
+        Calculates the next value in a sequence where the `FREQ` parameter is
+        specified along with a `BYXXX` parameter at the same "level"
+        (e.g. `HOURLY` specified with `BYHOUR`).
+
+        :param value:
+            The old value of the component.
+        :param byxxx:
+            The `BYXXX` set, which should have been generated by
+            `rrule._construct_byset`, or something else which checks that a
+            valid rule is present.
+        :param base:
+            The largest allowable value for the specified frequency (e.g.
+            24 hours, 60 minutes).
+
+        If a valid value is not found after `base` iterations (the maximum
+        number before the sequence would start to repeat), this raises a
+        :exception:`ValueError`, as no valid values were found.
+
+        This returns a tuple of `divmod(n*interval, base)`, where `n` is the
+        smallest number of `interval` repetitions until the next specified
+        value in `byxxx` is found.
+        """
+        accumulator = 0
+        for ii in range(1, base + 1):
+            # Using divmod() over % to account for negative intervals
+            div, value = divmod(value + self._interval, base)
+            accumulator += div
+            if value in byxxx:
+                return (accumulator, value)
+
+
+class _iterinfo(object):
+    __slots__ = ["rrule", "lastyear", "lastmonth",
+                 "yearlen", "nextyearlen", "yearordinal", "yearweekday",
+                 "mmask", "mrange", "mdaymask", "nmdaymask",
+                 "wdaymask", "wnomask", "nwdaymask", "eastermask"]
+
+    def __init__(self, rrule):
+        for attr in self.__slots__:
+            setattr(self, attr, None)
+        self.rrule = rrule
+
+    def rebuild(self, year, month):
+        # Every mask is 7 days longer to handle cross-year weekly periods.
+        rr = self.rrule
+        if year != self.lastyear:
+            self.yearlen = 365 + calendar.isleap(year)
+            self.nextyearlen = 365 + calendar.isleap(year + 1)
+            firstyday = datetime.date(year, 1, 1)
+            self.yearordinal = firstyday.toordinal()
+            self.yearweekday = firstyday.weekday()
+
+            wday = datetime.date(year, 1, 1).weekday()
+            if self.yearlen == 365:
+                self.mmask = M365MASK
+                self.mdaymask = MDAY365MASK
+                self.nmdaymask = NMDAY365MASK
+                self.wdaymask = WDAYMASK[wday:]
+                self.mrange = M365RANGE
+            else:
+                self.mmask = M366MASK
+                self.mdaymask = MDAY366MASK
+                self.nmdaymask = NMDAY366MASK
+                self.wdaymask = WDAYMASK[wday:]
+                self.mrange = M366RANGE
+
+            if not rr._byweekno:
+                self.wnomask = None
+            else:
+                self.wnomask = [0]*(self.yearlen+7)
+                # no1wkst = firstwkst = self.wdaymask.index(rr._wkst)
+                no1wkst = firstwkst = (7-self.yearweekday+rr._wkst) % 7
+                if no1wkst >= 4:
+                    no1wkst = 0
+                    # Number of days in the year, plus the days we got
+                    # from last year.
+                    wyearlen = self.yearlen+(self.yearweekday-rr._wkst) % 7
+                else:
+                    # Number of days in the year, minus the days we
+                    # left in last year.
+                    wyearlen = self.yearlen-no1wkst
+                div, mod = divmod(wyearlen, 7)
+                numweeks = div+mod//4
+                for n in rr._byweekno:
+                    if n < 0:
+                        n += numweeks+1
+                    if not (0 < n <= numweeks):
+                        continue
+                    if n > 1:
+                        i = no1wkst+(n-1)*7
+                        if no1wkst != firstwkst:
+                            i -= 7-firstwkst
+                    else:
+                        i = no1wkst
+                    for j in range(7):
+                        self.wnomask[i] = 1
+                        i += 1
+                        if self.wdaymask[i] == rr._wkst:
+                            break
+                if 1 in rr._byweekno:
+                    # Check week number 1 of next year as well
+                    # TODO: Check -numweeks for next year.
+                    i = no1wkst+numweeks*7
+                    if no1wkst != firstwkst:
+                        i -= 7-firstwkst
+                    if i < self.yearlen:
+                        # If week starts in next year, we
+                        # don't care about it.
+                        for j in range(7):
+                            self.wnomask[i] = 1
+                            i += 1
+                            if self.wdaymask[i] == rr._wkst:
+                                break
+                if no1wkst:
+                    # Check last week number of last year as
+                    # well. If no1wkst is 0, either the year
+                    # started on week start, or week number 1
+                    # got days from last year, so there are no
+                    # days from last year's last week number in
+                    # this year.
+                    if -1 not in rr._byweekno:
+                        lyearweekday = datetime.date(year-1, 1, 1).weekday()
+                        lno1wkst = (7-lyearweekday+rr._wkst) % 7
+                        lyearlen = 365+calendar.isleap(year-1)
+                        if lno1wkst >= 4:
+                            lno1wkst = 0
+                            lnumweeks = 52+(lyearlen +
+                                            (lyearweekday-rr._wkst) % 7) % 7//4
+                        else:
+                            lnumweeks = 52+(self.yearlen-no1wkst) % 7//4
+                    else:
+                        lnumweeks = -1
+                    if lnumweeks in rr._byweekno:
+                        for i in range(no1wkst):
+                            self.wnomask[i] = 1
+
+        if (rr._bynweekday and (month != self.lastmonth or
+                                year != self.lastyear)):
+            ranges = []
+            if rr._freq == YEARLY:
+                if rr._bymonth:
+                    for month in rr._bymonth:
+                        ranges.append(self.mrange[month-1:month+1])
+                else:
+                    ranges = [(0, self.yearlen)]
+            elif rr._freq == MONTHLY:
+                ranges = [self.mrange[month-1:month+1]]
+            if ranges:
+                # Weekly frequency won't get here, so we may not
+                # care about cross-year weekly periods.
+                self.nwdaymask = [0]*self.yearlen
+                for first, last in ranges:
+                    last -= 1
+                    for wday, n in rr._bynweekday:
+                        if n < 0:
+                            i = last+(n+1)*7
+                            i -= (self.wdaymask[i]-wday) % 7
+                        else:
+                            i = first+(n-1)*7
+                            i += (7-self.wdaymask[i]+wday) % 7
+                        if first <= i <= last:
+                            self.nwdaymask[i] = 1
+
+        if rr._byeaster:
+            self.eastermask = [0]*(self.yearlen+7)
+            eyday = easter.easter(year).toordinal()-self.yearordinal
+            for offset in rr._byeaster:
+                self.eastermask[eyday+offset] = 1
+
+        self.lastyear = year
+        self.lastmonth = month
+
+    def ydayset(self, year, month, day):
+        return list(range(self.yearlen)), 0, self.yearlen
+
+    def mdayset(self, year, month, day):
+        dset = [None]*self.yearlen
+        start, end = self.mrange[month-1:month+1]
+        for i in range(start, end):
+            dset[i] = i
+        return dset, start, end
+
+    def wdayset(self, year, month, day):
+        # We need to handle cross-year weeks here.
+        dset = [None]*(self.yearlen+7)
+        i = datetime.date(year, month, day).toordinal()-self.yearordinal
+        start = i
+        for j in range(7):
+            dset[i] = i
+            i += 1
+            # if (not (0 <= i < self.yearlen) or
+            #    self.wdaymask[i] == self.rrule._wkst):
+            # This will cross the year boundary, if necessary.
+            if self.wdaymask[i] == self.rrule._wkst:
+                break
+        return dset, start, i
+
+    def ddayset(self, year, month, day):
+        dset = [None] * self.yearlen
+        i = datetime.date(year, month, day).toordinal() - self.yearordinal
+        dset[i] = i
+        return dset, i, i + 1
+
+    def htimeset(self, hour, minute, second):
+        tset = []
+        rr = self.rrule
+        for minute in rr._byminute:
+            for second in rr._bysecond:
+                tset.append(datetime.time(hour, minute, second,
+                                          tzinfo=rr._tzinfo))
+        tset.sort()
+        return tset
+
+    def mtimeset(self, hour, minute, second):
+        tset = []
+        rr = self.rrule
+        for second in rr._bysecond:
+            tset.append(datetime.time(hour, minute, second, tzinfo=rr._tzinfo))
+        tset.sort()
+        return tset
+
+    def stimeset(self, hour, minute, second):
+        return (datetime.time(hour, minute, second,
+                tzinfo=self.rrule._tzinfo),)
+
+
+class rruleset(rrulebase):
+    """ The rruleset type allows more complex recurrence setups, mixing
+    multiple rules, dates, exclusion rules, and exclusion dates. The type
+    constructor takes the following keyword arguments:
+
+    :param cache: If True, caching of results will be enabled, improving
+                  performance of multiple queries considerably. """
+
+    class _genitem(object):
+        def __init__(self, genlist, gen):
+            try:
+                self.dt = advance_iterator(gen)
+                genlist.append(self)
+            except StopIteration:
+                pass
+            self.genlist = genlist
+            self.gen = gen
+
+        def __next__(self):
+            try:
+                self.dt = advance_iterator(self.gen)
+            except StopIteration:
+                if self.genlist[0] is self:
+                    heapq.heappop(self.genlist)
+                else:
+                    self.genlist.remove(self)
+                    heapq.heapify(self.genlist)
+
+        next = __next__
+
+        def __lt__(self, other):
+            return self.dt < other.dt
+
+        def __gt__(self, other):
+            return self.dt > other.dt
+
+        def __eq__(self, other):
+            return self.dt == other.dt
+
+        def __ne__(self, other):
+            return self.dt != other.dt
+
+    def __init__(self, cache=False):
+        super(rruleset, self).__init__(cache)
+        self._rrule = []
+        self._rdate = []
+        self._exrule = []
+        self._exdate = []
+
+    @_invalidates_cache
+    def rrule(self, rrule):
+        """ Include the given :py:class:`rrule` instance in the recurrence set
+            generation. """
+        self._rrule.append(rrule)
+
+    @_invalidates_cache
+    def rdate(self, rdate):
+        """ Include the given :py:class:`datetime` instance in the recurrence
+            set generation. """
+        self._rdate.append(rdate)
+
+    @_invalidates_cache
+    def exrule(self, exrule):
+        """ Include the given rrule instance in the recurrence set exclusion
+            list. Dates which are part of the given recurrence rules will not
+            be generated, even if some inclusive rrule or rdate matches them.
+        """
+        self._exrule.append(exrule)
+
+    @_invalidates_cache
+    def exdate(self, exdate):
+        """ Include the given datetime instance in the recurrence set
+            exclusion list. Dates included that way will not be generated,
+            even if some inclusive rrule or rdate matches them. """
+        self._exdate.append(exdate)
+
+    def _iter(self):
+        rlist = []
+        self._rdate.sort()
+        self._genitem(rlist, iter(self._rdate))
+        for gen in [iter(x) for x in self._rrule]:
+            self._genitem(rlist, gen)
+        exlist = []
+        self._exdate.sort()
+        self._genitem(exlist, iter(self._exdate))
+        for gen in [iter(x) for x in self._exrule]:
+            self._genitem(exlist, gen)
+        lastdt = None
+        total = 0
+        heapq.heapify(rlist)
+        heapq.heapify(exlist)
+        while rlist:
+            ritem = rlist[0]
+            if not lastdt or lastdt != ritem.dt:
+                while exlist and exlist[0] < ritem:
+                    exitem = exlist[0]
+                    advance_iterator(exitem)
+                    if exlist and exlist[0] is exitem:
+                        heapq.heapreplace(exlist, exitem)
+                if not exlist or ritem != exlist[0]:
+                    total += 1
+                    yield ritem.dt
+                lastdt = ritem.dt
+            advance_iterator(ritem)
+            if rlist and rlist[0] is ritem:
+                heapq.heapreplace(rlist, ritem)
+        self._len = total
+
+
+
+
+class _rrulestr(object):
+    """ Parses a string representation of a recurrence rule or set of
+    recurrence rules.
+
+    :param s:
+        Required, a string defining one or more recurrence rules.
+
+    :param dtstart:
+        If given, used as the default recurrence start if not specified in the
+        rule string.
+
+    :param cache:
+        If set ``True`` caching of results will be enabled, improving
+        performance of multiple queries considerably.
+
+    :param unfold:
+        If set ``True`` indicates that a rule string is split over more
+        than one line and should be joined before processing.
+
+    :param forceset:
+        If set ``True`` forces a :class:`dateutil.rrule.rruleset` to
+        be returned.
+
+    :param compatible:
+        If set ``True`` forces ``unfold`` and ``forceset`` to be ``True``.
+
+    :param ignoretz:
+        If set ``True``, time zones in parsed strings are ignored and a naive
+        :class:`datetime.datetime` object is returned.
+
+    :param tzids:
+        If given, a callable or mapping used to retrieve a
+        :class:`datetime.tzinfo` from a string representation.
+        Defaults to :func:`dateutil.tz.gettz`.
+
+    :param tzinfos:
+        Additional time zone names / aliases which may be present in a string
+        representation.  See :func:`dateutil.parser.parse` for more
+        information.
+
+    :return:
+        Returns a :class:`dateutil.rrule.rruleset` or
+        :class:`dateutil.rrule.rrule`
+    """
+
+    _freq_map = {"YEARLY": YEARLY,
+                 "MONTHLY": MONTHLY,
+                 "WEEKLY": WEEKLY,
+                 "DAILY": DAILY,
+                 "HOURLY": HOURLY,
+                 "MINUTELY": MINUTELY,
+                 "SECONDLY": SECONDLY}
+
+    _weekday_map = {"MO": 0, "TU": 1, "WE": 2, "TH": 3,
+                    "FR": 4, "SA": 5, "SU": 6}
+
+    def _handle_int(self, rrkwargs, name, value, **kwargs):
+        rrkwargs[name.lower()] = int(value)
+
+    def _handle_int_list(self, rrkwargs, name, value, **kwargs):
+        rrkwargs[name.lower()] = [int(x) for x in value.split(',')]
+
+    _handle_INTERVAL = _handle_int
+    _handle_COUNT = _handle_int
+    _handle_BYSETPOS = _handle_int_list
+    _handle_BYMONTH = _handle_int_list
+    _handle_BYMONTHDAY = _handle_int_list
+    _handle_BYYEARDAY = _handle_int_list
+    _handle_BYEASTER = _handle_int_list
+    _handle_BYWEEKNO = _handle_int_list
+    _handle_BYHOUR = _handle_int_list
+    _handle_BYMINUTE = _handle_int_list
+    _handle_BYSECOND = _handle_int_list
+
+    def _handle_FREQ(self, rrkwargs, name, value, **kwargs):
+        rrkwargs["freq"] = self._freq_map[value]
+
+    def _handle_UNTIL(self, rrkwargs, name, value, **kwargs):
+        global parser
+        if not parser:
+            from dateutil import parser
+        try:
+            rrkwargs["until"] = parser.parse(value,
+                                             ignoretz=kwargs.get("ignoretz"),
+                                             tzinfos=kwargs.get("tzinfos"))
+        except ValueError:
+            raise ValueError("invalid until date")
+
+    def _handle_WKST(self, rrkwargs, name, value, **kwargs):
+        rrkwargs["wkst"] = self._weekday_map[value]
+
+    def _handle_BYWEEKDAY(self, rrkwargs, name, value, **kwargs):
+        """
+        Two ways to specify this: +1MO or MO(+1)
+        """
+        l = []
+        for wday in value.split(','):
+            if '(' in wday:
+                # If it's of the form TH(+1), etc.
+                splt = wday.split('(')
+                w = splt[0]
+                n = int(splt[1][:-1])
+            elif len(wday):
+                # If it's of the form +1MO
+                for i in range(len(wday)):
+                    if wday[i] not in '+-0123456789':
+                        break
+                n = wday[:i] or None
+                w = wday[i:]
+                if n:
+                    n = int(n)
+            else:
+                raise ValueError("Invalid (empty) BYDAY specification.")
+
+            l.append(weekdays[self._weekday_map[w]](n))
+        rrkwargs["byweekday"] = l
+
+    _handle_BYDAY = _handle_BYWEEKDAY
+
+    def _parse_rfc_rrule(self, line,
+                         dtstart=None,
+                         cache=False,
+                         ignoretz=False,
+                         tzinfos=None):
+        if line.find(':') != -1:
+            name, value = line.split(':')
+            if name != "RRULE":
+                raise ValueError("unknown parameter name")
+        else:
+            value = line
+        rrkwargs = {}
+        for pair in value.split(';'):
+            name, value = pair.split('=')
+            name = name.upper()
+            value = value.upper()
+            try:
+                getattr(self, "_handle_"+name)(rrkwargs, name, value,
+                                               ignoretz=ignoretz,
+                                               tzinfos=tzinfos)
+            except AttributeError:
+                raise ValueError("unknown parameter '%s'" % name)
+            except (KeyError, ValueError):
+                raise ValueError("invalid '%s': %s" % (name, value))
+        return rrule(dtstart=dtstart, cache=cache, **rrkwargs)
+
+    def _parse_date_value(self, date_value, parms, rule_tzids,
+                          ignoretz, tzids, tzinfos):
+        global parser
+        if not parser:
+            from dateutil import parser
+
+        datevals = []
+        value_found = False
+        TZID = None
+
+        for parm in parms:
+            if parm.startswith("TZID="):
+                try:
+                    tzkey = rule_tzids[parm.split('TZID=')[-1]]
+                except KeyError:
+                    continue
+                if tzids is None:
+                    from . import tz
+                    tzlookup = tz.gettz
+                elif callable(tzids):
+                    tzlookup = tzids
+                else:
+                    tzlookup = getattr(tzids, 'get', None)
+                    if tzlookup is None:
+                        msg = ('tzids must be a callable, mapping, or None, '
+                               'not %s' % tzids)
+                        raise ValueError(msg)
+
+                TZID = tzlookup(tzkey)
+                continue
+
+            # RFC 5445 3.8.2.4: The VALUE parameter is optional, but may be found
+            # only once.
+            if parm not in {"VALUE=DATE-TIME", "VALUE=DATE"}:
+                raise ValueError("unsupported parm: " + parm)
+            else:
+                if value_found:
+                    msg = ("Duplicate value parameter found in: " + parm)
+                    raise ValueError(msg)
+                value_found = True
+
+        for datestr in date_value.split(','):
+            date = parser.parse(datestr, ignoretz=ignoretz, tzinfos=tzinfos)
+            if TZID is not None:
+                if date.tzinfo is None:
+                    date = date.replace(tzinfo=TZID)
+                else:
+                    raise ValueError('DTSTART/EXDATE specifies multiple timezone')
+            datevals.append(date)
+
+        return datevals
+
+    def _parse_rfc(self, s,
+                   dtstart=None,
+                   cache=False,
+                   unfold=False,
+                   forceset=False,
+                   compatible=False,
+                   ignoretz=False,
+                   tzids=None,
+                   tzinfos=None):
+        global parser
+        if compatible:
+            forceset = True
+            unfold = True
+
+        TZID_NAMES = dict(map(
+            lambda x: (x.upper(), x),
+            re.findall('TZID=(?P<name>[^:]+):', s)
+        ))
+        s = s.upper()
+        if not s.strip():
+            raise ValueError("empty string")
+        if unfold:
+            lines = s.splitlines()
+            i = 0
+            while i < len(lines):
+                line = lines[i].rstrip()
+                if not line:
+                    del lines[i]
+                elif i > 0 and line[0] == " ":
+                    lines[i-1] += line[1:]
+                    del lines[i]
+                else:
+                    i += 1
+        else:
+            lines = s.split()
+        if (not forceset and len(lines) == 1 and (s.find(':') == -1 or
+                                                  s.startswith('RRULE:'))):
+            return self._parse_rfc_rrule(lines[0], cache=cache,
+                                         dtstart=dtstart, ignoretz=ignoretz,
+                                         tzinfos=tzinfos)
+        else:
+            rrulevals = []
+            rdatevals = []
+            exrulevals = []
+            exdatevals = []
+            for line in lines:
+                if not line:
+                    continue
+                if line.find(':') == -1:
+                    name = "RRULE"
+                    value = line
+                else:
+                    name, value = line.split(':', 1)
+                parms = name.split(';')
+                if not parms:
+                    raise ValueError("empty property name")
+                name = parms[0]
+                parms = parms[1:]
+                if name == "RRULE":
+                    for parm in parms:
+                        raise ValueError("unsupported RRULE parm: "+parm)
+                    rrulevals.append(value)
+                elif name == "RDATE":
+                    for parm in parms:
+                        if parm != "VALUE=DATE-TIME":
+                            raise ValueError("unsupported RDATE parm: "+parm)
+                    rdatevals.append(value)
+                elif name == "EXRULE":
+                    for parm in parms:
+                        raise ValueError("unsupported EXRULE parm: "+parm)
+                    exrulevals.append(value)
+                elif name == "EXDATE":
+                    exdatevals.extend(
+                        self._parse_date_value(value, parms,
+                                               TZID_NAMES, ignoretz,
+                                               tzids, tzinfos)
+                    )
+                elif name == "DTSTART":
+                    dtvals = self._parse_date_value(value, parms, TZID_NAMES,
+                                                    ignoretz, tzids, tzinfos)
+                    if len(dtvals) != 1:
+                        raise ValueError("Multiple DTSTART values specified:" +
+                                         value)
+                    dtstart = dtvals[0]
+                else:
+                    raise ValueError("unsupported property: "+name)
+            if (forceset or len(rrulevals) > 1 or rdatevals
+                    or exrulevals or exdatevals):
+                if not parser and (rdatevals or exdatevals):
+                    from dateutil import parser
+                rset = rruleset(cache=cache)
+                for value in rrulevals:
+                    rset.rrule(self._parse_rfc_rrule(value, dtstart=dtstart,
+                                                     ignoretz=ignoretz,
+                                                     tzinfos=tzinfos))
+                for value in rdatevals:
+                    for datestr in value.split(','):
+                        rset.rdate(parser.parse(datestr,
+                                                ignoretz=ignoretz,
+                                                tzinfos=tzinfos))
+                for value in exrulevals:
+                    rset.exrule(self._parse_rfc_rrule(value, dtstart=dtstart,
+                                                      ignoretz=ignoretz,
+                                                      tzinfos=tzinfos))
+                for value in exdatevals:
+                    rset.exdate(value)
+                if compatible and dtstart:
+                    rset.rdate(dtstart)
+                return rset
+            else:
+                return self._parse_rfc_rrule(rrulevals[0],
+                                             dtstart=dtstart,
+                                             cache=cache,
+                                             ignoretz=ignoretz,
+                                             tzinfos=tzinfos)
+
+    def __call__(self, s, **kwargs):
+        return self._parse_rfc(s, **kwargs)
+
+
+rrulestr = _rrulestr()
+
+# vim:ts=4:sw=4:et