Home | History | Annotate | Download | only in dateutil
      1 # -*- coding: utf-8 -*-
      2 """
      3 The rrule module offers a small, complete, and very fast, implementation of
      4 the recurrence rules documented in the
      5 `iCalendar RFC <https://tools.ietf.org/html/rfc5545>`_,
      6 including support for caching of results.
      7 """
      8 import itertools
      9 import datetime
     10 import calendar
     11 import re
     12 import sys
     13 
     14 try:
     15     from math import gcd
     16 except ImportError:
     17     from fractions import gcd
     18 
     19 from six import advance_iterator, integer_types
     20 from six.moves import _thread, range
     21 import heapq
     22 
     23 from ._common import weekday as weekdaybase
     24 from .tz import tzutc, tzlocal
     25 
     26 # For warning about deprecation of until and count
     27 from warnings import warn
     28 
     29 __all__ = ["rrule", "rruleset", "rrulestr",
     30            "YEARLY", "MONTHLY", "WEEKLY", "DAILY",
     31            "HOURLY", "MINUTELY", "SECONDLY",
     32            "MO", "TU", "WE", "TH", "FR", "SA", "SU"]
     33 
     34 # Every mask is 7 days longer to handle cross-year weekly periods.
     35 M366MASK = tuple([1]*31+[2]*29+[3]*31+[4]*30+[5]*31+[6]*30 +
     36                  [7]*31+[8]*31+[9]*30+[10]*31+[11]*30+[12]*31+[1]*7)
     37 M365MASK = list(M366MASK)
     38 M29, M30, M31 = list(range(1, 30)), list(range(1, 31)), list(range(1, 32))
     39 MDAY366MASK = tuple(M31+M29+M31+M30+M31+M30+M31+M31+M30+M31+M30+M31+M31[:7])
     40 MDAY365MASK = list(MDAY366MASK)
     41 M29, M30, M31 = list(range(-29, 0)), list(range(-30, 0)), list(range(-31, 0))
     42 NMDAY366MASK = tuple(M31+M29+M31+M30+M31+M30+M31+M31+M30+M31+M30+M31+M31[:7])
     43 NMDAY365MASK = list(NMDAY366MASK)
     44 M366RANGE = (0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366)
     45 M365RANGE = (0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365)
     46 WDAYMASK = [0, 1, 2, 3, 4, 5, 6]*55
     47 del M29, M30, M31, M365MASK[59], MDAY365MASK[59], NMDAY365MASK[31]
     48 MDAY365MASK = tuple(MDAY365MASK)
     49 M365MASK = tuple(M365MASK)
     50 
     51 FREQNAMES = ['YEARLY', 'MONTHLY', 'WEEKLY', 'DAILY', 'HOURLY', 'MINUTELY', 'SECONDLY']
     52 
     53 (YEARLY,
     54  MONTHLY,
     55  WEEKLY,
     56  DAILY,
     57  HOURLY,
     58  MINUTELY,
     59  SECONDLY) = list(range(7))
     60 
     61 # Imported on demand.
     62 easter = None
     63 parser = None
     64 
     65 
     66 class weekday(weekdaybase):
     67     """
     68     This version of weekday does not allow n = 0.
     69     """
     70     def __init__(self, wkday, n=None):
     71         if n == 0:
     72             raise ValueError("Can't create weekday with n==0")
     73 
     74         super(weekday, self).__init__(wkday, n)
     75 
     76 
     77 MO, TU, WE, TH, FR, SA, SU = weekdays = tuple(weekday(x) for x in range(7))
     78 
     79 
     80 def _invalidates_cache(f):
     81     """
     82     Decorator for rruleset methods which may invalidate the
     83     cached length.
     84     """
     85     def inner_func(self, *args, **kwargs):
     86         rv = f(self, *args, **kwargs)
     87         self._invalidate_cache()
     88         return rv
     89 
     90     return inner_func
     91 
     92 
     93 class rrulebase(object):
     94     def __init__(self, cache=False):
     95         if cache:
     96             self._cache = []
     97             self._cache_lock = _thread.allocate_lock()
     98             self._invalidate_cache()
     99         else:
    100             self._cache = None
    101             self._cache_complete = False
    102             self._len = None
    103 
    104     def __iter__(self):
    105         if self._cache_complete:
    106             return iter(self._cache)
    107         elif self._cache is None:
    108             return self._iter()
    109         else:
    110             return self._iter_cached()
    111 
    112     def _invalidate_cache(self):
    113         if self._cache is not None:
    114             self._cache = []
    115             self._cache_complete = False
    116             self._cache_gen = self._iter()
    117 
    118             if self._cache_lock.locked():
    119                 self._cache_lock.release()
    120 
    121         self._len = None
    122 
    123     def _iter_cached(self):
    124         i = 0
    125         gen = self._cache_gen
    126         cache = self._cache
    127         acquire = self._cache_lock.acquire
    128         release = self._cache_lock.release
    129         while gen:
    130             if i == len(cache):
    131                 acquire()
    132                 if self._cache_complete:
    133                     break
    134                 try:
    135                     for j in range(10):
    136                         cache.append(advance_iterator(gen))
    137                 except StopIteration:
    138                     self._cache_gen = gen = None
    139                     self._cache_complete = True
    140                     break
    141                 release()
    142             yield cache[i]
    143             i += 1
    144         while i < self._len:
    145             yield cache[i]
    146             i += 1
    147 
    148     def __getitem__(self, item):
    149         if self._cache_complete:
    150             return self._cache[item]
    151         elif isinstance(item, slice):
    152             if item.step and item.step < 0:
    153                 return list(iter(self))[item]
    154             else:
    155                 return list(itertools.islice(self,
    156                                              item.start or 0,
    157                                              item.stop or sys.maxsize,
    158                                              item.step or 1))
    159         elif item >= 0:
    160             gen = iter(self)
    161             try:
    162                 for i in range(item+1):
    163                     res = advance_iterator(gen)
    164             except StopIteration:
    165                 raise IndexError
    166             return res
    167         else:
    168             return list(iter(self))[item]
    169 
    170     def __contains__(self, item):
    171         if self._cache_complete:
    172             return item in self._cache
    173         else:
    174             for i in self:
    175                 if i == item:
    176                     return True
    177                 elif i > item:
    178                     return False
    179         return False
    180 
    181     # __len__() introduces a large performance penality.
    182     def count(self):
    183         """ Returns the number of recurrences in this set. It will have go
    184             trough the whole recurrence, if this hasn't been done before. """
    185         if self._len is None:
    186             for x in self:
    187                 pass
    188         return self._len
    189 
    190     def before(self, dt, inc=False):
    191         """ Returns the last recurrence before the given datetime instance. The
    192             inc keyword defines what happens if dt is an occurrence. With
    193             inc=True, if dt itself is an occurrence, it will be returned. """
    194         if self._cache_complete:
    195             gen = self._cache
    196         else:
    197             gen = self
    198         last = None
    199         if inc:
    200             for i in gen:
    201                 if i > dt:
    202                     break
    203                 last = i
    204         else:
    205             for i in gen:
    206                 if i >= dt:
    207                     break
    208                 last = i
    209         return last
    210 
    211     def after(self, dt, inc=False):
    212         """ Returns the first recurrence after the given datetime instance. The
    213             inc keyword defines what happens if dt is an occurrence. With
    214             inc=True, if dt itself is an occurrence, it will be returned.  """
    215         if self._cache_complete:
    216             gen = self._cache
    217         else:
    218             gen = self
    219         if inc:
    220             for i in gen:
    221                 if i >= dt:
    222                     return i
    223         else:
    224             for i in gen:
    225                 if i > dt:
    226                     return i
    227         return None
    228 
    229     def xafter(self, dt, count=None, inc=False):
    230         """
    231         Generator which yields up to `count` recurrences after the given
    232         datetime instance, equivalent to `after`.
    233 
    234         :param dt:
    235             The datetime at which to start generating recurrences.
    236 
    237         :param count:
    238             The maximum number of recurrences to generate. If `None` (default),
    239             dates are generated until the recurrence rule is exhausted.
    240 
    241         :param inc:
    242             If `dt` is an instance of the rule and `inc` is `True`, it is
    243             included in the output.
    244 
    245         :yields: Yields a sequence of `datetime` objects.
    246         """
    247 
    248         if self._cache_complete:
    249             gen = self._cache
    250         else:
    251             gen = self
    252 
    253         # Select the comparison function
    254         if inc:
    255             comp = lambda dc, dtc: dc >= dtc
    256         else:
    257             comp = lambda dc, dtc: dc > dtc
    258 
    259         # Generate dates
    260         n = 0
    261         for d in gen:
    262             if comp(d, dt):
    263                 if count is not None:
    264                     n += 1
    265                     if n > count:
    266                         break
    267 
    268                 yield d
    269 
    270     def between(self, after, before, inc=False, count=1):
    271         """ Returns all the occurrences of the rrule between after and before.
    272         The inc keyword defines what happens if after and/or before are
    273         themselves occurrences. With inc=True, they will be included in the
    274         list, if they are found in the recurrence set. """
    275         if self._cache_complete:
    276             gen = self._cache
    277         else:
    278             gen = self
    279         started = False
    280         l = []
    281         if inc:
    282             for i in gen:
    283                 if i > before:
    284                     break
    285                 elif not started:
    286                     if i >= after:
    287                         started = True
    288                         l.append(i)
    289                 else:
    290                     l.append(i)
    291         else:
    292             for i in gen:
    293                 if i >= before:
    294                     break
    295                 elif not started:
    296                     if i > after:
    297                         started = True
    298                         l.append(i)
    299                 else:
    300                     l.append(i)
    301         return l
    302 
    303 
    304 class rrule(rrulebase):
    305     """
    306     That's the base of the rrule operation. It accepts all the keywords
    307     defined in the RFC as its constructor parameters (except byday,
    308     which was renamed to byweekday) and more. The constructor prototype is::
    309 
    310             rrule(freq)
    311 
    312     Where freq must be one of YEARLY, MONTHLY, WEEKLY, DAILY, HOURLY, MINUTELY,
    313     or SECONDLY.
    314 
    315     .. note::
    316         Per RFC section 3.3.10, recurrence instances falling on invalid dates
    317         and times are ignored rather than coerced:
    318 
    319             Recurrence rules may generate recurrence instances with an invalid
    320             date (e.g., February 30) or nonexistent local time (e.g., 1:30 AM
    321             on a day where the local time is moved forward by an hour at 1:00
    322             AM).  Such recurrence instances MUST be ignored and MUST NOT be
    323             counted as part of the recurrence set.
    324 
    325         This can lead to possibly surprising behavior when, for example, the
    326         start date occurs at the end of the month:
    327 
    328         >>> from dateutil.rrule import rrule, MONTHLY
    329         >>> from datetime import datetime
    330         >>> start_date = datetime(2014, 12, 31)
    331         >>> list(rrule(freq=MONTHLY, count=4, dtstart=start_date))
    332         ... # doctest: +NORMALIZE_WHITESPACE
    333         [datetime.datetime(2014, 12, 31, 0, 0),
    334          datetime.datetime(2015, 1, 31, 0, 0),
    335          datetime.datetime(2015, 3, 31, 0, 0),
    336          datetime.datetime(2015, 5, 31, 0, 0)]
    337 
    338     Additionally, it supports the following keyword arguments:
    339 
    340     :param cache:
    341         If given, it must be a boolean value specifying to enable or disable
    342         caching of results. If you will use the same rrule instance multiple
    343         times, enabling caching will improve the performance considerably.
    344     :param dtstart:
    345         The recurrence start. Besides being the base for the recurrence,
    346         missing parameters in the final recurrence instances will also be
    347         extracted from this date. If not given, datetime.now() will be used
    348         instead.
    349     :param interval:
    350         The interval between each freq iteration. For example, when using
    351         YEARLY, an interval of 2 means once every two years, but with HOURLY,
    352         it means once every two hours. The default interval is 1.
    353     :param wkst:
    354         The week start day. Must be one of the MO, TU, WE constants, or an
    355         integer, specifying the first day of the week. This will affect
    356         recurrences based on weekly periods. The default week start is got
    357         from calendar.firstweekday(), and may be modified by
    358         calendar.setfirstweekday().
    359     :param count:
    360         How many occurrences will be generated.
    361 
    362         .. note::
    363             As of version 2.5.0, the use of the ``until`` keyword together
    364             with the ``count`` keyword is deprecated per RFC-5545 Sec. 3.3.10.
    365     :param until:
    366         If given, this must be a datetime instance, that will specify the
    367         limit of the recurrence. The last recurrence in the rule is the greatest
    368         datetime that is less than or equal to the value specified in the
    369         ``until`` parameter.
    370 
    371         .. note::
    372             As of version 2.5.0, the use of the ``until`` keyword together
    373             with the ``count`` keyword is deprecated per RFC-5545 Sec. 3.3.10.
    374     :param bysetpos:
    375         If given, it must be either an integer, or a sequence of integers,
    376         positive or negative. Each given integer will specify an occurrence
    377         number, corresponding to the nth occurrence of the rule inside the
    378         frequency period. For example, a bysetpos of -1 if combined with a
    379         MONTHLY frequency, and a byweekday of (MO, TU, WE, TH, FR), will
    380         result in the last work day of every month.
    381     :param bymonth:
    382         If given, it must be either an integer, or a sequence of integers,
    383         meaning the months to apply the recurrence to.
    384     :param bymonthday:
    385         If given, it must be either an integer, or a sequence of integers,
    386         meaning the month days to apply the recurrence to.
    387     :param byyearday:
    388         If given, it must be either an integer, or a sequence of integers,
    389         meaning the year days to apply the recurrence to.
    390     :param byweekno:
    391         If given, it must be either an integer, or a sequence of integers,
    392         meaning the week numbers to apply the recurrence to. Week numbers
    393         have the meaning described in ISO8601, that is, the first week of
    394         the year is that containing at least four days of the new year.
    395     :param byweekday:
    396         If given, it must be either an integer (0 == MO), a sequence of
    397         integers, one of the weekday constants (MO, TU, etc), or a sequence
    398         of these constants. When given, these variables will define the
    399         weekdays where the recurrence will be applied. It's also possible to
    400         use an argument n for the weekday instances, which will mean the nth
    401         occurrence of this weekday in the period. For example, with MONTHLY,
    402         or with YEARLY and BYMONTH, using FR(+1) in byweekday will specify the
    403         first friday of the month where the recurrence happens. Notice that in
    404         the RFC documentation, this is specified as BYDAY, but was renamed to
    405         avoid the ambiguity of that keyword.
    406     :param byhour:
    407         If given, it must be either an integer, or a sequence of integers,
    408         meaning the hours to apply the recurrence to.
    409     :param byminute:
    410         If given, it must be either an integer, or a sequence of integers,
    411         meaning the minutes to apply the recurrence to.
    412     :param bysecond:
    413         If given, it must be either an integer, or a sequence of integers,
    414         meaning the seconds to apply the recurrence to.
    415     :param byeaster:
    416         If given, it must be either an integer, or a sequence of integers,
    417         positive or negative. Each integer will define an offset from the
    418         Easter Sunday. Passing the offset 0 to byeaster will yield the Easter
    419         Sunday itself. This is an extension to the RFC specification.
    420      """
    421     def __init__(self, freq, dtstart=None,
    422                  interval=1, wkst=None, count=None, until=None, bysetpos=None,
    423                  bymonth=None, bymonthday=None, byyearday=None, byeaster=None,
    424                  byweekno=None, byweekday=None,
    425                  byhour=None, byminute=None, bysecond=None,
    426                  cache=False):
    427         super(rrule, self).__init__(cache)
    428         global easter
    429         if not dtstart:
    430             dtstart = datetime.datetime.now().replace(microsecond=0)
    431         elif not isinstance(dtstart, datetime.datetime):
    432             dtstart = datetime.datetime.fromordinal(dtstart.toordinal())
    433         else:
    434             dtstart = dtstart.replace(microsecond=0)
    435         self._dtstart = dtstart
    436         self._tzinfo = dtstart.tzinfo
    437         self._freq = freq
    438         self._interval = interval
    439         self._count = count
    440 
    441         # Cache the original byxxx rules, if they are provided, as the _byxxx
    442         # attributes do not necessarily map to the inputs, and this can be
    443         # a problem in generating the strings. Only store things if they've
    444         # been supplied (the string retrieval will just use .get())
    445         self._original_rule = {}
    446 
    447         if until and not isinstance(until, datetime.datetime):
    448             until = datetime.datetime.fromordinal(until.toordinal())
    449         self._until = until
    450 
    451         if self._dtstart and self._until:
    452             if (self._dtstart.tzinfo is not None) != (self._until.tzinfo is not None):
    453                 # According to RFC5545 Section 3.3.10:
    454                 # https://tools.ietf.org/html/rfc5545#section-3.3.10
    455                 #
    456                 # > If the "DTSTART" property is specified as a date with UTC
    457                 # > time or a date with local time and time zone reference,
    458                 # > then the UNTIL rule part MUST be specified as a date with
    459                 # > UTC time.
    460                 raise ValueError(
    461                     'RRULE UNTIL values must be specified in UTC when DTSTART '
    462                     'is timezone-aware'
    463                 )
    464 
    465         if count is not None and until:
    466             warn("Using both 'count' and 'until' is inconsistent with RFC 5545"
    467                  " and has been deprecated in dateutil. Future versions will "
    468                  "raise an error.", DeprecationWarning)
    469 
    470         if wkst is None:
    471             self._wkst = calendar.firstweekday()
    472         elif isinstance(wkst, integer_types):
    473             self._wkst = wkst
    474         else:
    475             self._wkst = wkst.weekday
    476 
    477         if bysetpos is None:
    478             self._bysetpos = None
    479         elif isinstance(bysetpos, integer_types):
    480             if bysetpos == 0 or not (-366 <= bysetpos <= 366):
    481                 raise ValueError("bysetpos must be between 1 and 366, "
    482                                  "or between -366 and -1")
    483             self._bysetpos = (bysetpos,)
    484         else:
    485             self._bysetpos = tuple(bysetpos)
    486             for pos in self._bysetpos:
    487                 if pos == 0 or not (-366 <= pos <= 366):
    488                     raise ValueError("bysetpos must be between 1 and 366, "
    489                                      "or between -366 and -1")
    490 
    491         if self._bysetpos:
    492             self._original_rule['bysetpos'] = self._bysetpos
    493 
    494         if (byweekno is None and byyearday is None and bymonthday is None and
    495                 byweekday is None and byeaster is None):
    496             if freq == YEARLY:
    497                 if bymonth is None:
    498                     bymonth = dtstart.month
    499                     self._original_rule['bymonth'] = None
    500                 bymonthday = dtstart.day
    501                 self._original_rule['bymonthday'] = None
    502             elif freq == MONTHLY:
    503                 bymonthday = dtstart.day
    504                 self._original_rule['bymonthday'] = None
    505             elif freq == WEEKLY:
    506                 byweekday = dtstart.weekday()
    507                 self._original_rule['byweekday'] = None
    508 
    509         # bymonth
    510         if bymonth is None:
    511             self._bymonth = None
    512         else:
    513             if isinstance(bymonth, integer_types):
    514                 bymonth = (bymonth,)
    515 
    516             self._bymonth = tuple(sorted(set(bymonth)))
    517 
    518             if 'bymonth' not in self._original_rule:
    519                 self._original_rule['bymonth'] = self._bymonth
    520 
    521         # byyearday
    522         if byyearday is None:
    523             self._byyearday = None
    524         else:
    525             if isinstance(byyearday, integer_types):
    526                 byyearday = (byyearday,)
    527 
    528             self._byyearday = tuple(sorted(set(byyearday)))
    529             self._original_rule['byyearday'] = self._byyearday
    530 
    531         # byeaster
    532         if byeaster is not None:
    533             if not easter:
    534                 from dateutil import easter
    535             if isinstance(byeaster, integer_types):
    536                 self._byeaster = (byeaster,)
    537             else:
    538                 self._byeaster = tuple(sorted(byeaster))
    539 
    540             self._original_rule['byeaster'] = self._byeaster
    541         else:
    542             self._byeaster = None
    543 
    544         # bymonthday
    545         if bymonthday is None:
    546             self._bymonthday = ()
    547             self._bynmonthday = ()
    548         else:
    549             if isinstance(bymonthday, integer_types):
    550                 bymonthday = (bymonthday,)
    551 
    552             bymonthday = set(bymonthday)            # Ensure it's unique
    553 
    554             self._bymonthday = tuple(sorted(x for x in bymonthday if x > 0))
    555             self._bynmonthday = tuple(sorted(x for x in bymonthday if x < 0))
    556 
    557             # Storing positive numbers first, then negative numbers
    558             if 'bymonthday' not in self._original_rule:
    559                 self._original_rule['bymonthday'] = tuple(
    560                     itertools.chain(self._bymonthday, self._bynmonthday))
    561 
    562         # byweekno
    563         if byweekno is None:
    564             self._byweekno = None
    565         else:
    566             if isinstance(byweekno, integer_types):
    567                 byweekno = (byweekno,)
    568 
    569             self._byweekno = tuple(sorted(set(byweekno)))
    570 
    571             self._original_rule['byweekno'] = self._byweekno
    572 
    573         # byweekday / bynweekday
    574         if byweekday is None:
    575             self._byweekday = None
    576             self._bynweekday = None
    577         else:
    578             # If it's one of the valid non-sequence types, convert to a
    579             # single-element sequence before the iterator that builds the
    580             # byweekday set.
    581             if isinstance(byweekday, integer_types) or hasattr(byweekday, "n"):
    582                 byweekday = (byweekday,)
    583 
    584             self._byweekday = set()
    585             self._bynweekday = set()
    586             for wday in byweekday:
    587                 if isinstance(wday, integer_types):
    588                     self._byweekday.add(wday)
    589                 elif not wday.n or freq > MONTHLY:
    590                     self._byweekday.add(wday.weekday)
    591                 else:
    592                     self._bynweekday.add((wday.weekday, wday.n))
    593 
    594             if not self._byweekday:
    595                 self._byweekday = None
    596             elif not self._bynweekday:
    597                 self._bynweekday = None
    598 
    599             if self._byweekday is not None:
    600                 self._byweekday = tuple(sorted(self._byweekday))
    601                 orig_byweekday = [weekday(x) for x in self._byweekday]
    602             else:
    603                 orig_byweekday = ()
    604 
    605             if self._bynweekday is not None:
    606                 self._bynweekday = tuple(sorted(self._bynweekday))
    607                 orig_bynweekday = [weekday(*x) for x in self._bynweekday]
    608             else:
    609                 orig_bynweekday = ()
    610 
    611             if 'byweekday' not in self._original_rule:
    612                 self._original_rule['byweekday'] = tuple(itertools.chain(
    613                     orig_byweekday, orig_bynweekday))
    614 
    615         # byhour
    616         if byhour is None:
    617             if freq < HOURLY:
    618                 self._byhour = {dtstart.hour}
    619             else:
    620                 self._byhour = None
    621         else:
    622             if isinstance(byhour, integer_types):
    623                 byhour = (byhour,)
    624 
    625             if freq == HOURLY:
    626                 self._byhour = self.__construct_byset(start=dtstart.hour,
    627                                                       byxxx=byhour,
    628                                                       base=24)
    629             else:
    630                 self._byhour = set(byhour)
    631 
    632             self._byhour = tuple(sorted(self._byhour))
    633             self._original_rule['byhour'] = self._byhour
    634 
    635         # byminute
    636         if byminute is None:
    637             if freq < MINUTELY:
    638                 self._byminute = {dtstart.minute}
    639             else:
    640                 self._byminute = None
    641         else:
    642             if isinstance(byminute, integer_types):
    643                 byminute = (byminute,)
    644 
    645             if freq == MINUTELY:
    646                 self._byminute = self.__construct_byset(start=dtstart.minute,
    647                                                         byxxx=byminute,
    648                                                         base=60)
    649             else:
    650                 self._byminute = set(byminute)
    651 
    652             self._byminute = tuple(sorted(self._byminute))
    653             self._original_rule['byminute'] = self._byminute
    654 
    655         # bysecond
    656         if bysecond is None:
    657             if freq < SECONDLY:
    658                 self._bysecond = ((dtstart.second,))
    659             else:
    660                 self._bysecond = None
    661         else:
    662             if isinstance(bysecond, integer_types):
    663                 bysecond = (bysecond,)
    664 
    665             self._bysecond = set(bysecond)
    666 
    667             if freq == SECONDLY:
    668                 self._bysecond = self.__construct_byset(start=dtstart.second,
    669                                                         byxxx=bysecond,
    670                                                         base=60)
    671             else:
    672                 self._bysecond = set(bysecond)
    673 
    674             self._bysecond = tuple(sorted(self._bysecond))
    675             self._original_rule['bysecond'] = self._bysecond
    676 
    677         if self._freq >= HOURLY:
    678             self._timeset = None
    679         else:
    680             self._timeset = []
    681             for hour in self._byhour:
    682                 for minute in self._byminute:
    683                     for second in self._bysecond:
    684                         self._timeset.append(
    685                             datetime.time(hour, minute, second,
    686                                           tzinfo=self._tzinfo))
    687             self._timeset.sort()
    688             self._timeset = tuple(self._timeset)
    689 
    690     def __str__(self):
    691         """
    692         Output a string that would generate this RRULE if passed to rrulestr.
    693         This is mostly compatible with RFC5545, except for the
    694         dateutil-specific extension BYEASTER.
    695         """
    696 
    697         output = []
    698         h, m, s = [None] * 3
    699         if self._dtstart:
    700             output.append(self._dtstart.strftime('DTSTART:%Y%m%dT%H%M%S'))
    701             h, m, s = self._dtstart.timetuple()[3:6]
    702 
    703         parts = ['FREQ=' + FREQNAMES[self._freq]]
    704         if self._interval != 1:
    705             parts.append('INTERVAL=' + str(self._interval))
    706 
    707         if self._wkst:
    708             parts.append('WKST=' + repr(weekday(self._wkst))[0:2])
    709 
    710         if self._count is not None:
    711             parts.append('COUNT=' + str(self._count))
    712 
    713         if self._until:
    714             parts.append(self._until.strftime('UNTIL=%Y%m%dT%H%M%S'))
    715 
    716         if self._original_rule.get('byweekday') is not None:
    717             # The str() method on weekday objects doesn't generate
    718             # RFC5545-compliant strings, so we should modify that.
    719             original_rule = dict(self._original_rule)
    720             wday_strings = []
    721             for wday in original_rule['byweekday']:
    722                 if wday.n:
    723                     wday_strings.append('{n:+d}{wday}'.format(
    724                         n=wday.n,
    725                         wday=repr(wday)[0:2]))
    726                 else:
    727                     wday_strings.append(repr(wday))
    728 
    729             original_rule['byweekday'] = wday_strings
    730         else:
    731             original_rule = self._original_rule
    732 
    733         partfmt = '{name}={vals}'
    734         for name, key in [('BYSETPOS', 'bysetpos'),
    735                           ('BYMONTH', 'bymonth'),
    736                           ('BYMONTHDAY', 'bymonthday'),
    737                           ('BYYEARDAY', 'byyearday'),
    738                           ('BYWEEKNO', 'byweekno'),
    739                           ('BYDAY', 'byweekday'),
    740                           ('BYHOUR', 'byhour'),
    741                           ('BYMINUTE', 'byminute'),
    742                           ('BYSECOND', 'bysecond'),
    743                           ('BYEASTER', 'byeaster')]:
    744             value = original_rule.get(key)
    745             if value:
    746                 parts.append(partfmt.format(name=name, vals=(','.join(str(v)
    747                                                              for v in value))))
    748 
    749         output.append('RRULE:' + ';'.join(parts))
    750         return '\n'.join(output)
    751 
    752     def replace(self, **kwargs):
    753         """Return new rrule with same attributes except for those attributes given new
    754            values by whichever keyword arguments are specified."""
    755         new_kwargs = {"interval": self._interval,
    756                       "count": self._count,
    757                       "dtstart": self._dtstart,
    758                       "freq": self._freq,
    759                       "until": self._until,
    760                       "wkst": self._wkst,
    761                       "cache": False if self._cache is None else True }
    762         new_kwargs.update(self._original_rule)
    763         new_kwargs.update(kwargs)
    764         return rrule(**new_kwargs)
    765 
    766     def _iter(self):
    767         year, month, day, hour, minute, second, weekday, yearday, _ = \
    768             self._dtstart.timetuple()
    769 
    770         # Some local variables to speed things up a bit
    771         freq = self._freq
    772         interval = self._interval
    773         wkst = self._wkst
    774         until = self._until
    775         bymonth = self._bymonth
    776         byweekno = self._byweekno
    777         byyearday = self._byyearday
    778         byweekday = self._byweekday
    779         byeaster = self._byeaster
    780         bymonthday = self._bymonthday
    781         bynmonthday = self._bynmonthday
    782         bysetpos = self._bysetpos
    783         byhour = self._byhour
    784         byminute = self._byminute
    785         bysecond = self._bysecond
    786 
    787         ii = _iterinfo(self)
    788         ii.rebuild(year, month)
    789 
    790         getdayset = {YEARLY: ii.ydayset,
    791                      MONTHLY: ii.mdayset,
    792                      WEEKLY: ii.wdayset,
    793                      DAILY: ii.ddayset,
    794                      HOURLY: ii.ddayset,
    795                      MINUTELY: ii.ddayset,
    796                      SECONDLY: ii.ddayset}[freq]
    797 
    798         if freq < HOURLY:
    799             timeset = self._timeset
    800         else:
    801             gettimeset = {HOURLY: ii.htimeset,
    802                           MINUTELY: ii.mtimeset,
    803                           SECONDLY: ii.stimeset}[freq]
    804             if ((freq >= HOURLY and
    805                  self._byhour and hour not in self._byhour) or
    806                 (freq >= MINUTELY and
    807                  self._byminute and minute not in self._byminute) or
    808                 (freq >= SECONDLY and
    809                  self._bysecond and second not in self._bysecond)):
    810                 timeset = ()
    811             else:
    812                 timeset = gettimeset(hour, minute, second)
    813 
    814         total = 0
    815         count = self._count
    816         while True:
    817             # Get dayset with the right frequency
    818             dayset, start, end = getdayset(year, month, day)
    819 
    820             # Do the "hard" work ;-)
    821             filtered = False
    822             for i in dayset[start:end]:
    823                 if ((bymonth and ii.mmask[i] not in bymonth) or
    824                     (byweekno and not ii.wnomask[i]) or
    825                     (byweekday and ii.wdaymask[i] not in byweekday) or
    826                     (ii.nwdaymask and not ii.nwdaymask[i]) or
    827                     (byeaster and not ii.eastermask[i]) or
    828                     ((bymonthday or bynmonthday) and
    829                      ii.mdaymask[i] not in bymonthday and
    830                      ii.nmdaymask[i] not in bynmonthday) or
    831                     (byyearday and
    832                      ((i < ii.yearlen and i+1 not in byyearday and
    833                        -ii.yearlen+i not in byyearday) or
    834                       (i >= ii.yearlen and i+1-ii.yearlen not in byyearday and
    835                        -ii.nextyearlen+i-ii.yearlen not in byyearday)))):
    836                     dayset[i] = None
    837                     filtered = True
    838 
    839             # Output results
    840             if bysetpos and timeset:
    841                 poslist = []
    842                 for pos in bysetpos:
    843                     if pos < 0:
    844                         daypos, timepos = divmod(pos, len(timeset))
    845                     else:
    846                         daypos, timepos = divmod(pos-1, len(timeset))
    847                     try:
    848                         i = [x for x in dayset[start:end]
    849                              if x is not None][daypos]
    850                         time = timeset[timepos]
    851                     except IndexError:
    852                         pass
    853                     else:
    854                         date = datetime.date.fromordinal(ii.yearordinal+i)
    855                         res = datetime.datetime.combine(date, time)
    856                         if res not in poslist:
    857                             poslist.append(res)
    858                 poslist.sort()
    859                 for res in poslist:
    860                     if until and res > until:
    861                         self._len = total
    862                         return
    863                     elif res >= self._dtstart:
    864                         if count is not None:
    865                             count -= 1
    866                             if count < 0:
    867                                 self._len = total
    868                                 return
    869                         total += 1
    870                         yield res
    871             else:
    872                 for i in dayset[start:end]:
    873                     if i is not None:
    874                         date = datetime.date.fromordinal(ii.yearordinal + i)
    875                         for time in timeset:
    876                             res = datetime.datetime.combine(date, time)
    877                             if until and res > until:
    878                                 self._len = total
    879                                 return
    880                             elif res >= self._dtstart:
    881                                 if count is not None:
    882                                     count -= 1
    883                                     if count < 0:
    884                                         self._len = total
    885                                         return
    886 
    887                                 total += 1
    888                                 yield res
    889 
    890             # Handle frequency and interval
    891             fixday = False
    892             if freq == YEARLY:
    893                 year += interval
    894                 if year > datetime.MAXYEAR:
    895                     self._len = total
    896                     return
    897                 ii.rebuild(year, month)
    898             elif freq == MONTHLY:
    899                 month += interval
    900                 if month > 12:
    901                     div, mod = divmod(month, 12)
    902                     month = mod
    903                     year += div
    904                     if month == 0:
    905                         month = 12
    906                         year -= 1
    907                     if year > datetime.MAXYEAR:
    908                         self._len = total
    909                         return
    910                 ii.rebuild(year, month)
    911             elif freq == WEEKLY:
    912                 if wkst > weekday:
    913                     day += -(weekday+1+(6-wkst))+self._interval*7
    914                 else:
    915                     day += -(weekday-wkst)+self._interval*7
    916                 weekday = wkst
    917                 fixday = True
    918             elif freq == DAILY:
    919                 day += interval
    920                 fixday = True
    921             elif freq == HOURLY:
    922                 if filtered:
    923                     # Jump to one iteration before next day
    924                     hour += ((23-hour)//interval)*interval
    925 
    926                 if byhour:
    927                     ndays, hour = self.__mod_distance(value=hour,
    928                                                       byxxx=self._byhour,
    929                                                       base=24)
    930                 else:
    931                     ndays, hour = divmod(hour+interval, 24)
    932 
    933                 if ndays:
    934                     day += ndays
    935                     fixday = True
    936 
    937                 timeset = gettimeset(hour, minute, second)
    938             elif freq == MINUTELY:
    939                 if filtered:
    940                     # Jump to one iteration before next day
    941                     minute += ((1439-(hour*60+minute))//interval)*interval
    942 
    943                 valid = False
    944                 rep_rate = (24*60)
    945                 for j in range(rep_rate // gcd(interval, rep_rate)):
    946                     if byminute:
    947                         nhours, minute = \
    948                             self.__mod_distance(value=minute,
    949                                                 byxxx=self._byminute,
    950                                                 base=60)
    951                     else:
    952                         nhours, minute = divmod(minute+interval, 60)
    953 
    954                     div, hour = divmod(hour+nhours, 24)
    955                     if div:
    956                         day += div
    957                         fixday = True
    958                         filtered = False
    959 
    960                     if not byhour or hour in byhour:
    961                         valid = True
    962                         break
    963 
    964                 if not valid:
    965                     raise ValueError('Invalid combination of interval and ' +
    966                                      'byhour resulting in empty rule.')
    967 
    968                 timeset = gettimeset(hour, minute, second)
    969             elif freq == SECONDLY:
    970                 if filtered:
    971                     # Jump to one iteration before next day
    972                     second += (((86399 - (hour * 3600 + minute * 60 + second))
    973                                 // interval) * interval)
    974 
    975                 rep_rate = (24 * 3600)
    976                 valid = False
    977                 for j in range(0, rep_rate // gcd(interval, rep_rate)):
    978                     if bysecond:
    979                         nminutes, second = \
    980                             self.__mod_distance(value=second,
    981                                                 byxxx=self._bysecond,
    982                                                 base=60)
    983                     else:
    984                         nminutes, second = divmod(second+interval, 60)
    985 
    986                     div, minute = divmod(minute+nminutes, 60)
    987                     if div:
    988                         hour += div
    989                         div, hour = divmod(hour, 24)
    990                         if div:
    991                             day += div
    992                             fixday = True
    993 
    994                     if ((not byhour or hour in byhour) and
    995                             (not byminute or minute in byminute) and
    996                             (not bysecond or second in bysecond)):
    997                         valid = True
    998                         break
    999 
   1000                 if not valid:
   1001                     raise ValueError('Invalid combination of interval, ' +
   1002                                      'byhour and byminute resulting in empty' +
   1003                                      ' rule.')
   1004 
   1005                 timeset = gettimeset(hour, minute, second)
   1006 
   1007             if fixday and day > 28:
   1008                 daysinmonth = calendar.monthrange(year, month)[1]
   1009                 if day > daysinmonth:
   1010                     while day > daysinmonth:
   1011                         day -= daysinmonth
   1012                         month += 1
   1013                         if month == 13:
   1014                             month = 1
   1015                             year += 1
   1016                             if year > datetime.MAXYEAR:
   1017                                 self._len = total
   1018                                 return
   1019                         daysinmonth = calendar.monthrange(year, month)[1]
   1020                     ii.rebuild(year, month)
   1021 
   1022     def __construct_byset(self, start, byxxx, base):
   1023         """
   1024         If a `BYXXX` sequence is passed to the constructor at the same level as
   1025         `FREQ` (e.g. `FREQ=HOURLY,BYHOUR={2,4,7},INTERVAL=3`), there are some
   1026         specifications which cannot be reached given some starting conditions.
   1027 
   1028         This occurs whenever the interval is not coprime with the base of a
   1029         given unit and the difference between the starting position and the
   1030         ending position is not coprime with the greatest common denominator
   1031         between the interval and the base. For example, with a FREQ of hourly
   1032         starting at 17:00 and an interval of 4, the only valid values for
   1033         BYHOUR would be {21, 1, 5, 9, 13, 17}, because 4 and 24 are not
   1034         coprime.
   1035 
   1036         :param start:
   1037             Specifies the starting position.
   1038         :param byxxx:
   1039             An iterable containing the list of allowed values.
   1040         :param base:
   1041             The largest allowable value for the specified frequency (e.g.
   1042             24 hours, 60 minutes).
   1043 
   1044         This does not preserve the type of the iterable, returning a set, since
   1045         the values should be unique and the order is irrelevant, this will
   1046         speed up later lookups.
   1047 
   1048         In the event of an empty set, raises a :exception:`ValueError`, as this
   1049         results in an empty rrule.
   1050         """
   1051 
   1052         cset = set()
   1053 
   1054         # Support a single byxxx value.
   1055         if isinstance(byxxx, integer_types):
   1056             byxxx = (byxxx, )
   1057 
   1058         for num in byxxx:
   1059             i_gcd = gcd(self._interval, base)
   1060             # Use divmod rather than % because we need to wrap negative nums.
   1061             if i_gcd == 1 or divmod(num - start, i_gcd)[1] == 0:
   1062                 cset.add(num)
   1063 
   1064         if len(cset) == 0:
   1065             raise ValueError("Invalid rrule byxxx generates an empty set.")
   1066 
   1067         return cset
   1068 
   1069     def __mod_distance(self, value, byxxx, base):
   1070         """
   1071         Calculates the next value in a sequence where the `FREQ` parameter is
   1072         specified along with a `BYXXX` parameter at the same "level"
   1073         (e.g. `HOURLY` specified with `BYHOUR`).
   1074 
   1075         :param value:
   1076             The old value of the component.
   1077         :param byxxx:
   1078             The `BYXXX` set, which should have been generated by
   1079             `rrule._construct_byset`, or something else which checks that a
   1080             valid rule is present.
   1081         :param base:
   1082             The largest allowable value for the specified frequency (e.g.
   1083             24 hours, 60 minutes).
   1084 
   1085         If a valid value is not found after `base` iterations (the maximum
   1086         number before the sequence would start to repeat), this raises a
   1087         :exception:`ValueError`, as no valid values were found.
   1088 
   1089         This returns a tuple of `divmod(n*interval, base)`, where `n` is the
   1090         smallest number of `interval` repetitions until the next specified
   1091         value in `byxxx` is found.
   1092         """
   1093         accumulator = 0
   1094         for ii in range(1, base + 1):
   1095             # Using divmod() over % to account for negative intervals
   1096             div, value = divmod(value + self._interval, base)
   1097             accumulator += div
   1098             if value in byxxx:
   1099                 return (accumulator, value)
   1100 
   1101 
   1102 class _iterinfo(object):
   1103     __slots__ = ["rrule", "lastyear", "lastmonth",
   1104                  "yearlen", "nextyearlen", "yearordinal", "yearweekday",
   1105                  "mmask", "mrange", "mdaymask", "nmdaymask",
   1106                  "wdaymask", "wnomask", "nwdaymask", "eastermask"]
   1107 
   1108     def __init__(self, rrule):
   1109         for attr in self.__slots__:
   1110             setattr(self, attr, None)
   1111         self.rrule = rrule
   1112 
   1113     def rebuild(self, year, month):
   1114         # Every mask is 7 days longer to handle cross-year weekly periods.
   1115         rr = self.rrule
   1116         if year != self.lastyear:
   1117             self.yearlen = 365 + calendar.isleap(year)
   1118             self.nextyearlen = 365 + calendar.isleap(year + 1)
   1119             firstyday = datetime.date(year, 1, 1)
   1120             self.yearordinal = firstyday.toordinal()
   1121             self.yearweekday = firstyday.weekday()
   1122 
   1123             wday = datetime.date(year, 1, 1).weekday()
   1124             if self.yearlen == 365:
   1125                 self.mmask = M365MASK
   1126                 self.mdaymask = MDAY365MASK
   1127                 self.nmdaymask = NMDAY365MASK
   1128                 self.wdaymask = WDAYMASK[wday:]
   1129                 self.mrange = M365RANGE
   1130             else:
   1131                 self.mmask = M366MASK
   1132                 self.mdaymask = MDAY366MASK
   1133                 self.nmdaymask = NMDAY366MASK
   1134                 self.wdaymask = WDAYMASK[wday:]
   1135                 self.mrange = M366RANGE
   1136 
   1137             if not rr._byweekno:
   1138                 self.wnomask = None
   1139             else:
   1140                 self.wnomask = [0]*(self.yearlen+7)
   1141                 # no1wkst = firstwkst = self.wdaymask.index(rr._wkst)
   1142                 no1wkst = firstwkst = (7-self.yearweekday+rr._wkst) % 7
   1143                 if no1wkst >= 4:
   1144                     no1wkst = 0
   1145                     # Number of days in the year, plus the days we got
   1146                     # from last year.
   1147                     wyearlen = self.yearlen+(self.yearweekday-rr._wkst) % 7
   1148                 else:
   1149                     # Number of days in the year, minus the days we
   1150                     # left in last year.
   1151                     wyearlen = self.yearlen-no1wkst
   1152                 div, mod = divmod(wyearlen, 7)
   1153                 numweeks = div+mod//4
   1154                 for n in rr._byweekno:
   1155                     if n < 0:
   1156                         n += numweeks+1
   1157                     if not (0 < n <= numweeks):
   1158                         continue
   1159                     if n > 1:
   1160                         i = no1wkst+(n-1)*7
   1161                         if no1wkst != firstwkst:
   1162                             i -= 7-firstwkst
   1163                     else:
   1164                         i = no1wkst
   1165                     for j in range(7):
   1166                         self.wnomask[i] = 1
   1167                         i += 1
   1168                         if self.wdaymask[i] == rr._wkst:
   1169                             break
   1170                 if 1 in rr._byweekno:
   1171                     # Check week number 1 of next year as well
   1172                     # TODO: Check -numweeks for next year.
   1173                     i = no1wkst+numweeks*7
   1174                     if no1wkst != firstwkst:
   1175                         i -= 7-firstwkst
   1176                     if i < self.yearlen:
   1177                         # If week starts in next year, we
   1178                         # don't care about it.
   1179                         for j in range(7):
   1180                             self.wnomask[i] = 1
   1181                             i += 1
   1182                             if self.wdaymask[i] == rr._wkst:
   1183                                 break
   1184                 if no1wkst:
   1185                     # Check last week number of last year as
   1186                     # well. If no1wkst is 0, either the year
   1187                     # started on week start, or week number 1
   1188                     # got days from last year, so there are no
   1189                     # days from last year's last week number in
   1190                     # this year.
   1191                     if -1 not in rr._byweekno:
   1192                         lyearweekday = datetime.date(year-1, 1, 1).weekday()
   1193                         lno1wkst = (7-lyearweekday+rr._wkst) % 7
   1194                         lyearlen = 365+calendar.isleap(year-1)
   1195                         if lno1wkst >= 4:
   1196                             lno1wkst = 0
   1197                             lnumweeks = 52+(lyearlen +
   1198                                             (lyearweekday-rr._wkst) % 7) % 7//4
   1199                         else:
   1200                             lnumweeks = 52+(self.yearlen-no1wkst) % 7//4
   1201                     else:
   1202                         lnumweeks = -1
   1203                     if lnumweeks in rr._byweekno:
   1204                         for i in range(no1wkst):
   1205                             self.wnomask[i] = 1
   1206 
   1207         if (rr._bynweekday and (month != self.lastmonth or
   1208                                 year != self.lastyear)):
   1209             ranges = []
   1210             if rr._freq == YEARLY:
   1211                 if rr._bymonth:
   1212                     for month in rr._bymonth:
   1213                         ranges.append(self.mrange[month-1:month+1])
   1214                 else:
   1215                     ranges = [(0, self.yearlen)]
   1216             elif rr._freq == MONTHLY:
   1217                 ranges = [self.mrange[month-1:month+1]]
   1218             if ranges:
   1219                 # Weekly frequency won't get here, so we may not
   1220                 # care about cross-year weekly periods.
   1221                 self.nwdaymask = [0]*self.yearlen
   1222                 for first, last in ranges:
   1223                     last -= 1
   1224                     for wday, n in rr._bynweekday:
   1225                         if n < 0:
   1226                             i = last+(n+1)*7
   1227                             i -= (self.wdaymask[i]-wday) % 7
   1228                         else:
   1229                             i = first+(n-1)*7
   1230                             i += (7-self.wdaymask[i]+wday) % 7
   1231                         if first <= i <= last:
   1232                             self.nwdaymask[i] = 1
   1233 
   1234         if rr._byeaster:
   1235             self.eastermask = [0]*(self.yearlen+7)
   1236             eyday = easter.easter(year).toordinal()-self.yearordinal
   1237             for offset in rr._byeaster:
   1238                 self.eastermask[eyday+offset] = 1
   1239 
   1240         self.lastyear = year
   1241         self.lastmonth = month
   1242 
   1243     def ydayset(self, year, month, day):
   1244         return list(range(self.yearlen)), 0, self.yearlen
   1245 
   1246     def mdayset(self, year, month, day):
   1247         dset = [None]*self.yearlen
   1248         start, end = self.mrange[month-1:month+1]
   1249         for i in range(start, end):
   1250             dset[i] = i
   1251         return dset, start, end
   1252 
   1253     def wdayset(self, year, month, day):
   1254         # We need to handle cross-year weeks here.
   1255         dset = [None]*(self.yearlen+7)
   1256         i = datetime.date(year, month, day).toordinal()-self.yearordinal
   1257         start = i
   1258         for j in range(7):
   1259             dset[i] = i
   1260             i += 1
   1261             # if (not (0 <= i < self.yearlen) or
   1262             #    self.wdaymask[i] == self.rrule._wkst):
   1263             # This will cross the year boundary, if necessary.
   1264             if self.wdaymask[i] == self.rrule._wkst:
   1265                 break
   1266         return dset, start, i
   1267 
   1268     def ddayset(self, year, month, day):
   1269         dset = [None] * self.yearlen
   1270         i = datetime.date(year, month, day).toordinal() - self.yearordinal
   1271         dset[i] = i
   1272         return dset, i, i + 1
   1273 
   1274     def htimeset(self, hour, minute, second):
   1275         tset = []
   1276         rr = self.rrule
   1277         for minute in rr._byminute:
   1278             for second in rr._bysecond:
   1279                 tset.append(datetime.time(hour, minute, second,
   1280                                           tzinfo=rr._tzinfo))
   1281         tset.sort()
   1282         return tset
   1283 
   1284     def mtimeset(self, hour, minute, second):
   1285         tset = []
   1286         rr = self.rrule
   1287         for second in rr._bysecond:
   1288             tset.append(datetime.time(hour, minute, second, tzinfo=rr._tzinfo))
   1289         tset.sort()
   1290         return tset
   1291 
   1292     def stimeset(self, hour, minute, second):
   1293         return (datetime.time(hour, minute, second,
   1294                 tzinfo=self.rrule._tzinfo),)
   1295 
   1296 
   1297 class rruleset(rrulebase):
   1298     """ The rruleset type allows more complex recurrence setups, mixing
   1299     multiple rules, dates, exclusion rules, and exclusion dates. The type
   1300     constructor takes the following keyword arguments:
   1301 
   1302     :param cache: If True, caching of results will be enabled, improving
   1303                   performance of multiple queries considerably. """
   1304 
   1305     class _genitem(object):
   1306         def __init__(self, genlist, gen):
   1307             try:
   1308                 self.dt = advance_iterator(gen)
   1309                 genlist.append(self)
   1310             except StopIteration:
   1311                 pass
   1312             self.genlist = genlist
   1313             self.gen = gen
   1314 
   1315         def __next__(self):
   1316             try:
   1317                 self.dt = advance_iterator(self.gen)
   1318             except StopIteration:
   1319                 if self.genlist[0] is self:
   1320                     heapq.heappop(self.genlist)
   1321                 else:
   1322                     self.genlist.remove(self)
   1323                     heapq.heapify(self.genlist)
   1324 
   1325         next = __next__
   1326 
   1327         def __lt__(self, other):
   1328             return self.dt < other.dt
   1329 
   1330         def __gt__(self, other):
   1331             return self.dt > other.dt
   1332 
   1333         def __eq__(self, other):
   1334             return self.dt == other.dt
   1335 
   1336         def __ne__(self, other):
   1337             return self.dt != other.dt
   1338 
   1339     def __init__(self, cache=False):
   1340         super(rruleset, self).__init__(cache)
   1341         self._rrule = []
   1342         self._rdate = []
   1343         self._exrule = []
   1344         self._exdate = []
   1345 
   1346     @_invalidates_cache
   1347     def rrule(self, rrule):
   1348         """ Include the given :py:class:`rrule` instance in the recurrence set
   1349             generation. """
   1350         self._rrule.append(rrule)
   1351 
   1352     @_invalidates_cache
   1353     def rdate(self, rdate):
   1354         """ Include the given :py:class:`datetime` instance in the recurrence
   1355             set generation. """
   1356         self._rdate.append(rdate)
   1357 
   1358     @_invalidates_cache
   1359     def exrule(self, exrule):
   1360         """ Include the given rrule instance in the recurrence set exclusion
   1361             list. Dates which are part of the given recurrence rules will not
   1362             be generated, even if some inclusive rrule or rdate matches them.
   1363         """
   1364         self._exrule.append(exrule)
   1365 
   1366     @_invalidates_cache
   1367     def exdate(self, exdate):
   1368         """ Include the given datetime instance in the recurrence set
   1369             exclusion list. Dates included that way will not be generated,
   1370             even if some inclusive rrule or rdate matches them. """
   1371         self._exdate.append(exdate)
   1372 
   1373     def _iter(self):
   1374         rlist = []
   1375         self._rdate.sort()
   1376         self._genitem(rlist, iter(self._rdate))
   1377         for gen in [iter(x) for x in self._rrule]:
   1378             self._genitem(rlist, gen)
   1379         exlist = []
   1380         self._exdate.sort()
   1381         self._genitem(exlist, iter(self._exdate))
   1382         for gen in [iter(x) for x in self._exrule]:
   1383             self._genitem(exlist, gen)
   1384         lastdt = None
   1385         total = 0
   1386         heapq.heapify(rlist)
   1387         heapq.heapify(exlist)
   1388         while rlist:
   1389             ritem = rlist[0]
   1390             if not lastdt or lastdt != ritem.dt:
   1391                 while exlist and exlist[0] < ritem:
   1392                     exitem = exlist[0]
   1393                     advance_iterator(exitem)
   1394                     if exlist and exlist[0] is exitem:
   1395                         heapq.heapreplace(exlist, exitem)
   1396                 if not exlist or ritem != exlist[0]:
   1397                     total += 1
   1398                     yield ritem.dt
   1399                 lastdt = ritem.dt
   1400             advance_iterator(ritem)
   1401             if rlist and rlist[0] is ritem:
   1402                 heapq.heapreplace(rlist, ritem)
   1403         self._len = total
   1404 
   1405 
   1406 class _rrulestr(object):
   1407 
   1408     _freq_map = {"YEARLY": YEARLY,
   1409                  "MONTHLY": MONTHLY,
   1410                  "WEEKLY": WEEKLY,
   1411                  "DAILY": DAILY,
   1412                  "HOURLY": HOURLY,
   1413                  "MINUTELY": MINUTELY,
   1414                  "SECONDLY": SECONDLY}
   1415 
   1416     _weekday_map = {"MO": 0, "TU": 1, "WE": 2, "TH": 3,
   1417                     "FR": 4, "SA": 5, "SU": 6}
   1418 
   1419     def _handle_int(self, rrkwargs, name, value, **kwargs):
   1420         rrkwargs[name.lower()] = int(value)
   1421 
   1422     def _handle_int_list(self, rrkwargs, name, value, **kwargs):
   1423         rrkwargs[name.lower()] = [int(x) for x in value.split(',')]
   1424 
   1425     _handle_INTERVAL = _handle_int
   1426     _handle_COUNT = _handle_int
   1427     _handle_BYSETPOS = _handle_int_list
   1428     _handle_BYMONTH = _handle_int_list
   1429     _handle_BYMONTHDAY = _handle_int_list
   1430     _handle_BYYEARDAY = _handle_int_list
   1431     _handle_BYEASTER = _handle_int_list
   1432     _handle_BYWEEKNO = _handle_int_list
   1433     _handle_BYHOUR = _handle_int_list
   1434     _handle_BYMINUTE = _handle_int_list
   1435     _handle_BYSECOND = _handle_int_list
   1436 
   1437     def _handle_FREQ(self, rrkwargs, name, value, **kwargs):
   1438         rrkwargs["freq"] = self._freq_map[value]
   1439 
   1440     def _handle_UNTIL(self, rrkwargs, name, value, **kwargs):
   1441         global parser
   1442         if not parser:
   1443             from dateutil import parser
   1444         try:
   1445             rrkwargs["until"] = parser.parse(value,
   1446                                              ignoretz=kwargs.get("ignoretz"),
   1447                                              tzinfos=kwargs.get("tzinfos"))
   1448         except ValueError:
   1449             raise ValueError("invalid until date")
   1450 
   1451     def _handle_WKST(self, rrkwargs, name, value, **kwargs):
   1452         rrkwargs["wkst"] = self._weekday_map[value]
   1453 
   1454     def _handle_BYWEEKDAY(self, rrkwargs, name, value, **kwargs):
   1455         """
   1456         Two ways to specify this: +1MO or MO(+1)
   1457         """
   1458         l = []
   1459         for wday in value.split(','):
   1460             if '(' in wday:
   1461                 # If it's of the form TH(+1), etc.
   1462                 splt = wday.split('(')
   1463                 w = splt[0]
   1464                 n = int(splt[1][:-1])
   1465             elif len(wday):
   1466                 # If it's of the form +1MO
   1467                 for i in range(len(wday)):
   1468                     if wday[i] not in '+-0123456789':
   1469                         break
   1470                 n = wday[:i] or None
   1471                 w = wday[i:]
   1472                 if n:
   1473                     n = int(n)
   1474             else:
   1475                 raise ValueError("Invalid (empty) BYDAY specification.")
   1476 
   1477             l.append(weekdays[self._weekday_map[w]](n))
   1478         rrkwargs["byweekday"] = l
   1479 
   1480     _handle_BYDAY = _handle_BYWEEKDAY
   1481 
   1482     def _parse_rfc_rrule(self, line,
   1483                          dtstart=None,
   1484                          cache=False,
   1485                          ignoretz=False,
   1486                          tzinfos=None):
   1487         if line.find(':') != -1:
   1488             name, value = line.split(':')
   1489             if name != "RRULE":
   1490                 raise ValueError("unknown parameter name")
   1491         else:
   1492             value = line
   1493         rrkwargs = {}
   1494         for pair in value.split(';'):
   1495             name, value = pair.split('=')
   1496             name = name.upper()
   1497             value = value.upper()
   1498             try:
   1499                 getattr(self, "_handle_"+name)(rrkwargs, name, value,
   1500                                                ignoretz=ignoretz,
   1501                                                tzinfos=tzinfos)
   1502             except AttributeError:
   1503                 raise ValueError("unknown parameter '%s'" % name)
   1504             except (KeyError, ValueError):
   1505                 raise ValueError("invalid '%s': %s" % (name, value))
   1506         return rrule(dtstart=dtstart, cache=cache, **rrkwargs)
   1507 
   1508     def _parse_rfc(self, s,
   1509                    dtstart=None,
   1510                    cache=False,
   1511                    unfold=False,
   1512                    forceset=False,
   1513                    compatible=False,
   1514                    ignoretz=False,
   1515                    tzids=None,
   1516                    tzinfos=None):
   1517         global parser
   1518         if compatible:
   1519             forceset = True
   1520             unfold = True
   1521 
   1522         TZID_NAMES = dict(map(
   1523             lambda x: (x.upper(), x),
   1524             re.findall('TZID=(?P<name>[^:]+):', s)
   1525         ))
   1526         s = s.upper()
   1527         if not s.strip():
   1528             raise ValueError("empty string")
   1529         if unfold:
   1530             lines = s.splitlines()
   1531             i = 0
   1532             while i < len(lines):
   1533                 line = lines[i].rstrip()
   1534                 if not line:
   1535                     del lines[i]
   1536                 elif i > 0 and line[0] == " ":
   1537                     lines[i-1] += line[1:]
   1538                     del lines[i]
   1539                 else:
   1540                     i += 1
   1541         else:
   1542             lines = s.split()
   1543         if (not forceset and len(lines) == 1 and (s.find(':') == -1 or
   1544                                                   s.startswith('RRULE:'))):
   1545             return self._parse_rfc_rrule(lines[0], cache=cache,
   1546                                          dtstart=dtstart, ignoretz=ignoretz,
   1547                                          tzinfos=tzinfos)
   1548         else:
   1549             rrulevals = []
   1550             rdatevals = []
   1551             exrulevals = []
   1552             exdatevals = []
   1553             for line in lines:
   1554                 if not line:
   1555                     continue
   1556                 if line.find(':') == -1:
   1557                     name = "RRULE"
   1558                     value = line
   1559                 else:
   1560                     name, value = line.split(':', 1)
   1561                 parms = name.split(';')
   1562                 if not parms:
   1563                     raise ValueError("empty property name")
   1564                 name = parms[0]
   1565                 parms = parms[1:]
   1566                 if name == "RRULE":
   1567                     for parm in parms:
   1568                         raise ValueError("unsupported RRULE parm: "+parm)
   1569                     rrulevals.append(value)
   1570                 elif name == "RDATE":
   1571                     for parm in parms:
   1572                         if parm != "VALUE=DATE-TIME":
   1573                             raise ValueError("unsupported RDATE parm: "+parm)
   1574                     rdatevals.append(value)
   1575                 elif name == "EXRULE":
   1576                     for parm in parms:
   1577                         raise ValueError("unsupported EXRULE parm: "+parm)
   1578                     exrulevals.append(value)
   1579                 elif name == "EXDATE":
   1580                     for parm in parms:
   1581                         if parm != "VALUE=DATE-TIME":
   1582                             raise ValueError("unsupported EXDATE parm: "+parm)
   1583                     exdatevals.append(value)
   1584                 elif name == "DTSTART":
   1585                     # RFC 5445 3.8.2.4: The VALUE parameter is optional, but
   1586                     # may be found only once.
   1587                     value_found = False
   1588                     TZID = None
   1589                     valid_values = {"VALUE=DATE-TIME", "VALUE=DATE"}
   1590                     for parm in parms:
   1591                         if parm.startswith("TZID="):
   1592                             try:
   1593                                 tzkey = TZID_NAMES[parm.split('TZID=')[-1]]
   1594                             except KeyError:
   1595                                 continue
   1596                             if tzids is None:
   1597                                 from . import tz
   1598                                 tzlookup = tz.gettz
   1599                             elif callable(tzids):
   1600                                 tzlookup = tzids
   1601                             else:
   1602                                 tzlookup = getattr(tzids, 'get', None)
   1603                                 if tzlookup is None:
   1604                                     msg = ('tzids must be a callable, ' +
   1605                                            'mapping, or None, ' +
   1606                                            'not %s' % tzids)
   1607                                     raise ValueError(msg)
   1608 
   1609                             TZID = tzlookup(tzkey)
   1610                             continue
   1611                         if parm not in valid_values:
   1612                             raise ValueError("unsupported DTSTART parm: "+parm)
   1613                         else:
   1614                             if value_found:
   1615                                 msg = ("Duplicate value parameter found in " +
   1616                                        "DTSTART: " + parm)
   1617                                 raise ValueError(msg)
   1618                             value_found = True
   1619                     if not parser:
   1620                         from dateutil import parser
   1621                     dtstart = parser.parse(value, ignoretz=ignoretz,
   1622                                            tzinfos=tzinfos)
   1623                     if TZID is not None:
   1624                         if dtstart.tzinfo is None:
   1625                             dtstart = dtstart.replace(tzinfo=TZID)
   1626                         else:
   1627                             raise ValueError('DTSTART specifies multiple timezones')
   1628                 else:
   1629                     raise ValueError("unsupported property: "+name)
   1630             if (forceset or len(rrulevals) > 1 or rdatevals
   1631                     or exrulevals or exdatevals):
   1632                 if not parser and (rdatevals or exdatevals):
   1633                     from dateutil import parser
   1634                 rset = rruleset(cache=cache)
   1635                 for value in rrulevals:
   1636                     rset.rrule(self._parse_rfc_rrule(value, dtstart=dtstart,
   1637                                                      ignoretz=ignoretz,
   1638                                                      tzinfos=tzinfos))
   1639                 for value in rdatevals:
   1640                     for datestr in value.split(','):
   1641                         rset.rdate(parser.parse(datestr,
   1642                                                 ignoretz=ignoretz,
   1643                                                 tzinfos=tzinfos))
   1644                 for value in exrulevals:
   1645                     rset.exrule(self._parse_rfc_rrule(value, dtstart=dtstart,
   1646                                                       ignoretz=ignoretz,
   1647                                                       tzinfos=tzinfos))
   1648                 for value in exdatevals:
   1649                     for datestr in value.split(','):
   1650                         rset.exdate(parser.parse(datestr,
   1651                                                  ignoretz=ignoretz,
   1652                                                  tzinfos=tzinfos))
   1653                 if compatible and dtstart:
   1654                     rset.rdate(dtstart)
   1655                 return rset
   1656             else:
   1657                 return self._parse_rfc_rrule(rrulevals[0],
   1658                                              dtstart=dtstart,
   1659                                              cache=cache,
   1660                                              ignoretz=ignoretz,
   1661                                              tzinfos=tzinfos)
   1662 
   1663     def __call__(self, s, **kwargs):
   1664         return self._parse_rfc(s, **kwargs)
   1665 
   1666 
   1667 rrulestr = _rrulestr()
   1668 
   1669 # vim:ts=4:sw=4:et
   1670