diff --git a/doc/source/whatsnew/v1.3.0.rst b/doc/source/whatsnew/v1.3.0.rst index 17d8c79994dbe..95aec64109489 100644 --- a/doc/source/whatsnew/v1.3.0.rst +++ b/doc/source/whatsnew/v1.3.0.rst @@ -57,7 +57,7 @@ Other enhancements - :meth:`.Styler.set_tooltips_class` and :meth:`.Styler.set_table_styles` amended to optionally allow certain css-string input arguments (:issue:`39564`) - :meth:`Series.loc.__getitem__` and :meth:`Series.loc.__setitem__` with :class:`MultiIndex` now raising helpful error message when indexer has too many dimensions (:issue:`35349`) - :meth:`pandas.read_stata` and :class:`StataReader` support reading data from compressed files. - +- Add support for parsing ``ISO 8601``-like timestamps with negative signs to :meth:`pandas.Timedelta` (:issue:`37172`) .. --------------------------------------------------------------------------- diff --git a/pandas/_libs/tslibs/timedeltas.pyx b/pandas/_libs/tslibs/timedeltas.pyx index 748a4c27e64ad..25e0941db75bd 100644 --- a/pandas/_libs/tslibs/timedeltas.pyx +++ b/pandas/_libs/tslibs/timedeltas.pyx @@ -276,7 +276,7 @@ cdef convert_to_timedelta64(object ts, str unit): ts = cast_from_unit(ts, unit) ts = np.timedelta64(ts, "ns") elif isinstance(ts, str): - if len(ts) > 0 and ts[0] == "P": + if (len(ts) > 0 and ts[0] == "P") or (len(ts) > 1 and ts[:2] == "-P"): ts = parse_iso_format_string(ts) else: ts = parse_timedelta_string(ts) @@ -673,13 +673,17 @@ cdef inline int64_t parse_iso_format_string(str ts) except? -1: cdef: unicode c int64_t result = 0, r - int p = 0 + int p = 0, sign = 1 object dec_unit = 'ms', err_msg bint have_dot = 0, have_value = 0, neg = 0 list number = [], unit = [] err_msg = f"Invalid ISO 8601 Duration format - {ts}" + if ts[0] == "-": + sign = -1 + ts = ts[1:] + for c in ts: # number (ascii codes) if 48 <= ord(c) <= 57: @@ -711,6 +715,8 @@ cdef inline int64_t parse_iso_format_string(str ts) except? -1: raise ValueError(err_msg) else: neg = 1 + elif c == "+": + pass elif c in ['W', 'D', 'H', 'M']: if c in ['H', 'M'] and len(number) > 2: raise ValueError(err_msg) @@ -751,7 +757,7 @@ cdef inline int64_t parse_iso_format_string(str ts) except? -1: # Received string only - never parsed any values raise ValueError(err_msg) - return result + return sign*result cdef _to_py_int_float(v): @@ -1252,7 +1258,9 @@ class Timedelta(_Timedelta): elif isinstance(value, str): if unit is not None: raise ValueError("unit must not be specified if the value is a str") - if len(value) > 0 and value[0] == 'P': + if (len(value) > 0 and value[0] == 'P') or ( + len(value) > 1 and value[:2] == '-P' + ): value = parse_iso_format_string(value) else: value = parse_timedelta_string(value) diff --git a/pandas/tests/scalar/timedelta/test_constructors.py b/pandas/tests/scalar/timedelta/test_constructors.py index 64d5a5e9b3fff..de7a0dc97d565 100644 --- a/pandas/tests/scalar/timedelta/test_constructors.py +++ b/pandas/tests/scalar/timedelta/test_constructors.py @@ -263,6 +263,9 @@ def test_construction_out_of_bounds_td64(): ("P1W", Timedelta(days=7)), ("PT300S", Timedelta(seconds=300)), ("P1DT0H0M00000000000S", Timedelta(days=1)), + ("PT-6H3M", Timedelta(hours=-6, minutes=3)), + ("-PT6H3M", Timedelta(hours=-6, minutes=-3)), + ("-PT-6H+3M", Timedelta(hours=6, minutes=-3)), ], ) def test_iso_constructor(fmt, exp): @@ -277,6 +280,8 @@ def test_iso_constructor(fmt, exp): "P0DT999H999M999S", "P1DT0H0M0.0000000000000S", "P1DT0H0M0.S", + "P", + "-P", ], ) def test_iso_constructor_raises(fmt):