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