diff --git a/asv_bench/benchmarks/timeseries.py b/asv_bench/benchmarks/timeseries.py index b494dbd8a38fa..aeb465297d774 100644 --- a/asv_bench/benchmarks/timeseries.py +++ b/asv_bench/benchmarks/timeseries.py @@ -394,33 +394,42 @@ def time_dup_string_tzoffset_dates(self, cache): class DatetimeAccessor: - params = [None, "US/Eastern", "UTC", dateutil.tz.tzutc()] - param_names = "tz" - - def setup(self, tz): + 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="T", tz=tz)) + self.series = Series( + date_range(start="1/1/2000", periods=N, freq=frequency, tz=tz) + ) - def time_dt_accessor(self, tz): + def time_dt_accessor(self, tz, fmt, frequency): self.series.dt - def time_dt_accessor_normalize(self, tz): + def time_dt_accessor_normalize(self, tz, fmt, frequency): self.series.dt.normalize() - def time_dt_accessor_month_name(self, tz): + def time_dt_accessor_month_name(self, tz, fmt, frequency): self.series.dt.month_name() - def time_dt_accessor_day_name(self, tz): + def time_dt_accessor_day_name(self, tz, fmt, frequency): self.series.dt.day_name() - def time_dt_accessor_time(self, tz): + def time_dt_accessor_time(self, tz, fmt, frequency): self.series.dt.time - def time_dt_accessor_date(self, tz): + def time_dt_accessor_date(self, tz, fmt, frequency): self.series.dt.date - def time_dt_accessor_year(self, tz): + def time_dt_accessor_year(self, tz, fmt, frequency): self.series.dt.year + 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 3ef9b814dd79e..cb714d4491312 100644 --- a/asv_bench/benchmarks/tslibs/timestamp.py +++ b/asv_bench/benchmarks/tslibs/timestamp.py @@ -109,6 +109,17 @@ def time_month_name(self, tz, freq): self.ts.month_name() +class TimestampMethods: + 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 = [None, "US/Eastern", pytz.UTC, dateutil.tz.tzutc()] param_names = ["tz"] diff --git a/doc/source/whatsnew/v1.1.0.rst b/doc/source/whatsnew/v1.1.0.rst index cf5a6976524de..95728489ee480 100644 --- a/doc/source/whatsnew/v1.1.0.rst +++ b/doc/source/whatsnew/v1.1.0.rst @@ -815,6 +815,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`) Timedelta ^^^^^^^^^ diff --git a/pandas/_libs/tslibs/timestamps.pyx b/pandas/_libs/tslibs/timestamps.pyx index 471ed557f4327..5558ebcb27ff1 100644 --- a/pandas/_libs/tslibs/timestamps.pyx +++ b/pandas/_libs/tslibs/timestamps.pyx @@ -7,6 +7,8 @@ construction requirements, we need to do object instantiation in python shadows the python class, where we do any heavy lifting. """ import warnings +import time as _time +import re import numpy as np cimport numpy as cnp @@ -493,14 +495,7 @@ 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 + return self.strftime('%H:%M:%S.%f') @property def _short_repr(self) -> str: @@ -1455,6 +1450,17 @@ default 'raise' np.array([self.value], dtype="i8"), tz=own_tz) return Timestamp(normalized[0]).tz_localize(own_tz) + def strftime(self, format: str) -> str: + # time.strftime() doesn't support %f so we manually replace it + if '%f' in format: + # always show six digits of microseconds, even if its 0s + replacement = f'{self.microsecond:06d}' + # only show nanoseconds if we have them (for comparison to datetime) + if self.nanosecond: + replacement = f'{self.microsecond * 1000 + self.nanosecond:09d}' + format = re.sub('%f', replacement, format) + return _time.strftime(format, self.timetuple()) + # Add the min and max fields at the class level cdef int64_t _NS_UPPER_BOUND = np.iinfo(np.int64).max diff --git a/pandas/tests/scalar/timestamp/test_timestamp.py b/pandas/tests/scalar/timestamp/test_timestamp.py index cee7ac450e411..4804706f5461b 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,56 @@ 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", tz="UTC") + dt = to_datetime(ts) + assert isinstance(ts, Timestamp) and isinstance(dt, datetime) + assert ts.strftime(fmt) == dt.strftime(fmt) + class TestTimestampNsOperations: def test_nanosecond_string_parsing(self): @@ -442,6 +492,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):