Skip to content

Commit 3131106

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

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
@@ -1134,13 +1134,13 @@ For instance,
11341134
~~~~~~~~~~~~
11351135

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

11401140
.. ipython:: python
11411141
11421142
# datetime
1143-
s = Series(date_range('20130101 09:10:12',periods=4))
1143+
s = Series(date_range('20130101 09:10:12', periods=4))
11441144
s
11451145
s.dt.hour
11461146
s.dt.second
@@ -1166,20 +1166,36 @@ You can also chain these types of operations:
11661166
11671167
s.dt.tz_localize('UTC').dt.tz_convert('US/Eastern')
11681168
1169+
You can also format the datetime values with ``Series.dt.strftime``:
1170+
1171+
.. ipython:: python
1172+
1173+
# DatetimeIndex
1174+
s = Series(date_range('20130101', periods=4))
1175+
s
1176+
s.dt.strftime('%Y/%m/%d')
1177+
1178+
.. ipython:: python
1179+
1180+
# PeriodIndex
1181+
s = Series(period_range('20130101', periods=4))
1182+
s
1183+
s.dt.strftime('%Y/%m/%d')
1184+
11691185
The ``.dt`` accessor works for period and timedelta dtypes.
11701186

11711187
.. ipython:: python
11721188
11731189
# period
1174-
s = Series(period_range('20130101',periods=4,freq='D'))
1190+
s = Series(period_range('20130101', periods=4, freq='D'))
11751191
s
11761192
s.dt.year
11771193
s.dt.day
11781194
11791195
.. ipython:: python
11801196
11811197
# timedelta
1182-
s = Series(timedelta_range('1 day 00:00:05',periods=4,freq='s'))
1198+
s = Series(timedelta_range('1 day 00:00:05', periods=4, freq='s'))
11831199
s
11841200
s.dt.days
11851201
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
@@ -19,6 +19,26 @@
1919
import pandas.algos as _algos
2020
from pandas.core.config import get_option
2121

22+
23+
class DatelikeOps(object):
24+
""" common ops for DatetimeIndex/PeriodIndex, but not TimedeltaIndex """
25+
26+
def strftime(self, date_format):
27+
"""
28+
Return a list of formmatted strings specified by date_format
29+
30+
Parameters
31+
----------
32+
date_format : str
33+
date format string (e.g. "%Y-%m-%d")
34+
35+
Returns
36+
-------
37+
a list formatted strings
38+
"""
39+
return self.format(date_format=date_format)
40+
41+
2242
class DatetimeIndexOpsMixin(object):
2343
""" common ops mixin to support a unified inteface datetimelike Index """
2444

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
@@ -17,7 +17,7 @@
1717
from pandas.tseries.frequencies import (
1818
to_offset, get_period_alias,
1919
Resolution)
20-
from pandas.tseries.base import DatetimeIndexOpsMixin
20+
from pandas.tseries.base import DatelikeOps, DatetimeIndexOpsMixin
2121
from pandas.tseries.offsets import DateOffset, generate_range, Tick, CDay
2222
from pandas.tseries.tools import parse_time_string, normalize_date
2323
from pandas.util.decorators import cache_readonly, deprecate_kwarg
@@ -121,7 +121,7 @@ def _new_DatetimeIndex(cls, d):
121121
result.tz = tz
122122
return result
123123

124-
class DatetimeIndex(DatetimeIndexOpsMixin, Int64Index):
124+
class DatetimeIndex(DatelikeOps, DatetimeIndexOpsMixin, Int64Index):
125125
"""
126126
Immutable ndarray of datetime64 data, represented internally as int64, and
127127
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
@@ -8,7 +8,7 @@
88
import pandas.tseries.frequencies as frequencies
99
from pandas.tseries.frequencies import get_freq_code as _gfc
1010
from pandas.tseries.index import DatetimeIndex, Int64Index, Index
11-
from pandas.tseries.base import DatetimeIndexOpsMixin
11+
from pandas.tseries.base import DatelikeOps, DatetimeIndexOpsMixin
1212
from pandas.tseries.tools import parse_time_string
1313
import pandas.tseries.offsets as offsets
1414

@@ -96,7 +96,7 @@ def wrapper(self, other):
9696
return wrapper
9797

9898

99-
class PeriodIndex(DatetimeIndexOpsMixin, Int64Index):
99+
class PeriodIndex(DatelikeOps, DatetimeIndexOpsMixin, Int64Index):
100100
"""
101101
Immutable ndarray holding ordinal values indicating regular periods in
102102
time such as particular years, quarters, months, etc. A value of 1 is the
@@ -721,14 +721,17 @@ def __getitem__(self, key):
721721

722722
return PeriodIndex(result, name=self.name, freq=self.freq)
723723

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

726726
values = np.array(list(self), dtype=object)
727727
mask = isnull(self.values)
728728
values[mask] = na_rep
729-
730729
imask = ~mask
731-
values[imask] = np.array([u('%s') % dt for dt in values[imask]])
730+
731+
if date_format is None:
732+
values[imask] = np.array([u('%s') % dt for dt in values[imask]])
733+
else:
734+
values[imask] = np.array([dt.strftime(date_format) for dt in values[imask]])
732735
return values
733736

734737
def __array_finalize__(self, obj):

0 commit comments

Comments
 (0)