Skip to content

Commit 304a5f4

Browse files
committed
Merge branch 'mortada-dt_strftime'
2 parents babfc0d + 30d9a7f commit 304a5f4

File tree

9 files changed

+149
-21
lines changed

9 files changed

+149
-21
lines changed

doc/source/api.rst

+1
Original file line numberDiff line numberDiff line change
@@ -509,6 +509,7 @@ These can be accessed like ``Series.dt.<property>``.
509509
Series.dt.tz_localize
510510
Series.dt.tz_convert
511511
Series.dt.normalize
512+
Series.dt.strftime
512513

513514
**Timedelta Properties**
514515

doc/source/basics.rst

+21-4
Original file line numberDiff line numberDiff line change
@@ -1322,13 +1322,13 @@ data type of the values and is generally faster as :meth:`~DataFrame.iterrows`.
13221322
------------
13231323

13241324
``Series`` has an accessor to succinctly return datetime like properties for the
1325-
*values* of the Series, if its a datetime/period like Series.
1325+
*values* of the Series, if it is a datetime/period like Series.
13261326
This will return a Series, indexed like the existing Series.
13271327

13281328
.. ipython:: python
13291329
13301330
# datetime
1331-
s = pd.Series(pd.date_range('20130101 09:10:12',periods=4))
1331+
s = pd.Series(pd.date_range('20130101 09:10:12', periods=4))
13321332
s
13331333
s.dt.hour
13341334
s.dt.second
@@ -1354,20 +1354,37 @@ You can also chain these types of operations:
13541354
13551355
s.dt.tz_localize('UTC').dt.tz_convert('US/Eastern')
13561356
1357+
You can also format datetime values as strings with :meth:`Series.dt.strftime` which
1358+
supports the same format as the standard :meth:`~datetime.datetime.strftime`.
1359+
1360+
.. ipython:: python
1361+
1362+
# DatetimeIndex
1363+
s = pd.Series(pd.date_range('20130101', periods=4))
1364+
s
1365+
s.dt.strftime('%Y/%m/%d')
1366+
1367+
.. ipython:: python
1368+
1369+
# PeriodIndex
1370+
s = pd.Series(pd.period_range('20130101', periods=4))
1371+
s
1372+
s.dt.strftime('%Y/%m/%d')
1373+
13571374
The ``.dt`` accessor works for period and timedelta dtypes.
13581375

13591376
.. ipython:: python
13601377
13611378
# period
1362-
s = pd.Series(pd.period_range('20130101', periods=4,freq='D'))
1379+
s = pd.Series(pd.period_range('20130101', periods=4, freq='D'))
13631380
s
13641381
s.dt.year
13651382
s.dt.day
13661383
13671384
.. ipython:: python
13681385
13691386
# timedelta
1370-
s = pd.Series(pd.timedelta_range('1 day 00:00:05',periods=4,freq='s'))
1387+
s = pd.Series(pd.timedelta_range('1 day 00:00:05', periods=4, freq='s'))
13711388
s
13721389
s.dt.days
13731390
s.dt.seconds

doc/source/whatsnew/v0.17.0.txt

+24
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ Highlights include:
1818
previously this would return the original input, see :ref:`here <whatsnew_0170.api_breaking.to_datetime>`
1919
- The default for ``dropna`` in ``HDFStore`` has changed to ``False``, to store by default all rows even
2020
if they are all ``NaN``, see :ref:`here <whatsnew_0170.api_breaking.hdf_dropna>`
21+
- Support for ``Series.dt.strftime`` to generate formatted strings for datetime-likes, see :ref:`here <whatsnew_0170.strftime>`
2122
- Development installed versions of pandas will now have ``PEP440`` compliant version strings (:issue:`9518`)
2223

2324
Check the :ref:`API Changes <whatsnew_0170.api>` and :ref:`deprecations <whatsnew_0170.deprecations>` before updating.
@@ -60,6 +61,29 @@ Releasing of the GIL could benefit an application that uses threads for user int
6061
.. _dask: https://dask.readthedocs.org/en/latest/
6162
.. _QT: https://wiki.python.org/moin/PyQt
6263

