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