Skip to content

Commit cf629b4

Browse files
committed
ENH: support .strftime for datetimelikes (closes pandas-dev#10086)
1 parent 1a709c3 commit cf629b4

File tree

7 files changed

+85
-20
lines changed

7 files changed

+85
-20
lines changed

doc/source/api.rst

+1
Original file line numberDiff line numberDiff line change
@@ -498,6 +498,7 @@ These can be accessed like ``Series.dt.<property>``.
498498
Series.dt.tz_localize
499499
Series.dt.tz_convert
500500
Series.dt.normalize
501+
Series.dt.strftime
501502

502503
**Timedelta Properties**
503504

doc/source/basics.rst

+20-4
Original file line numberDiff line numberDiff line change
@@ -1132,13 +1132,13 @@ For instance,
11321132
~~~~~~~~~~~~
11331133

11341134
``Series`` has an accessor to succinctly return datetime like properties for the
1135-
*values* of the Series, if its a datetime/period like Series.
1135+
*values* of the Series, if it is a datetime/period like Series.
11361136
This will return a Series, indexed like the existing Series.
11371137

11381138
.. ipython:: python
11391139
11401140
# datetime
1141-
s = pd.Series(pd.date_range('20130101 09:10:12',periods=4))
1141+
s = pd.Series(pd.date_range('20130101 09:10:12', periods=4))
11421142
s
11431143
s.dt.hour
11441144
s.dt.second
@@ -1164,20 +1164,36 @@ You can also chain these types of operations:
11641164
11651165
s.dt.tz_localize('UTC').dt.tz_convert('US/Eastern')
11661166
1167+
You can also format the datetime values with ``Series.dt.strftime``:
1168+
1169+
.. ipython:: python
1170+
1171+
# DatetimeIndex
1172+
s = Series(date_range('20130101', periods=4))
1173+
s
1174+
s.dt.strftime('%Y/%m/%d')
1175+
1176+
.. ipython:: python
1177+
1178+
# PeriodIndex
1179+
s = Series(period_range('20130101', periods=4))
1180+
s
1181+
s.dt.strftime('%Y/%m/%d')
1182+
11671183
The ``.dt`` accessor works for period and timedelta dtypes.
11681184

11691185
.. ipython:: python
11701186
11711187
# period
1172-
s = pd.Series(pd.period_range('20130101', periods=4,freq='D'))
1188+
s = pd.Series(pd.period_range('20130101', periods=4, freq='D'))
11731189
s
11741190
s.dt.year
11751191
s.dt.day
11761192
11771193
.. ipython:: python
11781194
11791195
# timedelta
1180-
s = pd.Series(pd.timedelta_range('1 day 00:00:05',periods=4,freq='s'))
1196+
s = pd.Series(pd.timedelta_range('1 day 00:00:05', periods=4, freq='s'))
11811197
s
11821198
s.dt.days
11831199
s.dt.seconds

pandas/tests/test_series.py

+30-8
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,10 @@ def test_dt_namespace_accessor(self):
8181

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

@@ -109,13 +110,12 @@ def compare(s, name):
109110
Series(date_range('20130101',periods=5,freq='s')),
110111
Series(date_range('20130101 00:00:00',periods=5,freq='ms'))]:
111112
for prop in ok_for_dt:
112-
113113
# we test freq below
114114
if prop != 'freq':
115115
compare(s, prop)
116116

117117
for prop in ok_for_dt_methods:
118-
getattr(s.dt,prop)
118+
getattr(s.dt, prop)
119119

120120
result = s.dt.to_pydatetime()
121121
self.assertIsInstance(result,np.ndarray)
@@ -140,13 +140,12 @@ def compare(s, name):
140140
Series(timedelta_range('1 day 01:23:45',periods=5,freq='s')),
141141
Series(timedelta_range('2 days 01:23:45.012345',periods=5,freq='ms'))]:
142142
for prop in ok_for_td:
143-
144143
# we test freq below
145144
if prop != 'freq':
146145
compare(s, prop)
147146

148147
for prop in ok_for_td_methods:
149-
getattr(s.dt,prop)
148+
getattr(s.dt, prop)
150149

