From 01ea8c4d2619d2ebaa8bf5d65ad04cf911a3f95c Mon Sep 17 00:00:00 2001 From: Scott Talbert Date: Thu, 11 Nov 2021 15:21:10 -0500 Subject: [PATCH 1/5] ENH: Support timespec argument in Timestamp.isoformat() --- doc/source/whatsnew/v1.4.0.rst | 1 + pandas/_libs/tslibs/timestamps.pyx | 48 +++++++++++-- pandas/tests/scalar/timestamp/test_formats.py | 71 +++++++++++++++++++ 3 files changed, 113 insertions(+), 7 deletions(-) create mode 100644 pandas/tests/scalar/timestamp/test_formats.py diff --git a/doc/source/whatsnew/v1.4.0.rst b/doc/source/whatsnew/v1.4.0.rst index 8732e1c397ce5..a6751c486f25b 100644 --- a/doc/source/whatsnew/v1.4.0.rst +++ b/doc/source/whatsnew/v1.4.0.rst @@ -184,6 +184,7 @@ Other enhancements - :meth:`DataFrame.dropna` now accepts a single label as ``subset`` along with array-like (:issue:`41021`) - :meth:`read_excel` now accepts a ``decimal`` argument that allow the user to specify the decimal point when parsing string columns to numeric (:issue:`14403`) - :meth:`.GroupBy.mean` now supports `Numba `_ execution with the ``engine`` keyword (:issue:`43731`) +- :meth:`Timestamp.isoformat`, now handles the ``timespec`` argument from the base :class:``datetime`` class (:issue:`26131`) .. --------------------------------------------------------------------------- diff --git a/pandas/_libs/tslibs/timestamps.pyx b/pandas/_libs/tslibs/timestamps.pyx index 613da5a691736..71271a100c68a 100644 --- a/pandas/_libs/tslibs/timestamps.pyx +++ b/pandas/_libs/tslibs/timestamps.pyx @@ -737,9 +737,42 @@ cdef class _Timestamp(ABCTimestamp): # ----------------------------------------------------------------- # Rendering Methods - def isoformat(self, sep: str = "T") -> str: - base = super(_Timestamp, self).isoformat(sep=sep) - if self.nanosecond == 0: + def isoformat(self, sep: str = "T", timespec: str = "auto") -> str: + """ + Return the time formatted according to ISO. + + The full format looks like 'YYYY-MM-DD HH:MM:SS.mmmmmmnnn'. + By default, the fractional part is omitted if self.microsecond == 0 + and self.nanosecond == 0. + + If self.tzinfo is not None, the UTC offset is also attached, giving + giving a full format of 'YYYY-MM-DD HH:MM:SS.mmmmmmnnn+HH:MM'. + + Parameters + ---------- + sep : str, default 'T' + String used as the separator between the date and time. + + timespec : str, default 'auto' + Specifies the number of additional terms of the time to include. + The valid values are 'auto', 'hours', 'minutes', 'seconds', + 'milliseconds', 'microseconds', and 'nanoseconds'. + + Returns + ------- + str + + Examples + -------- + >>> ts = pd.Timestamp('2020-03-14T15:32:52.192548651') + >>> ts.isoformat() + '2020-03-14T15:32:52.192548651' + >>> ts.isoformat(timespec='microseconds') + '2020-03-14T15:32:52.192548' + """ + base_ts = "microseconds" if timespec == "nanoseconds" else timespec + base = super(_Timestamp, self).isoformat(sep=sep, timespec=base_ts) + if self.nanosecond == 0 and timespec != "nanoseconds": return base if self.tzinfo is not None: @@ -747,10 +780,11 @@ cdef class _Timestamp(ABCTimestamp): else: base1, base2 = base, "" - if self.microsecond != 0: - base1 += f"{self.nanosecond:03d}" - else: - base1 += f".{self.nanosecond:09d}" + if timespec == "nanoseconds" or (timespec == "auto" and self.nanosecond): + if self.microsecond: + base1 += f"{self.nanosecond:03d}" + else: + base1 += f".{self.nanosecond:09d}" return base1 + base2 diff --git a/pandas/tests/scalar/timestamp/test_formats.py b/pandas/tests/scalar/timestamp/test_formats.py new file mode 100644 index 0000000000000..71dbf3539bdb2 --- /dev/null +++ b/pandas/tests/scalar/timestamp/test_formats.py @@ -0,0 +1,71 @@ +import pytest + +from pandas import Timestamp + +ts_no_ns = Timestamp( + year=2019, + month=5, + day=18, + hour=15, + minute=17, + second=8, + microsecond=132263, +) +ts_ns = Timestamp( + year=2019, + month=5, + day=18, + hour=15, + minute=17, + second=8, + microsecond=132263, + nanosecond=123, +) +ts_ns_tz = Timestamp( + year=2019, + month=5, + day=18, + hour=15, + minute=17, + second=8, + microsecond=132263, + nanosecond=123, + tz="UTC", +) +ts_no_us = Timestamp( + year=2019, + month=5, + day=18, + hour=15, + minute=17, + second=8, + microsecond=0, + nanosecond=123, +) + + +@pytest.mark.parametrize( + "ts, timespec, expected_iso", + [ + (ts_no_ns, "auto", "2019-05-18T15:17:08.132263"), + (ts_no_ns, "seconds", "2019-05-18T15:17:08"), + (ts_no_ns, "nanoseconds", "2019-05-18T15:17:08.132263000"), + (ts_ns, "auto", "2019-05-18T15:17:08.132263123"), + (ts_ns, "hours", "2019-05-18T15"), + (ts_ns, "minutes", "2019-05-18T15:17"), + (ts_ns, "seconds", "2019-05-18T15:17:08"), + (ts_ns, "milliseconds", "2019-05-18T15:17:08.132"), + (ts_ns, "microseconds", "2019-05-18T15:17:08.132263"), + (ts_ns, "nanoseconds", "2019-05-18T15:17:08.132263123"), + (ts_ns_tz, "auto", "2019-05-18T15:17:08.132263123+00:00"), + (ts_ns_tz, "hours", "2019-05-18T15+00:00"), + (ts_ns_tz, "minutes", "2019-05-18T15:17+00:00"), + (ts_ns_tz, "seconds", "2019-05-18T15:17:08+00:00"), + (ts_ns_tz, "milliseconds", "2019-05-18T15:17:08.132+00:00"), + (ts_ns_tz, "microseconds", "2019-05-18T15:17:08.132263+00:00"), + (ts_ns_tz, "nanoseconds", "2019-05-18T15:17:08.132263123+00:00"), + (ts_no_us, "auto", "2019-05-18T15:17:08.000000123"), + ], +) +def test_isoformat(ts, timespec, expected_iso): + assert ts.isoformat(timespec=timespec) == expected_iso From 49abcfeb634be76389fe5e754bc44a2ecb63a9da Mon Sep 17 00:00:00 2001 From: Scott Talbert Date: Thu, 11 Nov 2021 15:52:30 -0500 Subject: [PATCH 2/5] Get rid of tabs --- pandas/_libs/tslibs/timestamps.pyx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/_libs/tslibs/timestamps.pyx b/pandas/_libs/tslibs/timestamps.pyx index 71271a100c68a..28b8158548ca8 100644 --- a/pandas/_libs/tslibs/timestamps.pyx +++ b/pandas/_libs/tslibs/timestamps.pyx @@ -756,7 +756,7 @@ cdef class _Timestamp(ABCTimestamp): timespec : str, default 'auto' Specifies the number of additional terms of the time to include. The valid values are 'auto', 'hours', 'minutes', 'seconds', - 'milliseconds', 'microseconds', and 'nanoseconds'. + 'milliseconds', 'microseconds', and 'nanoseconds'. Returns ------- From 56ae9bded3fa4183d7e27fea471436a0871237c2 Mon Sep 17 00:00:00 2001 From: Scott Talbert Date: Thu, 11 Nov 2021 16:41:29 -0500 Subject: [PATCH 3/5] Copy isoformat docstring to NaTType --- pandas/_libs/tslibs/nattype.pyx | 34 ++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/pandas/_libs/tslibs/nattype.pyx b/pandas/_libs/tslibs/nattype.pyx index 2aebf75ba35d4..16e3826975ef0 100644 --- a/pandas/_libs/tslibs/nattype.pyx +++ b/pandas/_libs/tslibs/nattype.pyx @@ -295,7 +295,39 @@ cdef class _NaT(datetime): def __str__(self) -> str: return "NaT" - def isoformat(self, sep="T") -> str: + def isoformat(self, sep: str = "T", timespec: str = "auto") -> str: + """ + Return the time formatted according to ISO. + + The full format looks like 'YYYY-MM-DD HH:MM:SS.mmmmmmnnn'. + By default, the fractional part is omitted if self.microsecond == 0 + and self.nanosecond == 0. + + If self.tzinfo is not None, the UTC offset is also attached, giving + giving a full format of 'YYYY-MM-DD HH:MM:SS.mmmmmmnnn+HH:MM'. + + Parameters + ---------- + sep : str, default 'T' + String used as the separator between the date and time. + + timespec : str, default 'auto' + Specifies the number of additional terms of the time to include. + The valid values are 'auto', 'hours', 'minutes', 'seconds', + 'milliseconds', 'microseconds', and 'nanoseconds'. + + Returns + ------- + str + + Examples + -------- + >>> ts = pd.Timestamp('2020-03-14T15:32:52.192548651') + >>> ts.isoformat() + '2020-03-14T15:32:52.192548651' + >>> ts.isoformat(timespec='microseconds') + '2020-03-14T15:32:52.192548' + """ # This allows Timestamp(ts.isoformat()) to always correctly roundtrip. return "NaT" From 8e595750aca80307cc477738f66cbf75fc9a3858 Mon Sep 17 00:00:00 2001 From: Scott Talbert Date: Fri, 12 Nov 2021 09:24:49 -0500 Subject: [PATCH 4/5] Remove NaT docstring changes & update NaT tests --- pandas/_libs/tslibs/nattype.pyx | 32 -------------------------------- pandas/tests/scalar/test_nat.py | 5 +++++ 2 files changed, 5 insertions(+), 32 deletions(-) diff --git a/pandas/_libs/tslibs/nattype.pyx b/pandas/_libs/tslibs/nattype.pyx index 16e3826975ef0..09bfc4527a428 100644 --- a/pandas/_libs/tslibs/nattype.pyx +++ b/pandas/_libs/tslibs/nattype.pyx @@ -296,38 +296,6 @@ cdef class _NaT(datetime): return "NaT" def isoformat(self, sep: str = "T", timespec: str = "auto") -> str: - """ - Return the time formatted according to ISO. - - The full format looks like 'YYYY-MM-DD HH:MM:SS.mmmmmmnnn'. - By default, the fractional part is omitted if self.microsecond == 0 - and self.nanosecond == 0. - - If self.tzinfo is not None, the UTC offset is also attached, giving - giving a full format of 'YYYY-MM-DD HH:MM:SS.mmmmmmnnn+HH:MM'. - - Parameters - ---------- - sep : str, default 'T' - String used as the separator between the date and time. - - timespec : str, default 'auto' - Specifies the number of additional terms of the time to include. - The valid values are 'auto', 'hours', 'minutes', 'seconds', - 'milliseconds', 'microseconds', and 'nanoseconds'. - - Returns - ------- - str - - Examples - -------- - >>> ts = pd.Timestamp('2020-03-14T15:32:52.192548651') - >>> ts.isoformat() - '2020-03-14T15:32:52.192548651' - >>> ts.isoformat(timespec='microseconds') - '2020-03-14T15:32:52.192548' - """ # This allows Timestamp(ts.isoformat()) to always correctly roundtrip. return "NaT" diff --git a/pandas/tests/scalar/test_nat.py b/pandas/tests/scalar/test_nat.py index 21ed57813b60d..c0e3d73d17126 100644 --- a/pandas/tests/scalar/test_nat.py +++ b/pandas/tests/scalar/test_nat.py @@ -182,6 +182,7 @@ def test_nat_methods_nat(method): def test_nat_iso_format(get_nat): # see gh-12300 assert get_nat("NaT").isoformat() == "NaT" + assert get_nat("NaT").isoformat(timespec="nanoseconds") == "NaT" @pytest.mark.parametrize( @@ -325,6 +326,10 @@ def test_nat_doc_strings(compare): klass, method = compare klass_doc = getattr(klass, method).__doc__ + # Ignore differences with Timestamp.isoformat() as they're intentional + if klass == Timestamp and method == 'isoformat': + return + nat_doc = getattr(NaT, method).__doc__ assert klass_doc == nat_doc From c29f3ca1bd8338f98f00d8e739c44722b7ce7eb6 Mon Sep 17 00:00:00 2001 From: Scott Talbert Date: Fri, 12 Nov 2021 10:19:39 -0500 Subject: [PATCH 5/5] Fix another black issue --- pandas/tests/scalar/test_nat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/tests/scalar/test_nat.py b/pandas/tests/scalar/test_nat.py index c0e3d73d17126..b9718249b38c8 100644 --- a/pandas/tests/scalar/test_nat.py +++ b/pandas/tests/scalar/test_nat.py @@ -327,7 +327,7 @@ def test_nat_doc_strings(compare): klass_doc = getattr(klass, method).__doc__ # Ignore differences with Timestamp.isoformat() as they're intentional - if klass == Timestamp and method == 'isoformat': + if klass == Timestamp and method == "isoformat": return nat_doc = getattr(NaT, method).__doc__