Skip to content

ENH: Timedelta isoformat #15136

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions doc/source/timedeltas.rst
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,18 @@ similarly to the ``Series``. These are the *displayed* values of the ``Timedelta
td.dt.components
td.dt.components.seconds

.. _timedeltas.isoformat:

You can convert a ``Timedelta`` to an ISO 8601 Duration string with the
``.isoformat`` method

.. ipython:: python
pd.Timedelta(days=6, minutes=50, seconds=3,
milliseconds=10, microseconds=10,
nanoseconds=12).isoformat()

.. _ISO 8601 duration: https://en.wikipedia.org/wiki/ISO_8601#Durations

.. _timedeltas.index:

TimedeltaIndex
Expand Down
3 changes: 3 additions & 0 deletions doc/source/whatsnew/v0.20.0.txt
Original file line number Diff line number Diff line change
Expand Up @@ -132,10 +132,13 @@ Other enhancements
- The ``skiprows`` argument in ``pd.read_csv`` now accepts a callable function as a value (:issue:`10882`)
- ``pd.DataFrame.plot`` now prints a title above each subplot if ``suplots=True`` and ``title`` is a list of strings (:issue:`14753`)
- ``pd.Series.interpolate`` now supports timedelta as an index type with ``method='time'`` (:issue:`6424`)
- ``Timedelta.isoformat`` method added for formatting Timedeltas as an `ISO 8601 duration`_. See the :ref:`Timedelta docs <timedeltas.isoformat>` (:issue:`15136`)
- ``pandas.io.json.json_normalize()`` gained the option ``errors='ignore'|'raise'``; the default is ``errors='raise'`` which is backward compatible. (:issue:`14583`)

- ``.select_dtypes()`` now allows the string 'datetimetz' to generically select datetimes with tz (:issue:`14910`)

.. _ISO 8601 duration: https://en.wikipedia.org/wiki/ISO_8601#Durations


.. _whatsnew_0200.api_breaking:

Expand Down
39 changes: 39 additions & 0 deletions pandas/tseries/tests/test_timedeltas.py
Original file line number Diff line number Diff line change
Expand Up @@ -1347,6 +1347,45 @@ def test_components(self):
self.assertFalse(result.iloc[0].isnull().all())
self.assertTrue(result.iloc[1].isnull().all())

def test_isoformat(self):
td = Timedelta(days=6, minutes=50, seconds=3,
milliseconds=10, microseconds=10, nanoseconds=12)
expected = 'P6DT0H50M3.010010012S'
result = td.isoformat()
self.assertEqual(result, expected)

td = Timedelta(days=4, hours=12, minutes=30, seconds=5)
result = td.isoformat()
expected = 'P4DT12H30M5S'
self.assertEqual(result, expected)

td = Timedelta(nanoseconds=123)
result = td.isoformat()
expected = 'P0DT0H0M0.000000123S'
self.assertEqual(result, expected)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add a test for NaT (which i think should just be nan), similar to what we do for Timestamp

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call. How about NaT to match Timestamp?

In [24]: pd.Timestamp('NaT').isoformat()
Out[24]: 'NaT'

In [25]: pd.Timedelta('NaT').isoformat()
Out[25]: 'NaT'

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep that's fine

# trim nano
td = Timedelta(microseconds=10)
result = td.isoformat()
expected = 'P0DT0H0M0.00001S'
self.assertEqual(result, expected)

# trim micro
td = Timedelta(milliseconds=1)
result = td.isoformat()
expected = 'P0DT0H0M0.001S'
self.assertEqual(result, expected)

# NaT
result = Timedelta('NaT').isoformat()
expected = 'NaT'
self.assertEqual(result, expected)

# don't strip every 0
result = Timedelta(minutes=1).isoformat()
expected = 'P0DT0H1M0S'
self.assertEqual(result, expected)

def test_constructor(self):
expected = TimedeltaIndex(['1 days', '1 days 00:00:05', '2 days',
'2 days 00:00:02', '0 days 00:00:03'])
Expand Down
48 changes: 48 additions & 0 deletions pandas/tslib.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -2966,6 +2966,54 @@ class Timedelta(_Timedelta):
"""
return 1e-9 *self.value

def isoformat(self):
"""
Format Timedelta as ISO 8601 Duration like
`P[n]Y[n]M[n]DT[n]H[n]M[n]S`, where the `[n]`s are replaced by the
values. See https://en.wikipedia.org/wiki/ISO_8601#Durations

Returns
-------
formatted : str

Notes
-----
The longest component is days, whose value may be larger than
365.
Every component is always included, even if its value is 0.
Pandas uses nanosecond precision, so up to 9 decimal places may
be included in the seconds component.
Trailing 0's are removed from the seconds component after the decimal.
We do not 0 pad components, so it's `...T5H...`, not `...T05H...`

Examples
--------
>>> td = pd.Timedelta(days=6, minutes=50, seconds=3,
... milliseconds=10, microseconds=10, nanoseconds=12)
>>> td.isoformat()
'P6DT0H50M3.010010012S'
>>> pd.Timedelta(hours=1, seconds=10).isoformat()
'P0DT0H0M10S'
>>> pd.Timedelta(hours=1, seconds=10).isoformat()
'P0DT0H0M10S'
>>> pd.Timedelta(days=500.5).isoformat()
'P500DT12H0MS'

See Also
--------
Timestamp.isoformat
"""
components = self.components
seconds = '{}.{:0>3}{:0>3}{:0>3}'.format(components.seconds,
components.milliseconds,
components.microseconds,
components.nanoseconds)
# Trim unnecessary 0s, 1.000000000 -> 1
seconds = seconds.rstrip('0').rstrip('.')
tpl = 'P{td.days}DT{td.hours}H{td.minutes}M{seconds}S'.format(
td=components, seconds=seconds)
return tpl

def __setstate__(self, state):
(value) = state
self.value = value
Expand Down