diff --git a/doc/source/timedeltas.rst b/doc/source/timedeltas.rst index f7aa879fa7216..b65a24a29bb16 100644 --- a/doc/source/timedeltas.rst +++ b/doc/source/timedeltas.rst @@ -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 diff --git a/doc/source/whatsnew/v0.20.0.txt b/doc/source/whatsnew/v0.20.0.txt index 4ce7e6d2cd3b8..bc6f8cbc18f0d 100644 --- a/doc/source/whatsnew/v0.20.0.txt +++ b/doc/source/whatsnew/v0.20.0.txt @@ -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 ` (: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: diff --git a/pandas/tseries/tests/test_timedeltas.py b/pandas/tseries/tests/test_timedeltas.py index 48ef9a4db7e28..7bc47f84690b4 100644 --- a/pandas/tseries/tests/test_timedeltas.py +++ b/pandas/tseries/tests/test_timedeltas.py @@ -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) + + # 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']) diff --git a/pandas/tslib.pyx b/pandas/tslib.pyx index cde7c0dc10740..a5b5e278ade7b 100644 --- a/pandas/tslib.pyx +++ b/pandas/tslib.pyx @@ -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