64+
.. _whatsnew_0170.strftime:
65+
66+
Support strftime for Datetimelikes
67+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
68+
69+
We are now supporting a ``Series.dt.strftime`` method for datetime-likes to generate a formatted string (:issue:`10110`). Examples:
70+
71+
.. ipython:: python
72+
73+
# DatetimeIndex
74+
s = pd.Series(pd.date_range('20130101', periods=4))
75+
s
76+
s.dt.strftime('%Y/%m/%d')
77+
78+
.. ipython:: python
79+
80+
# PeriodIndex
81+
s = pd.Series(pd.period_range('20130101', periods=4))
82+
s
83+
s.dt.strftime('%Y/%m/%d')
84+
85+
The string format is as the python standard library and details can be found `here <https://docs.python.org/2/library/datetime.html#strftime-and-strptime-behavior>`_
86+
6387
.. _whatsnew_0170.enhancements.other:
6488

6589
Other enhancements

pandas/tests/test_series.py

+64-8
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,10 @@ def test_dt_namespace_accessor(self):
8383

8484
ok_for_base = ['year','month','day','hour','minute','second','weekofyear','week','dayofweek','weekday','dayofyear','quarter','freq','days_in_month','daysinmonth']
8585
ok_for_period = ok_for_base + ['qyear']
86+
ok_for_period_methods = ['strftime']
8687
ok_for_dt = ok_for_base + ['date','time','microsecond','nanosecond', 'is_month_start', 'is_month_end', 'is_quarter_start',
8788
'is_quarter_end', 'is_year_start', 'is_year_end', 'tz']
88-
ok_for_dt_methods = ['to_period','to_pydatetime','tz_localize','tz_convert', 'normalize']
89+
ok_for_dt_methods = ['to_period','to_pydatetime','tz_localize','tz_convert', 'normalize', 'strftime']
8990
ok_for_td = ['days','seconds','microseconds','nanoseconds']
9091
ok_for_td_methods = ['components','to_pytimedelta']
9192

@@ -111,13 +112,12 @@ def compare(s, name):
111112
Series(date_range('20130101',periods=5,freq='s')),
112113
Series(date_range('20130101 00:00:00',periods=5,freq='ms'))]:
113114
for prop in ok_for_dt:
114-
115115
# we test freq below
116116
if prop != 'freq':
117117
compare(s, prop)
118118

119119
for prop in ok_for_dt_methods:
120-
getattr(s.dt,prop)
120+
getattr(s.dt, prop)
121121

122122
result = s.dt.to_pydatetime()
123123
self.assertIsInstance(result,np.ndarray)
@@ -142,13 +142,12 @@ def compare(s, name):
142142
Series(timedelta_range('1 day 01:23:45',periods=5,freq='s')),
143143
Series(timedelta_range('2 days 01:23:45.012345',periods=5,freq='ms'))]:
144144
for prop in ok_for_td:
145-
146145
# we test freq below
147146
if prop != 'freq':
148147
compare(s, prop)
149148

150149
for prop in ok_for_td_methods:
151-
getattr(s.dt,prop)
150+
getattr(s.dt, prop)
152151

153152
result = s.dt.components
154153
self.assertIsInstance(result,DataFrame)
@@ -171,13 +170,14 @@ def compare(s, name):
171170

172171
# periodindex
173172
for s in [Series(period_range('20130101',periods=5,freq='D'))]:
174-
175173
for prop in ok_for_period:
176-
177174
# we test freq below
178175
if prop != 'freq':
179176
compare(s, prop)
180177

178+
for prop in ok_for_period_methods:
179+
getattr(s.dt, prop)
180+
181181
freq_result = s.dt.freq
182182
self.assertEqual(freq_result, PeriodIndex(s.values).freq)
183183

@@ -192,7 +192,7 @@ def get_dir(s):
192192

193193
s = Series(period_range('20130101',periods=5,freq='D').asobject)
194194
results = get_dir(s)
195-
tm.assert_almost_equal(results,list(sorted(set(ok_for_period))))
195+
tm.assert_almost_equal(results, list(sorted(set(ok_for_period + ok_for_period_methods))))
196196

197197
# no setting allowed
198198
s = Series(date_range('20130101',periods=5,freq='D'))
@@ -205,6 +205,62 @@ def f():
205205
s.dt.hour[0] = 5
206206
self.assertRaises(com.SettingWithCopyError, f)
207207

