Repository: pandas-dev/pandas

Issue:
ENH: make period_range parse strings like '2012-Q1', not just '2012Q1'
This format is used, e.g., in datasets from the ECB. 

Code example:

In [25]: PD.period_range(start = '2012Q1', periods = 4, freq = 'Q')
Out[25]:
<class 'pandas.tseries.period.PeriodIndex'>
[2012Q1, ..., 2012Q4]
Length: 4, Freq: Q-DEC
## In [26]: PD.period_range(start = '2012-Q1', periods = 4, freq = 'Q')

ValueError                                Traceback (most recent call last)
C:\Users\stefan\Anaconda\envs\py343\lib\site-packages\pandas\tseries\tools.py in
 parse_time_string(arg, freq, dayfirst, yearfirst)
    463         parsed, reso = dateutil_parse(arg, default, dayfirst=dayfirst,
--> 464                                       yearfirst=yearfirst)
    465     except Exception as e:

C:\Users\stefan\Anaconda\envs\py343\lib\site-packages\pandas\tseries\tools.py in
 dateutil_parse(timestr, default, ignoretz, tzinfos, **kwargs)
    489     if res is None:
--> 490         raise ValueError("unknown string format")
    491

ValueError: unknown string format

During handling of the above exception, another exception occurred:

DateParseError                            Traceback (most recent call last)
<ipython-input-26-a960a544a087> in <module>()
----> 1 PD.period_range(start = '2012-Q1', periods = 4, freq = 'Q')

