Skip to content

Commit 3e8aff2

Browse files
TomAugspurgerPingviinituutti
authored andcommitted
REF: Refactor Date/TimeLikeOps (pandas-dev#24038)
1 parent 8dde0ab commit 3e8aff2

File tree

7 files changed

+234
-233
lines changed

7 files changed

+234
-233
lines changed

pandas/core/arrays/datetimelike.py

+222-2
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,12 @@
1010
from pandas._libs.tslibs.period import (
1111
DIFFERENT_FREQ_INDEX, IncompatibleFrequency, Period)
1212
from pandas._libs.tslibs.timedeltas import Timedelta, delta_to_nanoseconds
13-
from pandas._libs.tslibs.timestamps import maybe_integer_op_deprecated
13+
from pandas._libs.tslibs.timestamps import (
14+
RoundTo, maybe_integer_op_deprecated, round_nsint64)
1415
import pandas.compat as compat
1516
from pandas.errors import (
1617
AbstractMethodError, NullFrequencyError, PerformanceWarning)
17-
from pandas.util._decorators import deprecate_kwarg
18+
from pandas.util._decorators import Appender, deprecate_kwarg
1819

1920
from pandas.core.dtypes.common import (
2021
is_bool_dtype, is_datetime64_any_dtype, is_datetime64_dtype,
@@ -80,6 +81,189 @@ def _get_attributes_dict(self):
8081
return {k: getattr(self, k, None) for k in self._attributes}
8182

8283

84+
class DatelikeOps(object):
85+
"""
86+
Common ops for DatetimeIndex/PeriodIndex, but not TimedeltaIndex.
87+
"""
88+
89+
def strftime(self, date_format):
90+
from pandas import Index
91+
return Index(self.format(date_format=date_format),
92+
dtype=compat.text_type)
93+
strftime.__doc__ = """
94+
Convert to Index using specified date_format.
95+
96+
Return an Index of formatted strings specified by date_format, which
97+
supports the same string format as the python standard library. Details
98+
of the string format can be found in `python string format doc <{0}>`__
99+
100+
Parameters
101+
----------
102+
date_format : str
103+
Date format string (e.g. "%Y-%m-%d").
104+
105+
Returns
106+
-------
107+
Index
108+
Index of formatted strings
109+
110+
See Also
111+
--------
112+
to_datetime : Convert the given argument to datetime.
113+
DatetimeIndex.normalize : Return DatetimeIndex with times to midnight.
114+
DatetimeIndex.round : Round the DatetimeIndex to the specified freq.
115+
DatetimeIndex.floor : Floor the DatetimeIndex to the specified freq.
116+
117+
Examples
118+
--------
119+
>>> rng = pd.date_range(pd.Timestamp("2018-03-10 09:00"),
120+
... periods=3, freq='s')
121+
>>> rng.strftime('%B %d, %Y, %r')
122+
Index(['March 10, 2018, 09:00:00 AM', 'March 10, 2018, 09:00:01 AM',
123+
'March 10, 2018, 09:00:02 AM'],
124+
dtype='object')
125+
""".format("https://docs.python.org/3/library/datetime.html"
126+
"#strftime-and-strptime-behavior")
127+
128+
129+
class TimelikeOps(object):
130+
"""
131+
Common ops for TimedeltaIndex/DatetimeIndex, but not PeriodIndex.
132+
"""
133+
134+
_round_doc = (
135+
"""
136+
Perform {op} operation on the data to the specified `freq`.
137+
138+
Parameters
139+
----------
140+
freq : str or Offset
141+
The frequency level to {op} the index to. Must be a fixed
142+
frequency like 'S' (second) not 'ME' (month end). See
143+
:ref:`frequency aliases <timeseries.offset_aliases>` for
144+
a list of possible `freq` values.
145+
ambiguous : 'infer', bool-ndarray, 'NaT', default 'raise'
146+
Only relevant for DatetimeIndex:
147+
148+
- 'infer' will attempt to infer fall dst-transition hours based on
149+
order
150+
- bool-ndarray where True signifies a DST time, False designates
151+
a non-DST time (note that this flag is only applicable for
152+
ambiguous times)
153+
- 'NaT' will return NaT where there are ambiguous times
154+
- 'raise' will raise an AmbiguousTimeError if there are ambiguous
155+
times
156+
157+
.. versionadded:: 0.24.0
158+
nonexistent : 'shift', 'NaT', default 'raise'
159+
A nonexistent time does not exist in a particular timezone
160+
where clocks moved forward due to DST.
161+
162+
- 'shift' will shift the nonexistent time forward to the closest
163+
existing time
164+
- 'NaT' will return NaT where there are nonexistent times
165+
- 'raise' will raise an NonExistentTimeError if there are
166+
nonexistent times
167+
168+
.. versionadded:: 0.24.0
169+
170+
Returns
171+
-------
172+
DatetimeIndex, TimedeltaIndex, or Series
173+
Index of the same type for a DatetimeIndex or TimedeltaIndex,
174+
or a Series with the same index for a Series.
175+
176+
Raises
177+
------
178+
ValueError if the `freq` cannot be converted.
179+
180+
Examples
181+
--------
182+
**DatetimeIndex**
183+
184+
>>> rng = pd.date_range('1/1/2018 11:59:00', periods=3, freq='min')
185+
>>> rng
186+
DatetimeIndex(['2018-01-01 11:59:00', '2018-01-01 12:00:00',
187+
'2018-01-01 12:01:00'],
188+
dtype='datetime64[ns]', freq='T')
189+
""")
190+
191+
_round_example = (
192+
""">>> rng.round('H')
193+
DatetimeIndex(['2018-01-01 12:00:00', '2018-01-01 12:00:00',
194+
'2018-01-01 12:00:00'],
195+
dtype='datetime64[ns]', freq=None)
196+
197+
**Series**
198+
199+
>>> pd.Series(rng).dt.round("H")
200+
0 2018-01-01 12:00:00
201+
1 2018-01-01 12:00:00
202+
2 2018-01-01 12:00:00
203+
dtype: datetime64[ns]
204+
""")
205+
206+
_floor_example = (
207+
""">>> rng.floor('H')
208+
DatetimeIndex(['2018-01-01 11:00:00', '2018-01-01 12:00:00',
209+
'2018-01-01 12:00:00'],
210+
dtype='datetime64[ns]', freq=None)
211+
212+
**Series**
213+
214+
>>> pd.Series(rng).dt.floor("H")
215+
0 2018-01-01 11:00:00
216+
1 2018-01-01 12:00:00
217+
2 2018-01-01 12:00:00
218+
dtype: datetime64[ns]
219+
"""
220+
)
221+
222+
_ceil_example = (
223+
""">>> rng.ceil('H')
224+
DatetimeIndex(['2018-01-01 12:00:00', '2018-01-01 12:00:00',
225+
'2018-01-01 13:00:00'],
226+
dtype='datetime64[ns]', freq=None)
227+
228+
**Series**
229+
230+
>>> pd.Series(rng).dt.ceil("H")
231+
0 2018-01-01 12:00:00
232+
1 2018-01-01 12:00:00
233+
2 2018-01-01 13:00:00
234+
dtype: datetime64[ns]
235+
"""
236+
)
237+
238+
def _round(self, freq, mode, ambiguous, nonexistent):
239+
# round the local times
240+
values = _ensure_datetimelike_to_i8(self)
241+
result = round_nsint64(values, mode, freq)
242+
result = self._maybe_mask_results(result, fill_value=NaT)
243+
244+
attribs = self._get_attributes_dict()
245+
attribs['freq'] = None
246+
if 'tz' in attribs:
247+
attribs['tz'] = None
248+
return self._ensure_localized(
249+
self._shallow_copy(result, **attribs), ambiguous, nonexistent
250+
)
251+
252+
@Appender((_round_doc + _round_example).format(op="round"))
253+
def round(self, freq, ambiguous='raise', nonexistent='raise'):
254+
return self._round(
255+
freq, RoundTo.NEAREST_HALF_EVEN, ambiguous, nonexistent
256+
)
257+
258+
@Appender((_round_doc + _floor_example).format(op="floor"))
259+
def floor(self, freq, ambiguous='raise', nonexistent='raise'):
260+
return self._round(freq, RoundTo.MINUS_INFTY, ambiguous, nonexistent)
261+
262+
@Appender((_round_doc + _ceil_example).format(op="ceil"))
263+
def ceil(self, freq, ambiguous='raise', nonexistent='raise'):
264+
return self._round(freq, RoundTo.PLUS_INFTY, ambiguous, nonexistent)
265+
266+
83267
class DatetimeLikeArrayMixin(ExtensionOpsMixin, AttributesMixin):
84268
"""
85269
Shared Base/Mixin class for DatetimeArray, TimedeltaArray, PeriodArray
@@ -1023,3 +1207,39 @@ def validate_dtype_freq(dtype, freq):
10231207
raise IncompatibleFrequency('specified freq and dtype '
10241208
'are different')
10251209
return freq
1210+
1211+
1212+
def _ensure_datetimelike_to_i8(other, to_utc=False):
1213+
"""
1214+
Helper for coercing an input scalar or array to i8.
1215+
1216+
Parameters
1217+
----------
1218+
other : 1d array
1219+
to_utc : bool, default False
1220+
If True, convert the values to UTC before extracting the i8 values
1221+
If False, extract the i8 values directly.
1222+
1223+
Returns
1224+
-------
1225+
i8 1d array
1226+
"""
1227+
from pandas import Index
1228+
from pandas.core.arrays import PeriodArray
1229+
1230+
if lib.is_scalar(other) and isna(other):
1231+
return iNaT
1232+
elif isinstance(other, (PeriodArray, ABCIndexClass)):
1233+
# convert tz if needed
1234+
if getattr(other, 'tz', None) is not None:
1235+
if to_utc:
1236+
other = other.tz_convert('UTC')
1237+
else:
1238+
other = other.tz_localize(None)
1239+
else:
1240+
try:
1241+
return np.array(other, copy=False).view('i8')
1242+
except TypeError:
1243+
# period array cannot be coerced to int
1244+
other = Index(other)
1245+
return other.asi8

pandas/core/arrays/datetimes.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,9 @@ def wrapper(self, other):
156156
return compat.set_function_name(wrapper, opname, cls)
157157

158158

159-
class DatetimeArrayMixin(dtl.DatetimeLikeArrayMixin):
159+
class DatetimeArrayMixin(dtl.DatetimeLikeArrayMixin,
160+
dtl.TimelikeOps,
161+
dtl.DatelikeOps):
160162
"""
161163
Assumes that subclass __new__/__init__ defines:
162164
tz

pandas/core/arrays/timedeltas.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ def method(self, other):
129129
return method
130130

131131

132-
class TimedeltaArrayMixin(dtl.DatetimeLikeArrayMixin):
132+
class TimedeltaArrayMixin(dtl.DatetimeLikeArrayMixin, dtl.TimelikeOps):
133133
_typ = "timedeltaarray"
134134
__array_priority__ = 1000
135135

0 commit comments

Comments
 (0)