208+
def test_strftime(self):
209+
# GH 10086
210+
s = Series(date_range('20130101', periods=5))
211+
result = s.dt.strftime('%Y/%m/%d')
212+
expected = Series(['2013/01/01', '2013/01/02', '2013/01/03', '2013/01/04', '2013/01/05'])
213+
tm.assert_series_equal(result, expected)
214+
215+
s = Series(date_range('2015-02-03 11:22:33.4567', periods=5))
216+
result = s.dt.strftime('%Y/%m/%d %H-%M-%S')
217+
expected = Series(['2015/02/03 11-22-33', '2015/02/04 11-22-33', '2015/02/05 11-22-33',
218+
'2015/02/06 11-22-33', '2015/02/07 11-22-33'])
219+
tm.assert_series_equal(result, expected)
220+
221+
s = Series(period_range('20130101', periods=5))
222+
result = s.dt.strftime('%Y/%m/%d')
223+
expected = Series(['2013/01/01', '2013/01/02', '2013/01/03', '2013/01/04', '2013/01/05'])
224+
tm.assert_series_equal(result, expected)
225+
226+
s = Series(period_range('2015-02-03 11:22:33.4567', periods=5, freq='s'))
227+
result = s.dt.strftime('%Y/%m/%d %H-%M-%S')
228+
expected = Series(['2015/02/03 11-22-33', '2015/02/03 11-22-34', '2015/02/03 11-22-35',
229+
'2015/02/03 11-22-36', '2015/02/03 11-22-37'])
230+
tm.assert_series_equal(result, expected)
231+
232+
s = Series(date_range('20130101', periods=5))
233+
s.iloc[0] = pd.NaT
234+
result = s.dt.strftime('%Y/%m/%d')
235+
expected = Series(['NaT', '2013/01/02', '2013/01/03', '2013/01/04', '2013/01/05'])
236+
tm.assert_series_equal(result, expected)
237+
238+
datetime_index = date_range('20150301', periods=5)
239+
result = datetime_index.strftime("%Y/%m/%d")
240+
expected = np.array(['2015/03/01', '2015/03/02', '2015/03/03', '2015/03/04', '2015/03/05'], dtype=object)
241+
self.assert_numpy_array_equal(result, expected)
242+
243+
period_index = period_range('20150301', periods=5)
244+
result = period_index.strftime("%Y/%m/%d")
245+
expected = np.array(['2015/03/01', '2015/03/02', '2015/03/03', '2015/03/04', '2015/03/05'], dtype=object)
246+
self.assert_numpy_array_equal(result, expected)
247+
248+
s = Series([datetime(2013, 1, 1, 2, 32, 59), datetime(2013, 1, 2, 14, 32, 1)])
249+
result = s.dt.strftime('%Y-%m-%d %H:%M:%S')
250+
expected = Series(["2013-01-01 02:32:59", "2013-01-02 14:32:01"])
251+
tm.assert_series_equal(result, expected)
252+
253+
s = Series(period_range('20130101', periods=4, freq='H'))
254+
result = s.dt.strftime('%Y/%m/%d %H:%M:%S')
255+
expected = Series(["2013/01/01 00:00:00", "2013/01/01 01:00:00",
256+
"2013/01/01 02:00:00", "2013/01/01 03:00:00"])
257+
258+
s = Series(period_range('20130101', periods=4, freq='L'))
259+
result = s.dt.strftime('%Y/%m/%d %H:%M:%S.%l')
260+
expected = Series(["2013/01/01 00:00:00.000", "2013/01/01 00:00:00.001",
261+
"2013/01/01 00:00:00.002", "2013/01/01 00:00:00.003"])
262+
tm.assert_series_equal(result, expected)
263+
208264
def test_valid_dt_with_missing_values(self):
209265

210266
from datetime import date, time

pandas/tseries/base.py

+23
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,29 @@
1717
import pandas.algos as _algos
1818

1919

20+
21+
class DatelikeOps(object):
22+
""" common ops for DatetimeIndex/PeriodIndex, but not TimedeltaIndex """
23+
24+
def strftime(self, date_format):
25+
"""
26+
Return an array of formatted strings specified by date_format, which
27+
supports the same string format as the python standard library. Details
28+
of the string format can be found in the `python string format doc
29+
<https://docs.python.org/2/library/datetime.html#strftime-and-strptime-behavior>`__
30+
31+
Parameters
32+
----------
33+
date_format : str
34+
date format string (e.g. "%Y-%m-%d")
35+
36+
Returns
37+
-------
38+
ndarray of formatted strings
39+
"""
40+
return np.asarray(self.format(date_format=date_format))
41+
42+
2043
class DatetimeIndexOpsMixin(object):
2144
""" common ops mixin to support a unified inteface datetimelike Index """
2245

