codekingpro/portable-devtools
114k
1# -*- coding: utf-8 -*-2"""3The rrule module offers a small, complete, and very fast, implementation of4the recurrence rules documented in the5`iCalendar RFC <https://tools.ietf.org/html/rfc5545>`_,6including support for caching of results.7"""8import calendar9import datetime10import heapq11import itertools12import re13import sys14from functools import wraps15# For warning about deprecation of until and count16from warnings import warn17 18from six import advance_iterator, integer_types19 20from six.moves import _thread, range21 22from ._common import weekday as weekdaybase23 24try:25 from math import gcd26except ImportError:27 from fractions import gcd28 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.35M366MASK = 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)37M365MASK = list(M366MASK)38M29, M30, M31 = list(range(1, 30)), list(range(1, 31)), list(range(1, 32))39MDAY366MASK = tuple(M31+M29+M31+M30+M31+M30+M31+M31+M30+M31+M30+M31+M31[:7])40MDAY365MASK = list(MDAY366MASK)41M29, M30, M31 = list(range(-29, 0)), list(range(-30, 0)), list(range(-31, 0))42NMDAY366MASK = tuple(M31+M29+M31+M30+M31+M30+M31+M31+M30+M31+M30+M31+M31[:7])43NMDAY365MASK = list(NMDAY366MASK)44M366RANGE = (0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366)45M365RANGE = (0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365)46WDAYMASK = [0, 1, 2, 3, 4, 5, 6]*5547del M29, M30, M31, M365MASK[59], MDAY365MASK[59], NMDAY365MASK[31]48MDAY365MASK = tuple(MDAY365MASK)49M365MASK = tuple(M365MASK)50 51FREQNAMES = ['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.62easter = None63parser = None64 65 66class 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 77MO, TU, WE, TH, FR, SA, SU = weekdays = tuple(weekday(x) for x in range(7))78 79 80def _invalidates_cache(f):81 """82 Decorator for rruleset methods which may invalidate the83 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 rv90 91 return inner_func92 93 94class 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 = None102 self._cache_complete = False103 self._len = None104 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 = False117 self._cache_gen = self._iter()118 119 if self._cache_lock.locked():120 self._cache_lock.release()121 122 self._len = None123 124 def _iter_cached(self):125 i = 0126 gen = self._cache_gen127 cache = self._cache128 acquire = self._cache_lock.acquire129 release = self._cache_lock.release130 while gen:131 if i == len(cache):132 acquire()133 if self._cache_complete:134 break135 try:136 for j in range(10):137 cache.append(advance_iterator(gen))138 except StopIteration:139 self._cache_gen = gen = None140 self._cache_complete = True141 break142 release()143 yield cache[i]144 i += 1145 while i < self._len:146 yield cache[i]147 i += 1148 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 IndexError167 return res168 else:169 return list(iter(self))[item]170 171 def __contains__(self, item):172 if self._cache_complete:173 return item in self._cache174 else:175 for i in self:176 if i == item:177 return True178 elif i > item:179 return False180 return False181 182 # __len__() introduces a large performance penalty.183 def count(self):184 """ Returns the number of recurrences in this set. It will have go185 through the whole recurrence, if this hasn't been done before. """186 if self._len is None:187 for x in self:188 pass189 return self._len190 191 def before(self, dt, inc=False):192 """ Returns the last recurrence before the given datetime instance. The193 inc keyword defines what happens if dt is an occurrence. With194 inc=True, if dt itself is an occurrence, it will be returned. """195 if self._cache_complete:196 gen = self._cache197 else:198 gen = self199 last = None200 if inc:201 for i in gen:202 if i > dt:203 break204 last = i205 else:206 for i in gen:207 if i >= dt:208 break209 last = i210 return last211 212 def after(self, dt, inc=False):213 """ Returns the first recurrence after the given datetime instance. The214 inc keyword defines what happens if dt is an occurrence. With215 inc=True, if dt itself is an occurrence, it will be returned. """216 if self._cache_complete:217 gen = self._cache218 else:219 gen = self220 if inc:221 for i in gen:222 if i >= dt:223 return i224 else:225 for i in gen:226 if i > dt:227 return i228 return None229 230 def xafter(self, dt, count=None, inc=False):231 """232 Generator which yields up to `count` recurrences after the given233 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 is244 included in the output.245 246 :yields: Yields a sequence of `datetime` objects.247 """248 249 if self._cache_complete:250 gen = self._cache251 else:252 gen = self253 254 # Select the comparison function255 if inc:256 comp = lambda dc, dtc: dc >= dtc257 else:258 comp = lambda dc, dtc: dc > dtc259 260 # Generate dates261 n = 0262 for d in gen:263 if comp(d, dt):264 if count is not None:265 n += 1266 if n > count:267 break268 269 yield d270 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 are274 themselves occurrences. With inc=True, they will be included in the275 list, if they are found in the recurrence set. """276 if self._cache_complete:277 gen = self._cache278 else:279 gen = self280 started = False281 l = []282 if inc:283 for i in gen:284 if i > before:285 break286 elif not started:287 if i >= after:288 started = True289 l.append(i)290 else:291 l.append(i)292 else:293 for i in gen:294 if i >= before:295 break296 elif not started:297 if i > after:298 started = True299 l.append(i)300 else:301 l.append(i)302 return l303 304 305class rrule(rrulebase):306 """307 That's the base of the rrule operation. It accepts all the keywords308 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 dates318 and times are ignored rather than coerced:319 320 Recurrence rules may generate recurrence instances with an invalid321 date (e.g., February 30) or nonexistent local time (e.g., 1:30 AM322 on a day where the local time is moved forward by an hour at 1:00323 AM). Such recurrence instances MUST be ignored and MUST NOT be324 counted as part of the recurrence set.325 326 This can lead to possibly surprising behavior when, for example, the327 start date occurs at the end of the month:328 329 >>> from dateutil.rrule import rrule, MONTHLY330 >>> from datetime import datetime331 >>> start_date = datetime(2014, 12, 31)332 >>> list(rrule(freq=MONTHLY, count=4, dtstart=start_date))333 ... # doctest: +NORMALIZE_WHITESPACE334 [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 be344 extracted from this date. If not given, datetime.now() will be used345 instead.346 :param interval:347 The interval between each freq iteration. For example, when using348 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 an352 integer, specifying the first day of the week. This will affect353 recurrences based on weekly periods. The default week start is got354 from calendar.firstweekday(), and may be modified by355 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 conjunction361 with ``count`` is deprecated, to make sure ``dateutil`` is fully362 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-bound367 limit of the recurrence. The last recurrence in the rule is the greatest368 datetime that is less than or equal to the value specified in the369 ``until`` parameter.370 371 .. note::372 As of version 2.5.0, the use of the keyword ``until`` in conjunction373 with ``count`` is deprecated, to make sure ``dateutil`` is fully374 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 occurrence380 number, corresponding to the nth occurrence of the rule inside the381 frequency period. For example, a bysetpos of -1 if combined with a382 MONTHLY frequency, and a byweekday of (MO, TU, WE, TH, FR), will383 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 the396 Easter Sunday. Passing the offset 0 to byeaster will yield the Easter397 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 numbers401 have the meaning described in ISO8601, that is, the first week of402 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 of405 integers, one of the weekday constants (MO, TU, etc), or a sequence406 of these constants. When given, these variables will define the407 weekdays where the recurrence will be applied. It's also possible to408 use an argument n for the weekday instances, which will mean the nth409 occurrence of this weekday in the period. For example, with MONTHLY,410 or with YEARLY and BYMONTH, using FR(+1) in byweekday will specify the411 first friday of the month where the recurrence happens. Notice that in412 the RFC documentation, this is specified as BYDAY, but was renamed to413 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 disable425 caching of results. If you will use the same rrule instance multiple426 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 easter436 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 = dtstart446 self._tzinfo = dtstart.tzinfo447 self._freq = freq448 self._interval = interval449 self._count = count450 451 # Cache the original byxxx rules, if they are provided, as the _byxxx452 # attributes do not necessarily map to the inputs, and this can be453 # a problem in generating the strings. Only store things if they've454 # 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 = until460 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.10465 #466 # > If the "DTSTART" property is specified as a date with UTC467 # > time or a date with local time and time zone reference,468 # > then the UNTIL rule part MUST be specified as a date with469 # > 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 = wkst484 else:485 self._wkst = wkst.weekday486 487 if bysetpos is None:488 self._bysetpos = None489 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._bysetpos503 504 if (byweekno is None and byyearday is None and bymonthday is None and505 byweekday is None and byeaster is None):506 if freq == YEARLY:507 if bymonth is None:508 bymonth = dtstart.month509 self._original_rule['bymonth'] = None510 bymonthday = dtstart.day511 self._original_rule['bymonthday'] = None512 elif freq == MONTHLY:513 bymonthday = dtstart.day514 self._original_rule['bymonthday'] = None515 elif freq == WEEKLY:516 byweekday = dtstart.weekday()517 self._original_rule['byweekday'] = None518 519 # bymonth520 if bymonth is None:521 self._bymonth = None522 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._bymonth530 531 # byyearday532 if byyearday is None:533 self._byyearday = None534 else:535 if isinstance(byyearday, integer_types):536 byyearday = (byyearday,)537 538 self._byyearday = tuple(sorted(set(byyearday)))539 self._original_rule['byyearday'] = self._byyearday540 541 # byeaster542 if byeaster is not None:543 if not easter:544 from dateutil import easter545 if isinstance(byeaster, integer_types):546 self._byeaster = (byeaster,)547 else:548 self._byeaster = tuple(sorted(byeaster))549 550 self._original_rule['byeaster'] = self._byeaster551 else:552 self._byeaster = None553 554 # bymonthday555 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 unique563 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 numbers568 if 'bymonthday' not in self._original_rule:569 self._original_rule['bymonthday'] = tuple(570 itertools.chain(self._bymonthday, self._bynmonthday))571 572 # byweekno573 if byweekno is None:574 self._byweekno = None575 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._byweekno582 583 # byweekday / bynweekday584 if byweekday is None:585 self._byweekday = None586 self._bynweekday = None587 else:588 # If it's one of the valid non-sequence types, convert to a589 # single-element sequence before the iterator that builds the590 # 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 = None606 elif not self._bynweekday:607 self._bynweekday = None608 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 # byhour626 if byhour is None:627 if freq < HOURLY:628 self._byhour = {dtstart.hour}629 else:630 self._byhour = None631 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._byhour644 645 # byminute646 if byminute is None:647 if freq < MINUTELY:648 self._byminute = {dtstart.minute}649 else:650 self._byminute = None651 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._byminute664 665 # bysecond666 if bysecond is None:667 if freq < SECONDLY:668 self._bysecond = ((dtstart.second,))669 else:670 self._bysecond = None671 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._bysecond686 687 if self._freq >= HOURLY:688 self._timeset = None689 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 the704 dateutil-specific extension BYEASTER.705 """706 707 output = []708 h, m, s = [None] * 3709 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 generate728 # 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_strings740 else:741 original_rule = self._original_rule742 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 new764 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 bit781 freq = self._freq782 interval = self._interval783 wkst = self._wkst784 until = self._until785 bymonth = self._bymonth786 byweekno = self._byweekno787 byyearday = self._byyearday788 byweekday = self._byweekday789 byeaster = self._byeaster790 bymonthday = self._bymonthday791 bynmonthday = self._bynmonthday792 bysetpos = self._bysetpos793 byhour = self._byhour794 byminute = self._byminute795 bysecond = self._bysecond796 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._timeset810 else:811 gettimeset = {HOURLY: ii.htimeset,812 MINUTELY: ii.mtimeset,813 SECONDLY: ii.stimeset}[freq]814 if ((freq >= HOURLY and815 self._byhour and hour not in self._byhour) or816 (freq >= MINUTELY and817 self._byminute and minute not in self._byminute) or818 (freq >= SECONDLY and819 self._bysecond and second not in self._bysecond)):820 timeset = ()821 else:822 timeset = gettimeset(hour, minute, second)823 824 total = 0825 count = self._count826 while True:827 # Get dayset with the right frequency828 dayset, start, end = getdayset(year, month, day)829 830 # Do the "hard" work ;-)831 filtered = False832 for i in dayset[start:end]:833 if ((bymonth and ii.mmask[i] not in bymonth) or834 (byweekno and not ii.wnomask[i]) or835 (byweekday and ii.wdaymask[i] not in byweekday) or836 (ii.nwdaymask and not ii.nwdaymask[i]) or837 (byeaster and not ii.eastermask[i]) or838 ((bymonthday or bynmonthday) and839 ii.mdaymask[i] not in bymonthday and840 ii.nmdaymask[i] not in bynmonthday) or841 (byyearday and842 ((i < ii.yearlen and i+1 not in byyearday and843 -ii.yearlen+i not in byyearday) or844 (i >= ii.yearlen and i+1-ii.yearlen not in byyearday and845 -ii.nextyearlen+i-ii.yearlen not in byyearday)))):846 dayset[i] = None847 filtered = True848 849 # Output results850 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 pass863 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 = total872 return873 elif res >= self._dtstart:874 if count is not None:875 count -= 1876 if count < 0:877 self._len = total878 return879 total += 1880 yield res881 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 = total889 return890 elif res >= self._dtstart:891 if count is not None:892 count -= 1893 if count < 0:894 self._len = total895 return896 897 total += 1898 yield res899 900 # Handle frequency and interval901 fixday = False902 if freq == YEARLY:903 year += interval904 if year > datetime.MAXYEAR:905 self._len = total906 return907 ii.rebuild(year, month)908 elif freq == MONTHLY:909 month += interval910 if month > 12:911 div, mod = divmod(month, 12)912 month = mod913 year += div914 if month == 0:915 month = 12916 year -= 1917 if year > datetime.MAXYEAR:918 self._len = total919 return920 ii.rebuild(year, month)921 elif freq == WEEKLY:922 if wkst > weekday:923 day += -(weekday+1+(6-wkst))+self._interval*7924 else:925 day += -(weekday-wkst)+self._interval*7926 weekday = wkst927 fixday = True928 elif freq == DAILY:929 day += interval930 fixday = True931 elif freq == HOURLY:932 if filtered:933 # Jump to one iteration before next day934 hour += ((23-hour)//interval)*interval935 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 += ndays945 fixday = True946 947 timeset = gettimeset(hour, minute, second)948 elif freq == MINUTELY:949 if filtered:950 # Jump to one iteration before next day951 minute += ((1439-(hour*60+minute))//interval)*interval952 953 valid = False954 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 += div967 fixday = True968 filtered = False969 970 if not byhour or hour in byhour:971 valid = True972 break973 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 day982 second += (((86399 - (hour * 3600 + minute * 60 + second))983 // interval) * interval)984 985 rep_rate = (24 * 3600)986 valid = False987 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 += div999 div, hour = divmod(hour, 24)1000 if div:1001 day += div1002 fixday = True1003 1004 if ((not byhour or hour in byhour) and1005 (not byminute or minute in byminute) and1006 (not bysecond or second in bysecond)):1007 valid = True1008 break1009 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 -= daysinmonth1022 month += 11023 if month == 13:1024 month = 11025 year += 11026 if year > datetime.MAXYEAR:1027 self._len = total1028 return1029 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 as1035 `FREQ` (e.g. `FREQ=HOURLY,BYHOUR={2,4,7},INTERVAL=3`), there are some1036 specifications which cannot be reached given some starting conditions.1037 1038 This occurs whenever the interval is not coprime with the base of a1039 given unit and the difference between the starting position and the1040 ending position is not coprime with the greatest common denominator1041 between the interval and the base. For example, with a FREQ of hourly1042 starting at 17:00 and an interval of 4, the only valid values for1043 BYHOUR would be {21, 1, 5, 9, 13, 17}, because 4 and 24 are not1044 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, since1055 the values should be unique and the order is irrelevant, this will1056 speed up later lookups.1057 1058 In the event of an empty set, raises a :exception:`ValueError`, as this1059 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 cset1078 1079 def __mod_distance(self, value, byxxx, base):1080 """1081 Calculates the next value in a sequence where the `FREQ` parameter is1082 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 by1089 `rrule._construct_byset`, or something else which checks that a1090 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 maximum1096 number before the sequence would start to repeat), this raises a1097 :exception:`ValueError`, as no valid values were found.1098 1099 This returns a tuple of `divmod(n*interval, base)`, where `n` is the1100 smallest number of `interval` repetitions until the next specified1101 value in `byxxx` is found.1102 """1103 accumulator = 01104 for ii in range(1, base + 1):1105 # Using divmod() over % to account for negative intervals1106 div, value = divmod(value + self._interval, base)1107 accumulator += div1108 if value in byxxx:1109 return (accumulator, value)1110 1111 1112class _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 = rrule1122 1123 def rebuild(self, year, month):1124 # Every mask is 7 days longer to handle cross-year weekly periods.1125 rr = self.rrule1126 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 = M365MASK1136 self.mdaymask = MDAY365MASK1137 self.nmdaymask = NMDAY365MASK1138 self.wdaymask = WDAYMASK[wday:]1139 self.mrange = M365RANGE1140 else:1141 self.mmask = M366MASK1142 self.mdaymask = MDAY366MASK1143 self.nmdaymask = NMDAY366MASK1144 self.wdaymask = WDAYMASK[wday:]1145 self.mrange = M366RANGE1146 1147 if not rr._byweekno:1148 self.wnomask = None1149 else:1150 self.wnomask = [0]*(self.yearlen+7)1151 # no1wkst = firstwkst = self.wdaymask.index(rr._wkst)1152 no1wkst = firstwkst = (7-self.yearweekday+rr._wkst) % 71153 if no1wkst >= 4:1154 no1wkst = 01155 # Number of days in the year, plus the days we got1156 # from last year.1157 wyearlen = self.yearlen+(self.yearweekday-rr._wkst) % 71158 else:1159 # Number of days in the year, minus the days we1160 # left in last year.1161 wyearlen = self.yearlen-no1wkst1162 div, mod = divmod(wyearlen, 7)1163 numweeks = div+mod//41164 for n in rr._byweekno:1165 if n < 0:1166 n += numweeks+11167 if not (0 < n <= numweeks):1168 continue1169 if n > 1:1170 i = no1wkst+(n-1)*71171 if no1wkst != firstwkst:1172 i -= 7-firstwkst1173 else:1174 i = no1wkst1175 for j in range(7):1176 self.wnomask[i] = 11177 i += 11178 if self.wdaymask[i] == rr._wkst:1179 break1180 if 1 in rr._byweekno:1181 # Check week number 1 of next year as well1182 # TODO: Check -numweeks for next year.1183 i = no1wkst+numweeks*71184 if no1wkst != firstwkst:1185 i -= 7-firstwkst1186 if i < self.yearlen:1187 # If week starts in next year, we1188 # don't care about it.1189 for j in range(7):1190 self.wnomask[i] = 11191 i += 11192 if self.wdaymask[i] == rr._wkst:1193 break1194 if no1wkst:1195 # Check last week number of last year as1196 # well. If no1wkst is 0, either the year1197 # started on week start, or week number 11198 # got days from last year, so there are no1199 # days from last year's last week number in1200 # this year.