aboutsummaryrefslogtreecommitdiff
path: root/dateutil/rrule.py
diff options
context:
space:
mode:
Diffstat (limited to 'dateutil/rrule.py')
-rw-r--r--dateutil/rrule.py201
1 files changed, 134 insertions, 67 deletions
diff --git a/dateutil/rrule.py b/dateutil/rrule.py
index ef4607a..20a0c4a 100644
--- a/dateutil/rrule.py
+++ b/dateutil/rrule.py
@@ -337,10 +337,6 @@ class rrule(rrulebase):
Additionally, it supports the following keyword arguments:
- :param cache:
- If given, it must be a boolean value specifying to enable or disable
- caching of results. If you will use the same rrule instance multiple
- times, enabling caching will improve the performance considerably.
:param dtstart:
The recurrence start. Besides being the base for the recurrence,
missing parameters in the final recurrence instances will also be
@@ -357,20 +353,26 @@ class rrule(rrulebase):
from calendar.firstweekday(), and may be modified by
calendar.setfirstweekday().
:param count:
- How many occurrences will be generated.
+ If given, this determines how many occurrences will be generated.
.. note::
- As of version 2.5.0, the use of the ``until`` keyword together
- with the ``count`` keyword is deprecated per RFC-5545 Sec. 3.3.10.
+ As of version 2.5.0, the use of the keyword ``until`` in conjunction
+ with ``count`` is deprecated, to make sure ``dateutil`` is fully
+ compliant with `RFC-5545 Sec. 3.3.10 <https://tools.ietf.org/
+ html/rfc5545#section-3.3.10>`_. Therefore, ``until`` and ``count``
+ **must not** occur in the same call to ``rrule``.
:param until:
- If given, this must be a datetime instance, that will specify the
+ If given, this must be a datetime instance specifying the upper-bound
limit of the recurrence. The last recurrence in the rule is the greatest
datetime that is less than or equal to the value specified in the
``until`` parameter.
.. note::
- As of version 2.5.0, the use of the ``until`` keyword together
- with the ``count`` keyword is deprecated per RFC-5545 Sec. 3.3.10.
+ As of version 2.5.0, the use of the keyword ``until`` in conjunction
+ with ``count`` is deprecated, to make sure ``dateutil`` is fully
+ compliant with `RFC-5545 Sec. 3.3.10 <https://tools.ietf.org/
+ html/rfc5545#section-3.3.10>`_. Therefore, ``until`` and ``count``
+ **must not** occur in the same call to ``rrule``.
:param bysetpos:
If given, it must be either an integer, or a sequence of integers,
positive or negative. Each given integer will specify an occurrence
@@ -387,6 +389,11 @@ class rrule(rrulebase):
:param byyearday:
If given, it must be either an integer, or a sequence of integers,
meaning the year days to apply the recurrence to.
+ :param byeaster:
+ If given, it must be either an integer, or a sequence of integers,
+ positive or negative. Each integer will define an offset from the
+ Easter Sunday. Passing the offset 0 to byeaster will yield the Easter
+ Sunday itself. This is an extension to the RFC specification.
:param byweekno:
If given, it must be either an integer, or a sequence of integers,
meaning the week numbers to apply the recurrence to. Week numbers
@@ -412,11 +419,10 @@ class rrule(rrulebase):
:param bysecond:
If given, it must be either an integer, or a sequence of integers,
meaning the seconds to apply the recurrence to.
- :param byeaster:
- If given, it must be either an integer, or a sequence of integers,
- positive or negative. Each integer will define an offset from the
- Easter Sunday. Passing the offset 0 to byeaster will yield the Easter
- Sunday itself. This is an extension to the RFC specification.
+ :param cache:
+ If given, it must be a boolean value specifying to enable or disable
+ caching of results. If you will use the same rrule instance multiple
+ times, enabling caching will improve the performance considerably.
"""
def __init__(self, freq, dtstart=None,
interval=1, wkst=None, count=None, until=None, bysetpos=None,
@@ -427,7 +433,10 @@ class rrule(rrulebase):
super(rrule, self).__init__(cache)
global easter
if not dtstart:
- dtstart = datetime.datetime.now().replace(microsecond=0)
+ if until and until.tzinfo:
+ dtstart = datetime.datetime.now(tz=until.tzinfo).replace(microsecond=0)
+ else:
+ dtstart = datetime.datetime.now().replace(microsecond=0)
elif not isinstance(dtstart, datetime.datetime):
dtstart = datetime.datetime.fromordinal(dtstart.toordinal())
else:
@@ -1403,7 +1412,52 @@ class rruleset(rrulebase):
self._len = total
+
+
class _rrulestr(object):
+ """ Parses a string representation of a recurrence rule or set of
+ recurrence rules.
+
+ :param s:
+ Required, a string defining one or more recurrence rules.
+
+ :param dtstart:
+ If given, used as the default recurrence start if not specified in the
+ rule string.
+
+ :param cache:
+ If set ``True`` caching of results will be enabled, improving
+ performance of multiple queries considerably.
+
+ :param unfold:
+ If set ``True`` indicates that a rule string is split over more
+ than one line and should be joined before processing.
+
+ :param forceset:
+ If set ``True`` forces a :class:`dateutil.rrule.rruleset` to
+ be returned.
+
+ :param compatible:
+ If set ``True`` forces ``unfold`` and ``forceset`` to be ``True``.
+
+ :param ignoretz:
+ If set ``True``, time zones in parsed strings are ignored and a naive
+ :class:`datetime.datetime` object is returned.
+
+ :param tzids:
+ If given, a callable or mapping used to retrieve a
+ :class:`datetime.tzinfo` from a string representation.
+ Defaults to :func:`dateutil.tz.gettz`.
+
+ :param tzinfos:
+ Additional time zone names / aliases which may be present in a string
+ representation. See :func:`dateutil.parser.parse` for more
+ information.
+
+ :return:
+ Returns a :class:`dateutil.rrule.rruleset` or
+ :class:`dateutil.rrule.rrule`
+ """
_freq_map = {"YEARLY": YEARLY,
"MONTHLY": MONTHLY,
@@ -1505,6 +1559,58 @@ class _rrulestr(object):
raise ValueError("invalid '%s': %s" % (name, value))
return rrule(dtstart=dtstart, cache=cache, **rrkwargs)
+ def _parse_date_value(self, date_value, parms, rule_tzids,
+ ignoretz, tzids, tzinfos):
+ global parser
+ if not parser:
+ from dateutil import parser
+
+ datevals = []
+ value_found = False
+ TZID = None
+
+ for parm in parms:
+ if parm.startswith("TZID="):
+ try:
+ tzkey = rule_tzids[parm.split('TZID=')[-1]]
+ except KeyError:
+ continue
+ if tzids is None:
+ from . import tz
+ tzlookup = tz.gettz
+ elif callable(tzids):
+ tzlookup = tzids
+ else:
+ tzlookup = getattr(tzids, 'get', None)
+ if tzlookup is None:
+ msg = ('tzids must be a callable, mapping, or None, '
+ 'not %s' % tzids)
+ raise ValueError(msg)
+
+ TZID = tzlookup(tzkey)
+ continue
+
+ # RFC 5445 3.8.2.4: The VALUE parameter is optional, but may be found
+ # only once.
+ if parm not in {"VALUE=DATE-TIME", "VALUE=DATE"}:
+ raise ValueError("unsupported parm: " + parm)
+ else:
+ if value_found:
+ msg = ("Duplicate value parameter found in: " + parm)
+ raise ValueError(msg)
+ value_found = True
+
+ for datestr in date_value.split(','):
+ date = parser.parse(datestr, ignoretz=ignoretz, tzinfos=tzinfos)
+ if TZID is not None:
+ if date.tzinfo is None:
+ date = date.replace(tzinfo=TZID)
+ else:
+ raise ValueError('DTSTART/EXDATE specifies multiple timezone')
+ datevals.append(date)
+
+ return datevals
+
def _parse_rfc(self, s,
dtstart=None,
cache=False,
@@ -1577,54 +1683,18 @@ class _rrulestr(object):
raise ValueError("unsupported EXRULE parm: "+parm)
exrulevals.append(value)
elif name == "EXDATE":
- for parm in parms:
- if parm != "VALUE=DATE-TIME":
- raise ValueError("unsupported EXDATE parm: "+parm)
- exdatevals.append(value)
+ exdatevals.extend(
+ self._parse_date_value(value, parms,
+ TZID_NAMES, ignoretz,
+ tzids, tzinfos)
+ )
elif name == "DTSTART":
- # RFC 5445 3.8.2.4: The VALUE parameter is optional, but
- # may be found only once.
- value_found = False
- TZID = None
- valid_values = {"VALUE=DATE-TIME", "VALUE=DATE"}
- for parm in parms:
- if parm.startswith("TZID="):
- try:
- tzkey = TZID_NAMES[parm.split('TZID=')[-1]]
- except KeyError:
- continue
- if tzids is None:
- from . import tz
- tzlookup = tz.gettz
- elif callable(tzids):
- tzlookup = tzids
- else:
- tzlookup = getattr(tzids, 'get', None)
- if tzlookup is None:
- msg = ('tzids must be a callable, ' +
- 'mapping, or None, ' +
- 'not %s' % tzids)
- raise ValueError(msg)
-
- TZID = tzlookup(tzkey)
- continue
- if parm not in valid_values:
- raise ValueError("unsupported DTSTART parm: "+parm)
- else:
- if value_found:
- msg = ("Duplicate value parameter found in " +
- "DTSTART: " + parm)
- raise ValueError(msg)
- value_found = True
- if not parser:
- from dateutil import parser
- dtstart = parser.parse(value, ignoretz=ignoretz,
- tzinfos=tzinfos)
- if TZID is not None:
- if dtstart.tzinfo is None:
- dtstart = dtstart.replace(tzinfo=TZID)
- else:
- raise ValueError('DTSTART specifies multiple timezones')
+ dtvals = self._parse_date_value(value, parms, TZID_NAMES,
+ ignoretz, tzids, tzinfos)
+ if len(dtvals) != 1:
+ raise ValueError("Multiple DTSTART values specified:" +
+ value)
+ dtstart = dtvals[0]
else:
raise ValueError("unsupported property: "+name)
if (forceset or len(rrulevals) > 1 or rdatevals
@@ -1646,10 +1716,7 @@ class _rrulestr(object):
ignoretz=ignoretz,
tzinfos=tzinfos))
for value in exdatevals:
- for datestr in value.split(','):
- rset.exdate(parser.parse(datestr,
- ignoretz=ignoretz,
- tzinfos=tzinfos))
+ rset.exdate(value)
if compatible and dtstart:
rset.rdate(dtstart)
return rset