151150
result = s.dt.components
152151
self.assertIsInstance(result,DataFrame)
@@ -169,13 +168,14 @@ def compare(s, name):
169168

170169
# periodindex
171170
for s in [Series(period_range('20130101',periods=5,freq='D'))]:
172-
173171
for prop in ok_for_period:
174-
175172
# we test freq below
176173
if prop != 'freq':
177174
compare(s, prop)
178175

176+
for prop in ok_for_period_methods:
177+
getattr(s.dt, prop)
178+
179179
freq_result = s.dt.freq
180180
self.assertEqual(freq_result, PeriodIndex(s.values).freq)
181181

@@ -190,7 +190,7 @@ def get_dir(s):
190190

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

195195
# no setting allowed
196196
s = Series(date_range('20130101',periods=5,freq='D'))
@@ -203,6 +203,28 @@ def f():
203203
s.dt.hour[0] = 5
204204
self.assertRaises(com.SettingWithCopyError, f)
205205

206+
def test_strftime(self):
207+
# GH 10086
208+
s = Series(date_range('20130101', periods=5))
209+
result = s.dt.strftime('%Y/%m/%d')
210+
expected = Series(['2013/01/01', '2013/01/02', '2013/01/03', '2013/01/04', '2013/01/05'])
211+
tm.assert_series_equal(result, expected)
212+
213+
s.iloc[0] = pd.NaT
214+
result = s.dt.strftime('%Y/%m/%d')
215+
expected = Series(['NaT', '2013/01/02', '2013/01/03', '2013/01/04', '2013/01/05'])
216+
tm.assert_series_equal(result, expected)
217+
218+
datetime_index = date_range('20150301', periods=5)
219+
result = datetime_index.strftime("%Y/%m/%d")
220+
expected = ['2015/03/01', '2015/03/02', '2015/03/03', '2015/03/04', '2015/03/05']
221+
self.assertEqual(result, expected)
222+
223+
period_index = period_range('20150301', periods=5)
224+
result = period_index.strftime("%Y/%m/%d")
225+
expected = ['2015/03/01', '2015/03/02', '2015/03/03', '2015/03/04', '2015/03/05']
226+
self.assertEqual(result, expected)
227+
206228
def test_valid_dt_with_missing_values(self):
207229

208230
from datetime import date, time

pandas/tseries/base.py

+20
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,26 @@
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 a list of formmatted strings specified by date_format
27+
28+
Parameters
29+
----------
30+
date_format : str
31+
date format string (e.g. "%Y-%m-%d")
32+
33+
Returns
34+
-------
35+
a list formatted strings
36+
"""
37+
return self.format(date_format=date_format)
38+
39+
2040
class DatetimeIndexOpsMixin(object):
2141
""" common ops mixin to support a unified inteface datetimelike Index """
2242

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
@@ -12,7 +12,7 @@
1212
from pandas.tseries.frequencies import (
1313
to_offset, get_period_alias,
1414
Resolution)
15-
from pandas.tseries.base import DatetimeIndexOpsMixin
15+
from pandas.tseries.base import DatelikeOps, DatetimeIndexOpsMixin
1616
from pandas.tseries.offsets import DateOffset, generate_range, Tick, CDay
1717
from pandas.tseries.tools import parse_time_string, normalize_date
1818
from pandas.util.decorators import cache_readonly, deprecate_kwarg
@@ -116,7 +116,7 @@ def _new_DatetimeIndex(cls, d):
116116
result.tz = tz
117117
return result
118118

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

pandas/tseries/period.py

+8-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
@@ -717,14 +717,17 @@ def __getitem__(self, key):
717717

718718
return PeriodIndex(result, name=self.name, freq=self.freq)
719719

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

722722
values = np.array(list(self), dtype=object)
723723
mask = isnull(self.values)
724724
values[mask] = na_rep
725-
726725
imask = ~mask
727-
values[imask] = np.array([u('%s') % dt for dt in values[imask]])
726+
727+
if date_format is None:
728+
values[imask] = np.array([u('%s') % dt for dt in values[imask]])
729+
else:
730+
values[imask] = np.array([dt.strftime(date_format) for dt in values[imask]])
728731
return values
729732

730733
def __array_finalize__(self, obj):

0 commit comments

Comments
 (0)