pandas/tseries/common.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ def to_pydatetime(self):
125125
accessors=DatetimeIndex._datetimelike_ops,
126126
typ='property')
127127
DatetimeProperties._add_delegate_accessors(delegate=DatetimeIndex,
128-
accessors=["to_period","tz_localize","tz_convert","normalize"],
128+
accessors=["to_period","tz_localize","tz_convert","normalize","strftime"],
129129
typ='method')
130130

131131
class TimedeltaProperties(Properties):
@@ -181,6 +181,9 @@ class PeriodProperties(Properties):
181181
PeriodProperties._add_delegate_accessors(delegate=PeriodIndex,
182182
accessors=PeriodIndex._datetimelike_ops,
183183
typ='property')
184+
PeriodProperties._add_delegate_accessors(delegate=PeriodIndex,
185+
accessors=["strftime"],
186+
typ='method')
184187

185188

186189
class CombinedDatetimelikeProperties(DatetimeProperties, TimedeltaProperties):

pandas/tseries/index.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from pandas.tseries.frequencies import (
1414
to_offset, get_period_alias,
1515
Resolution)
16-
from pandas.tseries.base import DatetimeIndexOpsMixin
16+
from pandas.tseries.base import DatelikeOps, DatetimeIndexOpsMixin
1717
from pandas.tseries.offsets import DateOffset, generate_range, Tick, CDay
1818
from pandas.tseries.tools import parse_time_string, normalize_date
1919
from pandas.util.decorators import cache_readonly, deprecate_kwarg
@@ -117,7 +117,7 @@ def _new_DatetimeIndex(cls, d):
117117
result.tz = tz
118118
return result
119119

120-
class DatetimeIndex(DatetimeIndexOpsMixin, Int64Index):
120+
class DatetimeIndex(DatelikeOps, DatetimeIndexOpsMixin, Int64Index):
121121
"""
122122
Immutable ndarray of datetime64 data, represented internally as int64, and
123123
which can be boxed to Timestamp objects that are subclasses of datetime and

pandas/tseries/period.py

+9-5
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import pandas.tseries.frequencies as frequencies
55
from pandas.tseries.frequencies import get_freq_code as _gfc
66
from pandas.tseries.index import DatetimeIndex, Int64Index, Index
7-
from pandas.tseries.base import DatetimeIndexOpsMixin
7+
from pandas.tseries.base import DatelikeOps, DatetimeIndexOpsMixin
88
from pandas.tseries.tools import parse_time_string
99
import pandas.tseries.offsets as offsets
1010

@@ -92,7 +92,7 @@ def wrapper(self, other):
9292
return wrapper
9393

9494

95-
class PeriodIndex(DatetimeIndexOpsMixin, Int64Index):
95+
class PeriodIndex(DatelikeOps, DatetimeIndexOpsMixin, Int64Index):
9696
"""
9797
Immutable ndarray holding ordinal values indicating regular periods in
9898
time such as particular years, quarters, months, etc. A value of 1 is the
@@ -737,14 +737,18 @@ def __getitem__(self, key):
737737

738738
return PeriodIndex(result, name=self.name, freq=self.freq)
739739

740-
def _format_native_types(self, na_rep=u('NaT'), **kwargs):
740+
def _format_native_types(self, na_rep=u('NaT'), date_format=None, **kwargs):
741741

742742
values = np.array(list(self), dtype=object)
743743
mask = isnull(self.values)
744744
values[mask] = na_rep
745-
746745
imask = ~mask
747-
values[imask] = np.array([u('%s') % dt for dt in values[imask]])
746+
747+
if date_format:
748+
formatter = lambda dt: dt.strftime(date_format)
749+
else:
750+
formatter = lambda dt: u('%s') % dt
751+
values[imask] = np.array([formatter(dt) for dt in values[imask]])
748752
return values
749753

750754
def __array_finalize__(self, obj):

pandas/tseries/tests/test_timeseries.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -2365,7 +2365,7 @@ def test_map(self):
23652365
f = lambda x: x.strftime('%Y%m%d')
23662366
result = rng.map(f)
23672367
exp = [f(x) for x in rng]
2368-
self.assert_numpy_array_equal(result, exp)
2368+
tm.assert_almost_equal(result, exp)
23692369

23702370

23712371
def test_iteration_preserves_tz(self):

0 commit comments

Comments
 (0)