aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Ganssle <pganssle@users.noreply.github.com>2019-02-04 09:51:31 -0500
committerGitHub <noreply@github.com>2019-02-04 09:51:31 -0500
commit9b9d84dde19b8f0b4b568977079eead975941c87 (patch)
treee3c3ddd677bad304ed2e72163277b9945d32f2f7
parentb9aa7f0bf92961bfd91e36bbaff2d4c8c5abaf51 (diff)
parent399725924c561d3729c849d9297f92e5c5543df4 (diff)
downloaddateutil-9b9d84dde19b8f0b4b568977079eead975941c87.tar.gz
Merge pull request #859 from nicoe/exdate-params
Add parameters parsing to EXDATE
-rw-r--r--AUTHORS.md1
-rw-r--r--changelog.d/410.feature.rst2
-rw-r--r--dateutil/rrule.py119
-rw-r--r--dateutil/test/test_rrule.py73
4 files changed, 143 insertions, 52 deletions
diff --git a/AUTHORS.md b/AUTHORS.md
index fcbb7ce..5628440 100644
--- a/AUTHORS.md
+++ b/AUTHORS.md
@@ -71,6 +71,7 @@ switch, and thus all their contributions are dual-licensed.
- Michael J. Schultz <mjschultz@MASKED>
- Mike Gilbert <floppym@MASKED>
- Nicholas Herrriot <Nicholas.Herriot@gmail.com> **D**
+- Nicolas Évrard (gh: @nicoe) **D**
- Nick Smith <nick.smith@MASKED>
- Orson Adams <orson.network@MASKED> (gh: @parsethis) **D**
- Paul Dickson (gh @prdickson) **D**
diff --git a/changelog.d/410.feature.rst b/changelog.d/410.feature.rst
new file mode 100644
index 0000000..5e5e0d1
--- /dev/null
+++ b/changelog.d/410.feature.rst
@@ -0,0 +1,2 @@
+Added support for EXDATE parameters when parsing rrule strings.
+Reported by @mlorant (gh issue #410), fixed by @nicoe (gh pr #859).
diff --git a/dateutil/rrule.py b/dateutil/rrule.py
index ea450f2..20a0c4a 100644
--- a/dateutil/rrule.py
+++ b/dateutil/rrule.py
@@ -435,7 +435,7 @@ class rrule(rrulebase):
if not dtstart:
if until and until.tzinfo:
dtstart = datetime.datetime.now(tz=until.tzinfo).replace(microsecond=0)
- else:
+ else:
dtstart = datetime.datetime.now().replace(microsecond=0)
elif not isinstance(dtstart, datetime.datetime):
dtstart = datetime.datetime.fromordinal(dtstart.toordinal())
@@ -1412,6 +1412,8 @@ class rruleset(rrulebase):
self._len = total
+
+
class _rrulestr(object):
""" Parses a string representation of a recurrence rule or set of
recurrence rules.
@@ -1557,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,
@@ -1629,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
@@ -1698,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
diff --git a/dateutil/test/test_rrule.py b/dateutil/test/test_rrule.py
index cd08ce2..9dfa544 100644
--- a/dateutil/test/test_rrule.py
+++ b/dateutil/test/test_rrule.py
@@ -2853,6 +2853,74 @@ class RRuleTest(WarningTestMixin, unittest.TestCase):
datetime(1997, 9, 9, 9, 0),
datetime(1997, 9, 16, 9, 0)])
+ def testStrSetExDateMultiple(self):
+ rrstr = ("DTSTART:19970902T090000\n"
+ "RRULE:FREQ=YEARLY;COUNT=6;BYDAY=TU,TH\n"
+ "EXDATE:19970904T090000,19970911T090000,19970918T090000\n")
+
+ rr = rrulestr(rrstr)
+ assert list(rr) == [datetime(1997, 9, 2, 9, 0),
+ datetime(1997, 9, 9, 9, 0),
+ datetime(1997, 9, 16, 9, 0)]
+
+ def testStrSetExDateWithTZID(self):
+ BXL = tz.gettz('Europe/Brussels')
+ rr = rrulestr("DTSTART;TZID=Europe/Brussels:19970902T090000\n"
+ "RRULE:FREQ=YEARLY;COUNT=6;BYDAY=TU,TH\n"
+ "EXDATE;TZID=Europe/Brussels:19970904T090000\n"
+ "EXDATE;TZID=Europe/Brussels:19970911T090000\n"
+ "EXDATE;TZID=Europe/Brussels:19970918T090000\n")
+
+ assert list(rr) == [datetime(1997, 9, 2, 9, 0, tzinfo=BXL),
+ datetime(1997, 9, 9, 9, 0, tzinfo=BXL),
+ datetime(1997, 9, 16, 9, 0, tzinfo=BXL)]
+
+ def testStrSetExDateValueDateTimeNoTZID(self):
+ rrstr = '\n'.join([
+ "DTSTART:19970902T090000",
+ "RRULE:FREQ=YEARLY;COUNT=4;BYDAY=TU,TH",
+ "EXDATE;VALUE=DATE-TIME:19970902T090000",
+ "EXDATE;VALUE=DATE-TIME:19970909T090000",
+ ])
+
+ rr = rrulestr(rrstr)
+ assert list(rr) == [datetime(1997, 9, 4, 9), datetime(1997, 9, 11, 9)]
+
+ def testStrSetExDateValueMixDateTimeNoTZID(self):
+ rrstr = '\n'.join([
+ "DTSTART:19970902T090000",
+ "RRULE:FREQ=YEARLY;COUNT=4;BYDAY=TU,TH",
+ "EXDATE;VALUE=DATE-TIME:19970902T090000",
+ "EXDATE:19970909T090000",
+ ])
+
+ rr = rrulestr(rrstr)
+ assert list(rr) == [datetime(1997, 9, 4, 9), datetime(1997, 9, 11, 9)]
+
+ def testStrSetExDateValueDateTimeWithTZID(self):
+ BXL = tz.gettz('Europe/Brussels')
+ rrstr = '\n'.join([
+ "DTSTART;VALUE=DATE-TIME;TZID=Europe/Brussels:19970902T090000",
+ "RRULE:FREQ=YEARLY;COUNT=4;BYDAY=TU,TH",
+ "EXDATE;VALUE=DATE-TIME;TZID=Europe/Brussels:19970902T090000",
+ "EXDATE;VALUE=DATE-TIME;TZID=Europe/Brussels:19970909T090000",
+ ])
+
+ rr = rrulestr(rrstr)
+ assert list(rr) == [datetime(1997, 9, 4, 9, tzinfo=BXL),
+ datetime(1997, 9, 11, 9, tzinfo=BXL)]
+
+ def testStrSetExDateValueDate(self):
+ rrstr = '\n'.join([
+ "DTSTART;VALUE=DATE:19970902",
+ "RRULE:FREQ=YEARLY;COUNT=4;BYDAY=TU,TH",
+ "EXDATE;VALUE=DATE:19970902",
+ "EXDATE;VALUE=DATE:19970909",
+ ])
+
+ rr = rrulestr(rrstr)
+ assert list(rr) == [datetime(1997, 9, 4), datetime(1997, 9, 11)]
+
def testStrSetDateAndExDate(self):
self.assertEqual(list(rrulestr(
"DTSTART:19970902T090000\n"
@@ -2929,6 +2997,11 @@ class RRuleTest(WarningTestMixin, unittest.TestCase):
self.assertEqual(list(rr), [datetime(1997, 9, 2, 0, 0, 0),
datetime(1998, 9, 2, 0, 0, 0)])
+ def testStrMultipleDTStartComma(self):
+ with pytest.raises(ValueError):
+ rr = rrulestr("DTSTART:19970101T000000,19970202T000000\n"
+ "RRULE:FREQ=YEARLY;COUNT=1")
+
def testStrInvalidUntil(self):
with self.assertRaises(ValueError):
list(rrulestr("DTSTART:19970902T090000\n"