Repository: dateutil/dateutil (Python)

Issue:
Parser error with unambiguous date in year 31
I started converting [this test](https://github.com/dateutil/dateutil/blob/master/dateutil/test/test_parser.py#L33) over to a hypothesis-based property test and found the following failure:

```python
from dateutil.parser import parse
from datetime import datetime

dt = datetime(31, 12, 30)
dstr = dt.strftime('%B.%04Y.%d')    # December.0031.30
parse(dt.strftime('%B.%04Y.%d'))     # datetime(30, 12, 31, 0, 0)
```

Pretty low priority, but related to #293. Here's the full hypothesis test that generates these examples:

```python
from datetime import datetime, time
import itertools

from hypothesis.strategies import integers, dates
from hypothesis import given

import pytest

from dateutil.parser import parserinfo, parse


def _get_ybd_formats():
    # If we have a 4-digit year, a non-numeric month (abbreviated or not),
    # and a day (1 or 2 digits), then there is no ambiguity as to which
    # token is a year/month/day.  This holds regardless of what order the
    # terms are in and for each of the separators below.
    try:
        datetime.now().strftime('%-d')
        PLATFORM_HAS_DASH_D = True
    except ValueError:
        PLATFORM_HAS_DASH_D = False

    seps = ['-', ' ', '/', '.']

    year_tokens = ['%04Y']
    month_tokens = ['%b', '%B']
    day_tokens = ['%d']

    if PLATFORM_HAS_DASH_D:
        day_tokens.append('%-d')

    prods = itertools.product(year_tokens, month_tokens, day_tokens)
    perms = [y for x in prods for y in itertools.permutations(x)]
    unambig_fmts = [sep.join(perm) for sep in seps for perm in perms]

    return unambig_fmts


@pytest.mark.parser
@pytest.mark.parametrize('fmt', _get_ybd_formats())
@given(dt=dates())
def test_ybd(dt, fmt):
    dt = datetime.combine(dt, time(0))  # Make this a datetime
    dstr = dt.strftime(fmt)
    res = parse(dstr)
    assert dt == res
```

In the end we may also want to change `_get_ybd_formats` over to a strategy, because I think repeating dozens of hypothesis tests separately is expensive.


Hints:
Could this be related to: https://bugs.python.org/issue13305 ?
@chimeno I don't think so:

```python
>>> parse('December.0031.30')
datetime.datetime(30, 12, 31, 0, 0)
```

Though likely that will cause problems with the implementation of the hypothesis tests as I wrote them.

That said, now that I think about it, is `%B.%Y.%d` a format that we really need to support / worry about? Are there real examples of Month-Year-Day datetimes? @jbrockmendel ?
> That said, now that I think about it, is %B.%Y.%d a format that we really need to support / worry about?

This definitely isn't a high priority format, but its sufficiently unambiguous that it would be nice to get right.  I'll see if I can put together a patch in the next day or two.


Patch:
diff --git a/dateutil/parser/_parser.py b/dateutil/parser/_parser.py
--- a/dateutil/parser/_parser.py
+++ b/dateutil/parser/_parser.py
@@ -458,10 +458,37 @@ def append(self, val, label=None):
                 raise ValueError('Year is already set')
             self.ystridx = len(self) - 1
 
+    def _resolve_from_stridxs(self, strids):
+        """
+        Try to resolve the identities of year/month/day elements using
+        ystridx, mstridx, and dstridx, if enough of these are specified.
+        """
+        if len(self) == 3 and len(strids) == 2:
+            # we can back out the remaining stridx value
+            missing = [x for x in range(3) if x not in strids.values()]
+            key = [x for x in ['y', 'm', 'd'] if x not in strids]
+            assert len(missing) == len(key) == 1
+            key = key[0]
+            val = missing[0]
+            strids[key] = val
+
+        assert len(self) == len(strids)  # otherwise this should not be called
+        out = {key: self[strids[key]] for key in strids}
+        return (out.get('y'), out.get('m'), out.get('d'))
+
     def resolve_ymd(self, yearfirst, dayfirst):
         len_ymd = len(self)
         year, month, day = (None, None, None)
 
+        strids = (('y', self.ystridx),
+                  ('m', self.mstridx),
+                  ('d', self.dstridx))
+
+        strids = {key: val for key, val in strids if val is not None}
+        if (len(self) == len(strids) > 0 or
+                (len(self) == 3 and len(strids) == 2)):
+            return self._resolve_from_stridxs(strids)
+
         mstridx = self.mstridx
 
         if len_ymd > 3:
@@ -470,13 +497,17 @@ def resolve_ymd(self, yearfirst, dayfirst):
             # One member, or two members with a month string
             if mstridx is not None:
                 month = self[mstridx]
-                del self[mstridx]
+                # since mstridx is 0 or 1, self[mstridx-1] always
+                # looks up the other element
+                other = self[mstridx - 1]
+            else:
+                other = self[0]
 
             if len_ymd > 1 or mstridx is None:
-                if self[0] > 31:
-                    year = self[0]
+                if other > 31:
+                    year = other
                 else:
-                    day = self[0]
+                    day = other
 
         elif len_ymd == 2:
             # Two members with numbers


Test patch:
diff --git a/dateutil/test/test_parser.py b/dateutil/test/test_parser.py
--- a/dateutil/test/test_parser.py
+++ b/dateutil/test/test_parser.py
@@ -1106,3 +1106,9 @@ def test_decimal_error(value):
     # constructed with an invalid value
     with pytest.raises(ValueError):
         parse(value)
+
+
+def test_BYd_corner_case():
+    # GH#687
+    res = parse('December.0031.30')
+    assert res == datetime(31, 12, 30)