Repository: dateutil/dateutil (Python)

Issue:
Parser fails to parse 'DDmonYYYY'
```python
>>> dateutil.parser.parse('13NOV2017', fuzzy=True)
datetime.datetime(2017, 4, 13, 0, 0)  # --- today's date

>>> dateutil.parser.parse('13NOV2017', fuzzy=True, dayfirst=True)
datetime.datetime(2017, 4, 13, 0, 0)  # --- today's date
```
Without fuzzy, it raises an error.

These should parse to the proper dates. I am not sure how the tokenization is done, but a possible solution could be to use an alphabet character (in addition to what I think is used - any non-alphanum character)


Hints:
@asishm With `fuzzy` it is basically ignoring `NOV` (though, interestingly, `NOV` is not showing up as a token). I think #326 is related.

`13 NOV 2017` works, if you have any control over the format of the string to parse.
I encountered this issue today also. It would be more manageable if the returned value doesn't default to todays date because at least then we know that something failed and to try something else.

Patch:
diff --git a/dateutil/parser.py b/dateutil/parser.py
--- a/dateutil/parser.py
+++ b/dateutil/parser.py
@@ -398,6 +398,20 @@ def has_month(self):
     def has_day(self):
         return self.dstridx is not None
 
+    def could_be_day(self, value):
+        if self.has_day:
+            return False
+        elif not self.has_month:
+            return 1 <= value <= 31
+        elif not self.has_year:
+            # Be permissive, assume leapyear
+            month = self[self.mstridx]
+            return 1 <= value <= monthrange(2000, month)[1]
+        else:
+            month = self[self.mstridx]
+            year = self[self.ystridx]
+            return 1 <= value <= monthrange(year, month)[1]
+
     @staticmethod
     def token_could_be_year(token, year):
         try:
@@ -719,8 +733,8 @@ def _parse(self, timestr, dayfirst=None, yearfirst=None, fuzzy=False,
             while i < len_l:
 
                 # Check if it's a number
+                value_repr = l[i]
                 try:
-                    value_repr = l[i]
                     value = float(value_repr)
                 except ValueError:
                     value = None
@@ -840,6 +854,9 @@ def _parse(self, timestr, dayfirst=None, yearfirst=None, fuzzy=False,
                         res.hour = self._adjust_ampm(hour, info.ampm(l[i + 1]))
                         i += 1
 
+                    elif ymd.could_be_day(value):
+                        ymd.append(value)
+
                     elif not fuzzy:
                         raise InvalidDatetimeError(timestr)
 


Test patch:
diff --git a/dateutil/test/test_internals.py b/dateutil/test/test_internals.py
new file mode 100644
--- /dev/null
+++ b/dateutil/test/test_internals.py
@@ -0,0 +1,41 @@
+# -*- coding: utf-8 -*-
+"""
+Tests for implementation details, not necessarily part of the user-facing
+API.
+
+The motivating case for these tests is #483, where we want to smoke-test
+code that may be difficult to reach through the standard API calls.
+"""
+
+import unittest
+
+from dateutil.parser import _ymd
+
+
+class TestYMD(unittest.TestCase):
+
+	# @pytest.mark.smoke
+	def test_could_be_day(self):
+		ymd = _ymd('foo bar 124 baz')
+
+		ymd.append(2, 'M')
+		assert ymd.has_month
+		assert not ymd.has_year
+		assert ymd.could_be_day(4)
+		assert not ymd.could_be_day(-6)
+		assert not ymd.could_be_day(32)
+
+		# Assumes leapyear
+		assert ymd.could_be_day(29)
+
+		ymd.append(1999)
+		assert ymd.has_year
+		assert not ymd.could_be_day(29)
+
+		ymd.append(16, 'D')
+		assert ymd.has_day
+		assert not ymd.could_be_day(1)
+
+		ymd = _ymd('foo bar 124 baz')
+		ymd.append(1999)
+		assert ymd.could_be_day(31)
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
@@ -872,6 +872,18 @@ def test_idx_check(self):
         res = parse(dtstr, fuzzy=True)
         self.assertEqual(res, datetime(2017, 7, 17, 6, 15))
 
+    def test_dBY(self):
+        # See GH360
+        dtstr = '13NOV2017'
+        res = parse(dtstr)
+        self.assertEqual(res, datetime(2017, 11, 13))
+
+    def test_hmBY(self):
+        # See GH#483
+        dtstr = '02:17NOV2017'
+        res = parse(dtstr, default=self.default)
+        self.assertEqual(res, datetime(2017, 11, self.default.day, 2, 17))
+
     def test_validate_hour(self):
         # See GH353
         invalid = "201A-01-01T23:58:39.239769+03:00"