diff --git a/asv_bench/benchmarks/timeseries.py b/asv_bench/benchmarks/timeseries.py index b494dbd8a38fa..23d4b05c380ad 100644 --- a/asv_bench/benchmarks/timeseries.py +++ b/asv_bench/benchmarks/timeseries.py @@ -423,4 +423,23 @@ def time_dt_accessor_year(self, tz): self.series.dt.year +class DateTimeAccessorStrftime: + + params = ( + [None, "US/Eastern", "UTC", dateutil.tz.tzutc()], + ["%Y-%m-%d %H:%M:%S.%f%z", "%Y-%m-%d %H:%M:%S%z"], + ["T", "S", "NS"], + ) + param_names = ["tz", "fmt", "frequency"] + + def setup(self, tz, fmt, frequency): + N = 100000 + self.series = Series( + date_range(start="1/1/2000", periods=N, freq=frequency, tz=tz) + ) + + def time_dt_accessor_strftime(self, tz, fmt, frequency): + self.series.dt.strftime(fmt) + + from .pandas_vb_common import setup # noqa: F401 isort:skip diff --git a/asv_bench/benchmarks/tslibs/timestamp.py b/asv_bench/benchmarks/tslibs/timestamp.py index 40f8e561f5238..b922e40adabc4 100644 --- a/asv_bench/benchmarks/tslibs/timestamp.py +++ b/asv_bench/benchmarks/tslibs/timestamp.py @@ -120,6 +120,17 @@ def time_weekday_name(self, tz, freq): self.ts.day_name() +class TimestampStrftimeMethod: + params = ["%Y-%m-%d %H:%M:%S", "%Y-%m-%d %H:%M:%S.%f"] + param_names = ["fmt"] + + def setup(self, fmt): + self.ts = Timestamp("2020-05-23 18:06:13.123456789") + + def time_strftime(self, fmt): + self.ts.strftime(fmt) + + class TimestampOps: params = _tzs param_names = ["tz"] diff --git a/doc/source/whatsnew/v1.1.0.rst b/doc/source/whatsnew/v1.1.0.rst index 43d1244c15d8a..7174f42a2144b 100644 --- a/doc/source/whatsnew/v1.1.0.rst +++ b/doc/source/whatsnew/v1.1.0.rst @@ -915,6 +915,7 @@ Datetimelike - Bug in :meth:`DatetimeIndex.intersection` and :meth:`TimedeltaIndex.intersection` with results not having the correct ``name`` attribute (:issue:`33904`) - Bug in :meth:`DatetimeArray.__setitem__`, :meth:`TimedeltaArray.__setitem__`, :meth:`PeriodArray.__setitem__` incorrectly allowing values with ``int64`` dtype to be silently cast (:issue:`33717`) - Bug in subtracting :class:`TimedeltaIndex` from :class:`Period` incorrectly raising ``TypeError`` in some cases where it should succeed and ``IncompatibleFrequency`` in some cases where it should raise ``TypeError`` (:issue:`33883`) +- Bug in :meth:`Timestamp.strftime` did not display full nanosecond precision (:issue:`29461`) - Bug in constructing a Series or Index from a read-only NumPy array with non-ns resolution which converted to object dtype instead of coercing to ``datetime64[ns]`` dtype when within the timestamp bounds (:issue:`34843`). diff --git a/pandas/_libs/tslibs/nattype.pyx b/pandas/_libs/tslibs/nattype.pyx index 264013f928d22..437e2dbefa694 100644 --- a/pandas/_libs/tslibs/nattype.pyx +++ b/pandas/_libs/tslibs/nattype.pyx @@ -452,7 +452,25 @@ class NaTType(_NaT): Function is not implemented. Use pd.to_datetime(). """, ) + strftime = _make_error_func( + "strftime", + """ + Constructs datetime style `format` string from Timestamp. + + See `datetime `_ module for all available directives. + + Parameters + ---------- + format : str + String of formatting directives + Returns + ------- + str + String representation of Timestamp + """, + ) utcfromtimestamp = _make_error_func( "utcfromtimestamp", """ diff --git a/pandas/_libs/tslibs/timestamps.pyx b/pandas/_libs/tslibs/timestamps.pyx index 8cef685933863..d67620442e25c 100644 --- a/pandas/_libs/tslibs/timestamps.pyx +++ b/pandas/_libs/tslibs/timestamps.pyx @@ -645,14 +645,10 @@ cdef class _Timestamp(ABCTimestamp): @property def _time_repr(self) -> str: - result = f'{self.hour:02d}:{self.minute:02d}:{self.second:02d}' - - if self.nanosecond != 0: - result += f'.{self.nanosecond + 1000 * self.microsecond:09d}' - elif self.microsecond != 0: - result += f'.{self.microsecond:06d}' - - return result + fmt = '%H:%M:%S' + if self.microsecond or self.nanosecond: + fmt = '%H:%M:%S.%f' + return self.strftime(fmt) @property def _short_repr(self) -> str: @@ -1473,6 +1469,28 @@ default 'raise' self.nanosecond / 3600.0 / 1e+9 ) / 24.0) + def strftime(self, format: str) -> str: + """ + Constructs datetime style `format` string from Timestamp. + + See `datetime `_ module for all available directives. + + Parameters + ---------- + format : str + String of formatting directives + + Returns + ------- + str + String representation of Timestamp + """ + if self.nanosecond and '%f' in format: + replacement = f'{self.microsecond * 1000 + self.nanosecond:09d}' + format = format.replace('%f', replacement) + return super().strftime(format) + # Aliases Timestamp.weekofyear = Timestamp.week diff --git a/pandas/tests/scalar/timestamp/test_timestamp.py b/pandas/tests/scalar/timestamp/test_timestamp.py index cee7ac450e411..420cf42e999a7 100644 --- a/pandas/tests/scalar/timestamp/test_timestamp.py +++ b/pandas/tests/scalar/timestamp/test_timestamp.py @@ -15,7 +15,7 @@ from pandas.compat.numpy import np_datetime64_compat import pandas.util._test_decorators as td -from pandas import NaT, Timedelta, Timestamp +from pandas import NaT, Timedelta, Timestamp, to_datetime import pandas._testing as tm from pandas.tseries import offsets @@ -381,6 +381,55 @@ def test_tz_conversion_freq(self, tz_naive_fixture): t2 = Timestamp("2019-01-02 12:00", tz="UTC", freq="T") assert t2.tz_convert(tz="UTC").freq == t2.freq + @pytest.mark.parametrize( + "_input,fmt,_output", + [ + ("2020-05-22 11:07:30", "%Y-%m-%d", "2020-05-22"), + ("2020-05-22 11:07:30.123456", "%Y-%m-%d %f", "2020-05-22 123456"), + ("2020-05-22 11:07:30.123456789", "%f", "123456789"), + ], + ) + def test_strftime(self, _input, fmt, _output): + ts = Timestamp(_input) + result = ts.strftime(fmt) + assert result == _output + + @pytest.mark.parametrize( + "fmt", + [ + "%a", + "%A", + "%w", + "%d", + "%b", + "%B", + "%m", + "%y", + "%Y", + "%H", + "%I", + "%p", + "%M", + "%S", + "%f", + "%z", + "%Z", + "%j", + "%U", + "%W", + "%c", + "%x", + "%X", + "%G", + "%u", + "%V", + ], + ) + def test_strftime_components(self, fmt): + ts = Timestamp("2020-06-09 09:04:11.123456", tzinfo=utc) + dt = datetime(2020, 6, 9, 9, 4, 11, 123456, tzinfo=utc) + assert ts.strftime(fmt) == dt.strftime(fmt) + class TestTimestampNsOperations: def test_nanosecond_string_parsing(self): @@ -442,6 +491,20 @@ def test_nanosecond_timestamp(self): assert t.value == expected assert t.nanosecond == 10 + @pytest.mark.parametrize( + "date", + [ + "2020-05-22 08:53:19.123456789", + "2020-05-22 08:53:19.123456", + "2020-05-22 08:53:19", + ], + ) + @pytest.mark.parametrize("fmt", ["%m/%d/%Y %H:%M:%S.%f", "%m%d%Y%H%M%S%f"]) + def test_nanosecond_roundtrip(self, date, fmt): + ts = Timestamp(date) + string = ts.strftime(fmt) + assert ts == to_datetime(string, format=fmt) + class TestTimestampToJulianDate: def test_compare_1700(self):