Skip to content

Commit a92501e

Browse files
ENH: Enable parsing of ISO8601-like timestamps with negative signs using pd.Timedelta (GH37172) (pandas-dev#39497)
1 parent f2ff676 commit a92501e

File tree

3 files changed

+18
-5
lines changed

3 files changed

+18
-5
lines changed

doc/source/whatsnew/v1.3.0.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ Other enhancements
5858
- :meth:`.Styler.apply` now more consistently accepts ndarray function returns, i.e. in all cases for ``axis`` is ``0, 1 or None``. (:issue:`39359`)
5959
- :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`)
6060
- :meth:`pandas.read_stata` and :class:`StataReader` support reading data from compressed files.
61-
61+
- Add support for parsing ``ISO 8601``-like timestamps with negative signs to :meth:`pandas.Timedelta` (:issue:`37172`)
6262

6363
.. ---------------------------------------------------------------------------
6464

pandas/_libs/tslibs/timedeltas.pyx

+12-4
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,7 @@ cdef convert_to_timedelta64(object ts, str unit):
276276
ts = cast_from_unit(ts, unit)
277277
ts = np.timedelta64(ts, "ns")
278278
elif isinstance(ts, str):
279-
if len(ts) > 0 and ts[0] == "P":
279+
if (len(ts) > 0 and ts[0] == "P") or (len(ts) > 1 and ts[:2] == "-P"):
280280
ts = parse_iso_format_string(ts)
281281
else:
282282
ts = parse_timedelta_string(ts)
@@ -673,13 +673,17 @@ cdef inline int64_t parse_iso_format_string(str ts) except? -1:
673673
cdef:
674674
unicode c
675675
int64_t result = 0, r
676-
int p = 0
676+
int p = 0, sign = 1
677677
object dec_unit = 'ms', err_msg
678678
bint have_dot = 0, have_value = 0, neg = 0
679679
list number = [], unit = []
680680

681681
err_msg = f"Invalid ISO 8601 Duration format - {ts}"
682682

683+
if ts[0] == "-":
684+
sign = -1
685+
ts = ts[1:]
686+
683687
for c in ts:
684688
# number (ascii codes)
685689
if 48 <= ord(c) <= 57:
@@ -711,6 +715,8 @@ cdef inline int64_t parse_iso_format_string(str ts) except? -1:
711715
raise ValueError(err_msg)
712716
else:
713717
neg = 1
718+
elif c == "+":
719+
pass
714720
elif c in ['W', 'D', 'H', 'M']:
715721
if c in ['H', 'M'] and len(number) > 2:
716722
raise ValueError(err_msg)
@@ -751,7 +757,7 @@ cdef inline int64_t parse_iso_format_string(str ts) except? -1:
751757
# Received string only - never parsed any values
752758
raise ValueError(err_msg)
753759

754-
return result
760+
return sign*result
755761

756762

757763
cdef _to_py_int_float(v):
@@ -1252,7 +1258,9 @@ class Timedelta(_Timedelta):
12521258
elif isinstance(value, str):
12531259
if unit is not None:
12541260
raise ValueError("unit must not be specified if the value is a str")
1255-
if len(value) > 0 and value[0] == 'P':
1261+
if (len(value) > 0 and value[0] == 'P') or (
1262+
len(value) > 1 and value[:2] == '-P'
1263+
):
12561264
value = parse_iso_format_string(value)
12571265
else:
12581266
value = parse_timedelta_string(value)

pandas/tests/scalar/timedelta/test_constructors.py

+5
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,9 @@ def test_construction_out_of_bounds_td64():
263263
("P1W", Timedelta(days=7)),
264264
("PT300S", Timedelta(seconds=300)),
265265
("P1DT0H0M00000000000S", Timedelta(days=1)),
266+
("PT-6H3M", Timedelta(hours=-6, minutes=3)),
267+
("-PT6H3M", Timedelta(hours=-6, minutes=-3)),
268+
("-PT-6H+3M", Timedelta(hours=6, minutes=-3)),
266269
],
267270
)
268271
def test_iso_constructor(fmt, exp):
@@ -277,6 +280,8 @@ def test_iso_constructor(fmt, exp):
277280
"P0DT999H999M999S",
278281
"P1DT0H0M0.0000000000000S",
279282
"P1DT0H0M0.S",
283+
"P",
284+
"-P",
280285
],
281286
)
282287
def test_iso_constructor_raises(fmt):

0 commit comments

Comments
 (0)