C:\Users\stefan\Anaconda\envs\py343\lib\site-packages\pandas\tseries\period.py i
n period_range(start, end, periods, freq, name)
   1433     """
   1434     return PeriodIndex(start=start, end=end, periods=periods,
-> 1435                        freq=freq, name=name)

C:\Users\stefan\Anaconda\envs\py343\lib\site-packages\pandas\tseries\period.py i
n **new**(cls, data, ordinal, freq, start, end, periods, copy, name, tz, **kwarg
s)
    637             else:
    638                 data, freq = cls._generate_range(start, end, periods,
--> 639                                                  freq, kwargs)
    640         else:
    641             ordinal, freq = cls._from_arraylike(data, freq, tz)

C:\Users\stefan\Anaconda\envs\py343\lib\site-packages\pandas\tseries\period.py i
n _generate_range(cls, start, end, periods, freq, fields)
    651                 raise ValueError('Can either instantiate from fields '
    652                                  'or endpoints, but not both')
--> 653             subarr, freq = _get_ordinal_range(start, end, periods, freq)

```
654         elif field_count > 0:
655             subarr, freq = _range_from_fields(freq=freq, **fields)
```

C:\Users\stefan\Anaconda\envs\py343\lib\site-packages\pandas\tseries\period.py i
n _get_ordinal_range(start, end, periods, freq)
   1281
   1282     if start is not None:
-> 1283         start = Period(start, freq)
   1284     if end is not None:
   1285         end = Period(end, freq)

C:\Users\stefan\Anaconda\envs\py343\lib\site-packages\pandas\tseries\period.py i
n **init**(self, value, freq, ordinal, year, month, quarter, day, hour, minute,
second)
    124             value = value.upper()
    125
--> 126             dt, _, reso = parse_time_string(value, freq)
    127             if freq is None:
    128                 try:

C:\Users\stefan\Anaconda\envs\py343\lib\site-packages\pandas\tseries\tools.py in
 parse_time_string(arg, freq, dayfirst, yearfirst)
    465     except Exception as e:
    466         # TODO: allow raise of errors within instead
--> 467         raise DateParseError(e)
    468
    469     if parsed is None:

DateParseError: unknown string format



Discussion:
seems like a straightforward extension, pull requests welcome!


Resolving patch:
diff --git a/pandas/tseries/tools.py b/pandas/tseries/tools.py
--- a/pandas/tseries/tools.py
+++ b/pandas/tseries/tools.py
@@ -383,10 +383,10 @@ def calc_with_mask(carg,mask):
     return None
 
 # patterns for quarters like '4Q2005', '05Q1'
-qpat1full = re.compile(r'(\d)Q(\d\d\d\d)')
-qpat2full = re.compile(r'(\d\d\d\d)Q(\d)')
-qpat1 = re.compile(r'(\d)Q(\d\d)')
-qpat2 = re.compile(r'(\d\d)Q(\d)')
+qpat1full = re.compile(r'(\d)Q-?(\d\d\d\d)')
+qpat2full = re.compile(r'(\d\d\d\d)-?Q(\d)')
+qpat1 = re.compile(r'(\d)Q-?(\d\d)')
+qpat2 = re.compile(r'(\d\d)-?Q(\d)')
 ypat = re.compile(r'(\d\d\d\d)$')
 has_time = re.compile('(.+)([\s]|T)+(.+)')
 
@@ -424,18 +424,18 @@ def parse_time_string(arg, freq=None, dayfirst=None, yearfirst=None):
                                         second=0, microsecond=0)
 
     # special handling for possibilities eg, 2Q2005, 2Q05, 2005Q1, 05Q1
-    if len(arg) in [4, 6]:
+    if len(arg) in [4, 5, 6, 7]:
         m = ypat.match(arg)
         if m:
             ret = default.replace(year=int(m.group(1)))
             return ret, ret, 'year'
 
         add_century = False
-        if len(arg) == 4:
+        if len(arg) > 5:
+            qpats = [(qpat1full, 1), (qpat2full, 0)]
+        else:
             add_century = True
             qpats = [(qpat1, 1), (qpat2, 0)]
-        else:
-            qpats = [(qpat1full, 1), (qpat2full, 0)]
 
         for pat, yfirst in qpats:
             qparse = pat.match(arg)


Test patch:
diff --git a/pandas/tseries/tests/test_offsets.py b/pandas/tseries/tests/test_offsets.py
--- a/pandas/tseries/tests/test_offsets.py
+++ b/pandas/tseries/tests/test_offsets.py
@@ -21,7 +21,7 @@
 from pandas import Series
 from pandas.tseries.frequencies import _offset_map
 from pandas.tseries.index import _to_m8, DatetimeIndex, _daterange_cache, date_range
-from pandas.tseries.tools import parse_time_string
+from pandas.tseries.tools import parse_time_string, DateParseError
 import pandas.tseries.offsets as offsets
 
 from pandas.io.pickle import read_pickle
@@ -3010,12 +3010,33 @@ def test_get_offset():
                                     (name, expected, offset))
 
 
-def test_parse_time_string():
-    (date, parsed, reso) = parse_time_string('4Q1984')
-    (date_lower, parsed_lower, reso_lower) = parse_time_string('4q1984')
-    assert date == date_lower
-    assert parsed == parsed_lower
-    assert reso == reso_lower
+class TestParseTimeString(tm.TestCase):
+
+    def test_parse_time_string(self):
+        (date, parsed, reso) = parse_time_string('4Q1984')
+        (date_lower, parsed_lower, reso_lower) = parse_time_string('4q1984')
+        self.assertEqual(date, date_lower)
+        self.assertEqual(parsed, parsed_lower)
+        self.assertEqual(reso, reso_lower)
+
+    def test_parse_time_quarter_w_dash(self):
+        # https://github.com/pydata/pandas/issue/9688
+        pairs = [
+            ('1988-Q2', '1988Q2'),
+            ('2Q-1988', '2Q1988'),
+        ]
+
+        for dashed, normal in pairs:
+            (date_dash, parsed_dash, reso_dash) = parse_time_string(dashed)
+            (date, parsed, reso) = parse_time_string(normal)
+
+            self.assertEqual(date_dash, date)
+            self.assertEqual(parsed_dash, parsed)
+            self.assertEqual(reso_dash, reso)
+
+        self.assertRaises(DateParseError, parse_time_string, "-2Q1992")
+        self.assertRaises(DateParseError, parse_time_string, "2-Q1992")
+        self.assertRaises(DateParseError, parse_time_string, "4-4Q1992")
 
 
 def test_get_standard_freq():