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