Skip to content

Commit 0bf7cb4

Browse files
committed
Added ISO 8601 Duration string constructor for Timedelta
1 parent 6552718 commit 0bf7cb4

File tree

5 files changed

+106
-1
lines changed

5 files changed

+106
-1
lines changed

asv_bench/benchmarks/timedelta.py

+31
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,40 @@
1+
import datetime
2+
13
import numpy as np
24
import pandas as pd
35

46
from pandas import to_timedelta, Timestamp, Timedelta
57

68

9+
class TimedeltaConstructor(object):
10+
goal_time = 0.2
11+
12+
def time_from_int(self):
13+
Timedelta(123456789)
14+
15+
def time_from_unit(self):
16+
Timedelta(1, unit='d')
17+
18+
def time_from_components(self):
19+
Timedelta(days=1, hours=2, minutes=3, seconds=4, milliseconds=5,
20+
microseconds=6, nanoseconds=7)
21+
22+
def time_from_datetime_timedelta(self):
23+
Timedelta(datetime.timedelta(days=1, seconds=1))
24+
25+
def time_from_np_timedelta(self):
26+
Timedelta(np.timedelta64(1, 'ms'))
27+
28+
def time_from_string(self):
29+
Timedelta('1 days')
30+
31+
def time_from_iso_format(self):
32+
Timedelta('P4DT12H30M5S')
33+
34+
def time_from_missing(self):
35+
Timedelta('nat')
36+
37+
738
class ToTimedelta(object):
839
goal_time = 0.2
940

doc/source/timedeltas.rst

+7
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,13 @@ You can construct a ``Timedelta`` scalar through various arguments:
6262
pd.Timedelta('nan')
6363
pd.Timedelta('nat')
6464
65+
# ISO 8601 Duration strings
66+
pd.Timedelta('P0DT0H1M0S')
67+
pd.Timedelta('P0DT0H0M0.000000123S')
68+
69+
.. versionadded:: 0.23.0
70+
Added constructor for `ISO 8601 Duration`_ strings
71+
6572
:ref:`DateOffsets<timeseries.offsets>` (``Day, Hour, Minute, Second, Milli, Micro, Nano``) can also be used in construction.
6673

6774
.. ipython:: python

doc/source/whatsnew/v0.23.0.txt

+1
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@ Other API Changes
208208
- In :func:`read_excel`, the ``comment`` argument is now exposed as a named parameter (:issue:`18735`)
209209
- Rearranged the order of keyword arguments in :func:`read_excel()` to align with :func:`read_csv()` (:issue:`16672`)
210210
- The options ``html.border`` and ``mode.use_inf_as_null`` were deprecated in prior versions, these will now show ``FutureWarning`` rather than a ``DeprecationWarning`` (:issue:`19003`)
211+
- The default ``Timedelta`` constructor now accepts an ``ISO 8601 Duration`` string as an argument (:issue:`19040`)
211212

212213
.. _whatsnew_0230.deprecations:
213214

pandas/_libs/tslibs/timedeltas.pyx

+52-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# -*- coding: utf-8 -*-
22
# cython: profile=False
33
import collections
4+
import re
45

56
import sys
67
cdef bint PY3 = (sys.version_info[0] >= 3)
@@ -235,6 +236,25 @@ cpdef inline int64_t cast_from_unit(object ts, object unit) except? -1:
235236
return <int64_t> (base *m) + <int64_t> (frac *m)
236237

237238

239+
cpdef match_iso_format(object ts):
240+
"""
241+
Match a provided string against an ISO 8601 pattern, providing a group for
242+
each ``Timedelta`` component.
243+
"""
244+
pater = re.compile(r"""P
245+
(?P<days>-?[0-9]*)DT
246+
(?P<hours>[0-9]{1,2})H
247+
(?P<minutes>[0-9]{1,2})M
248+
(?P<seconds>[0-9]{0,2})
249+
(\.
250+
(?P<milliseconds>[0-9]{0,3})
251+
(?P<microseconds>[0-9]{0,3})
252+
(?P<nanoseconds>[0-9]{0,3})
253+
)?S""", re.VERBOSE)
254+
255+
return re.match(pater, ts)
256+
257+
238258
cdef inline parse_timedelta_string(object ts):
239259
"""
240260
Parse a regular format timedelta string. Return an int64_t (in ns)
@@ -506,6 +526,33 @@ def _binary_op_method_timedeltalike(op, name):
506526
# ----------------------------------------------------------------------
507527
# Timedelta Construction
508528

529+
def _value_from_iso_match(match):
530+
"""
531+
Extracts and cleanses the appropriate values from a match object with
532+
groups for each component of an ISO 8601 duration
533+
534+
Parameters
535+
----------
536+
match:
537+
Regular expression with groups for each component of an ISO 8601
538+
duration
539+
540+
Returns
541+
-------
542+
int
543+
Precision in nanoseconds of matched ISO 8601 duration
544+
"""
545+
match_dict = {k: v for k, v in match.groupdict().items() if v}
546+
for comp in ['milliseconds', 'microseconds', 'nanoseconds']:
547+
if comp in match_dict:
548+
match_dict[comp] ='{:0<3}'.format(match_dict[comp])
549+
550+
match_dict = {k: int(v) for k, v in match_dict.items()}
551+
nano = match_dict.pop('nanoseconds', 0)
552+
553+
return nano + convert_to_timedelta64(timedelta(**match_dict), 'ns')
554+
555+
509556
cdef _to_py_int_float(v):
510557
# Note: This used to be defined inside Timedelta.__new__
511558
# but cython will not allow `cdef` functions to be defined dynamically.
@@ -825,7 +872,11 @@ class Timedelta(_Timedelta):
825872
if isinstance(value, Timedelta):
826873
value = value.value
827874
elif is_string_object(value):
828-
value = np.timedelta64(parse_timedelta_string(value))
875+
if len(value) > 0 and value[0] == 'P': # hackish
876+
match = match_iso_format(value)
877+
value = _value_from_iso_match(match)
878+
else:
879+
value = np.timedelta64(parse_timedelta_string(value))
829880
elif PyDelta_Check(value):
830881
value = convert_to_timedelta64(value, 'ns')
831882
elif is_timedelta64_object(value):

pandas/tests/scalar/test_timedelta.py

+15
Original file line numberDiff line numberDiff line change
@@ -853,3 +853,18 @@ def test_isoformat(self):
853853
result = Timedelta(minutes=1).isoformat()
854854
expected = 'P0DT0H1M0S'
855855
assert result == expected
856+
857+
@pytest.mark.parametrize('fmt,exp', [
858+
('P6DT0H50M3.010010012S', Timedelta(days=6, minutes=50, seconds=3,
859+
milliseconds=10, microseconds=10,
860+
nanoseconds=12)),
861+
('P-6DT0H50M3.010010012S', Timedelta(days=-6, minutes=50, seconds=3,
862+
milliseconds=10, microseconds=10,
863+
nanoseconds=12)),
864+
('P4DT12H30M5S', Timedelta(days=4, hours=12, minutes=30, seconds=5)),
865+
('P0DT0H0M0.000000123S', Timedelta(nanoseconds=123)),
866+
('P0DT0H0M0.00001S', Timedelta(microseconds=10)),
867+
('P0DT0H0M0.001S', Timedelta(milliseconds=1)),
868+
('P0DT0H1M0S', Timedelta(minutes=1))])
869+
def test_iso_constructor(self, fmt, exp):
870+
assert Timedelta(fmt) == exp

0 commit comments

Comments
 (0)