Skip to content

Commit 58048e7

Browse files
jbrockmendeljreback
authored andcommitted
REF: de-duplicate datetimelike wrapping code (#23099)
1 parent 08e2752 commit 58048e7

File tree

5 files changed

+119
-193
lines changed

5 files changed

+119
-193
lines changed

pandas/core/arrays/datetimelike.py

+6-16
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
from pandas.util._decorators import deprecate_kwarg
4242

4343

44-
def _make_comparison_op(op, cls):
44+
def _make_comparison_op(cls, op):
4545
# TODO: share code with indexes.base version? Main difference is that
4646
# the block for MultiIndex was removed here.
4747
def cmp_method(self, other):
@@ -740,6 +740,9 @@ def __isub__(self, other):
740740
# --------------------------------------------------------------
741741
# Comparison Methods
742742

743+
# Called by _add_comparison_methods defined in ExtensionOpsMixin
744+
_create_comparison_method = classmethod(_make_comparison_op)
745+
743746
def _evaluate_compare(self, other, op):
744747
"""
745748
We have been called because a comparison between
@@ -773,21 +776,8 @@ def _evaluate_compare(self, other, op):
773776
result[mask] = filler
774777
return result
775778

776-
# TODO: get this from ExtensionOpsMixin
777-
@classmethod
778-
def _add_comparison_methods(cls):
779-
""" add in comparison methods """
780-
# DatetimeArray and TimedeltaArray comparison methods will
781-
# call these as their super(...) methods
782-
cls.__eq__ = _make_comparison_op(operator.eq, cls)
783-
cls.__ne__ = _make_comparison_op(operator.ne, cls)
784-
cls.__lt__ = _make_comparison_op(operator.lt, cls)
785-
cls.__gt__ = _make_comparison_op(operator.gt, cls)
786-
cls.__le__ = _make_comparison_op(operator.le, cls)
787-
cls.__ge__ = _make_comparison_op(operator.ge, cls)
788-
789-
790-
DatetimeLikeArrayMixin._add_comparison_methods()
779+
780+
DatetimeLikeArrayMixin._add_comparison_ops()
791781

792782

793783
# -------------------------------------------------------------------

pandas/core/indexes/datetimelike.py

+57
Original file line numberDiff line numberDiff line change
@@ -744,3 +744,60 @@ def wrap_arithmetic_op(self, other, result):
744744
res_name = ops.get_op_result_name(self, other)
745745
result.name = res_name
746746
return result
747+
748+
749+
def wrap_array_method(method, pin_name=False):
750+
"""
751+
Wrap a DatetimeArray/TimedeltaArray/PeriodArray method so that the
752+
returned object is an Index subclass instead of ndarray or ExtensionArray
753+
subclass.
754+
755+
Parameters
756+
----------
757+
method : method of Datetime/Timedelta/Period Array class
758+
pin_name : bool
759+
Whether to set name=self.name on the output Index
760+
761+
Returns
762+
-------
763+
method
764+
"""
765+
def index_method(self, *args, **kwargs):
766+
result = method(self, *args, **kwargs)
767+
768+
# Index.__new__ will choose the appropriate subclass to return
769+
result = Index(result)
770+
if pin_name:
771+
result.name = self.name
772+
return result
773+
774+
index_method.__name__ = method.__name__
775+
index_method.__doc__ = method.__doc__
776+
return index_method
777+
778+
779+
def wrap_field_accessor(prop):
780+
"""
781+
Wrap a DatetimeArray/TimedeltaArray/PeriodArray array-returning property
782+
to return an Index subclass instead of ndarray or ExtensionArray subclass.
783+
784+
Parameters
785+
----------
786+
prop : property
787+
788+
Returns
789+
-------
790+
new_prop : property
791+
"""
792+
fget = prop.fget
793+
794+
def f(self):
795+
result = fget(self)
796+
if is_bool_dtype(result):
797+
# return numpy array b/c there is no BoolIndex
798+
return result
799+
return Index(result, name=self.name)
800+
801+
f.__name__ = fget.__name__
802+
f.__doc__ = fget.__doc__
803+
return property(f)

pandas/core/indexes/datetimes.py

+31-96
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
is_integer_dtype,
2121
is_datetime64_ns_dtype,
2222
is_period_dtype,
23-
is_bool_dtype,
2423
is_string_like,
2524
is_list_like,
2625
is_scalar,
@@ -34,11 +33,12 @@
3433
from pandas.core.arrays import datetimelike as dtl
3534

3635
from pandas.core.indexes.base import Index, _index_shared_docs
37-
from pandas.core.indexes.numeric import Int64Index, Float64Index
36+
from pandas.core.indexes.numeric import Int64Index
3837
import pandas.compat as compat
3938
from pandas.tseries.frequencies import to_offset, Resolution
4039
from pandas.core.indexes.datetimelike import (
41-
DatelikeOps, TimelikeOps, DatetimeIndexOpsMixin)
40+
DatelikeOps, TimelikeOps, DatetimeIndexOpsMixin,
41+
wrap_field_accessor, wrap_array_method)
4242
from pandas.tseries.offsets import (
4343
generate_range, CDay, prefix_mapping)
4444

@@ -53,49 +53,6 @@
5353
from pandas._libs.tslibs import (timezones, conversion, fields, parsing,
5454
ccalendar)
5555

56-
# -------- some conversion wrapper functions
57-
58-
59-
def _wrap_field_accessor(name):
60-
fget = getattr(DatetimeArrayMixin, name).fget
61-
62-
def f(self):
63-
result = fget(self)
64-
if is_bool_dtype(result):
65-
return result
66-
return Index(result, name=self.name)
67-
68-
f.__name__ = name
69-
f.__doc__ = fget.__doc__
70-
return property(f)
71-
72-
73-
def _wrap_in_index(name):
74-
meth = getattr(DatetimeArrayMixin, name)
75-
76-
def func(self, *args, **kwargs):
77-
result = meth(self, *args, **kwargs)
78-
return Index(result, name=self.name)
79-
80-
func.__doc__ = meth.__doc__
81-
func.__name__ = name
82-
return func
83-
84-
85-
def _dt_index_cmp(cls, op):
86-
"""
87-
Wrap comparison operations to convert datetime-like to datetime64
88-
"""
89-
opname = '__{name}__'.format(name=op.__name__)
90-
91-
def wrapper(self, other):
92-
result = getattr(DatetimeArrayMixin, opname)(self, other)
93-
if is_bool_dtype(result):
94-
return result
95-
return Index(result)
96-
97-
return compat.set_function_name(wrapper, opname, cls)
98-
9956

10057
def _new_DatetimeIndex(cls, d):
10158
""" This is called upon unpickling, rather than the default which doesn't
@@ -233,16 +190,6 @@ def _join_i8_wrapper(joinf, **kwargs):
233190
_left_indexer_unique = _join_i8_wrapper(
234191
libjoin.left_join_indexer_unique_int64, with_indexers=False)
235192

236-
@classmethod
237-
def _add_comparison_methods(cls):
238-
""" add in comparison methods """
239-
cls.__eq__ = _dt_index_cmp(cls, operator.eq)
240-
cls.__ne__ = _dt_index_cmp(cls, operator.ne)
241-
cls.__lt__ = _dt_index_cmp(cls, operator.lt)
242-
cls.__gt__ = _dt_index_cmp(cls, operator.gt)
243-
cls.__le__ = _dt_index_cmp(cls, operator.le)
244-
cls.__ge__ = _dt_index_cmp(cls, operator.ge)
245-
246193
_engine_type = libindex.DatetimeEngine
247194

248195
tz = None
@@ -1273,38 +1220,38 @@ def slice_indexer(self, start=None, end=None, step=None, kind=None):
12731220
else:
12741221
raise
12751222

1276-
year = _wrap_field_accessor('year')
1277-
month = _wrap_field_accessor('month')
1278-
day = _wrap_field_accessor('day')
1279-
hour = _wrap_field_accessor('hour')
1280-
minute = _wrap_field_accessor('minute')
1281-
second = _wrap_field_accessor('second')
1282-
microsecond = _wrap_field_accessor('microsecond')
1283-
nanosecond = _wrap_field_accessor('nanosecond')
1284-
weekofyear = _wrap_field_accessor('weekofyear')
1223+
year = wrap_field_accessor(DatetimeArrayMixin.year)
1224+
month = wrap_field_accessor(DatetimeArrayMixin.month)
1225+
day = wrap_field_accessor(DatetimeArrayMixin.day)
1226+
hour = wrap_field_accessor(DatetimeArrayMixin.hour)
1227+
minute = wrap_field_accessor(DatetimeArrayMixin.minute)
1228+
second = wrap_field_accessor(DatetimeArrayMixin.second)
1229+
microsecond = wrap_field_accessor(DatetimeArrayMixin.microsecond)
1230+
nanosecond = wrap_field_accessor(DatetimeArrayMixin.nanosecond)
1231+
weekofyear = wrap_field_accessor(DatetimeArrayMixin.weekofyear)
12851232
week = weekofyear
1286-
dayofweek = _wrap_field_accessor('dayofweek')
1233+
dayofweek = wrap_field_accessor(DatetimeArrayMixin.dayofweek)
12871234
weekday = dayofweek
12881235

1289-
weekday_name = _wrap_field_accessor('weekday_name')
1236+
weekday_name = wrap_field_accessor(DatetimeArrayMixin.weekday_name)
12901237

1291-
dayofyear = _wrap_field_accessor('dayofyear')
1292-
quarter = _wrap_field_accessor('quarter')
1293-
days_in_month = _wrap_field_accessor('days_in_month')
1238+
dayofyear = wrap_field_accessor(DatetimeArrayMixin.dayofyear)
1239+
quarter = wrap_field_accessor(DatetimeArrayMixin.quarter)
1240+
days_in_month = wrap_field_accessor(DatetimeArrayMixin.days_in_month)
12941241
daysinmonth = days_in_month
1295-
is_month_start = _wrap_field_accessor('is_month_start')
1296-
is_month_end = _wrap_field_accessor('is_month_end')
1297-
is_quarter_start = _wrap_field_accessor('is_quarter_start')
1298-
is_quarter_end = _wrap_field_accessor('is_quarter_end')
1299-
is_year_start = _wrap_field_accessor('is_year_start')
1300-
is_year_end = _wrap_field_accessor('is_year_end')
1301-
is_leap_year = _wrap_field_accessor('is_leap_year')
1302-
1303-
@Appender(DatetimeArrayMixin.normalize.__doc__)
1304-
def normalize(self):
1305-
result = DatetimeArrayMixin.normalize(self)
1306-
result.name = self.name
1307-
return result
1242+
is_month_start = wrap_field_accessor(DatetimeArrayMixin.is_month_start)
1243+
is_month_end = wrap_field_accessor(DatetimeArrayMixin.is_month_end)
1244+
is_quarter_start = wrap_field_accessor(DatetimeArrayMixin.is_quarter_start)
1245+
is_quarter_end = wrap_field_accessor(DatetimeArrayMixin.is_quarter_end)
1246+
is_year_start = wrap_field_accessor(DatetimeArrayMixin.is_year_start)
1247+
is_year_end = wrap_field_accessor(DatetimeArrayMixin.is_year_end)
1248+
is_leap_year = wrap_field_accessor(DatetimeArrayMixin.is_leap_year)
1249+
1250+
normalize = wrap_array_method(DatetimeArrayMixin.normalize, True)
1251+
to_julian_date = wrap_array_method(DatetimeArrayMixin.to_julian_date,
1252+
False)
1253+
month_name = wrap_array_method(DatetimeArrayMixin.month_name, True)
1254+
day_name = wrap_array_method(DatetimeArrayMixin.day_name, True)
13081255

13091256
@Substitution(klass='DatetimeIndex')
13101257
@Appender(_shared_docs['searchsorted'])
@@ -1492,20 +1439,8 @@ def indexer_between_time(self, start_time, end_time, include_start=True,
14921439

14931440
return mask.nonzero()[0]
14941441

1495-
def to_julian_date(self):
1496-
"""
1497-
Convert DatetimeIndex to Float64Index of Julian Dates.
1498-
0 Julian date is noon January 1, 4713 BC.
1499-
http://en.wikipedia.org/wiki/Julian_day
1500-
"""
1501-
result = DatetimeArrayMixin.to_julian_date(self)
1502-
return Float64Index(result)
1503-
1504-
month_name = _wrap_in_index("month_name")
1505-
day_name = _wrap_in_index("day_name")
1506-
15071442

1508-
DatetimeIndex._add_comparison_methods()
1443+
DatetimeIndex._add_comparison_ops()
15091444
DatetimeIndex._add_numeric_methods_disabled()
15101445
DatetimeIndex._add_logical_methods_disabled()
15111446
DatetimeIndex._add_datetimelike_methods()

pandas/core/indexes/period.py

+17-31
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@
2020
from pandas.tseries.frequencies import get_freq_code as _gfc
2121

2222
from pandas.core.indexes.datetimes import DatetimeIndex, Int64Index, Index
23-
from pandas.core.indexes.datetimelike import DatelikeOps, DatetimeIndexOpsMixin
23+
from pandas.core.indexes.datetimelike import (
24+
DatelikeOps, DatetimeIndexOpsMixin,
25+
wrap_array_method, wrap_field_accessor)
2426
from pandas.core.tools.datetimes import parse_time_string
2527

2628
from pandas._libs.lib import infer_dtype
@@ -43,19 +45,6 @@
4345
_index_doc_kwargs.update(
4446
dict(target_klass='PeriodIndex or list of Periods'))
4547

46-
47-
def _wrap_field_accessor(name):
48-
fget = getattr(PeriodArrayMixin, name).fget
49-
50-
def f(self):
51-
result = fget(self)
52-
return Index(result, name=self.name)
53-
54-
f.__name__ = name
55-
f.__doc__ = fget.__doc__
56-
return property(f)
57-
58-
5948
# --- Period index sketch
6049

6150

@@ -431,22 +420,24 @@ def is_full(self):
431420
values = self.asi8
432421
return ((values[1:] - values[:-1]) < 2).all()
433422

434-
year = _wrap_field_accessor('year')
435-
month = _wrap_field_accessor('month')
436-
day = _wrap_field_accessor('day')
437-
hour = _wrap_field_accessor('hour')
438-
minute = _wrap_field_accessor('minute')
439-
second = _wrap_field_accessor('second')
440-
weekofyear = _wrap_field_accessor('week')
423+
year = wrap_field_accessor(PeriodArrayMixin.year)
424+
month = wrap_field_accessor(PeriodArrayMixin.month)
425+
day = wrap_field_accessor(PeriodArrayMixin.day)
426+
hour = wrap_field_accessor(PeriodArrayMixin.hour)
427+
minute = wrap_field_accessor(PeriodArrayMixin.minute)
428+
second = wrap_field_accessor(PeriodArrayMixin.second)
429+
weekofyear = wrap_field_accessor(PeriodArrayMixin.week)
441430
week = weekofyear
442-
dayofweek = _wrap_field_accessor('dayofweek')
431+
dayofweek = wrap_field_accessor(PeriodArrayMixin.dayofweek)
443432
weekday = dayofweek
444-
dayofyear = day_of_year = _wrap_field_accessor('dayofyear')
445-
quarter = _wrap_field_accessor('quarter')
446-
qyear = _wrap_field_accessor('qyear')
447-
days_in_month = _wrap_field_accessor('days_in_month')
433+
dayofyear = day_of_year = wrap_field_accessor(PeriodArrayMixin.dayofyear)
434+
quarter = wrap_field_accessor(PeriodArrayMixin.quarter)
435+
qyear = wrap_field_accessor(PeriodArrayMixin.qyear)
436+
days_in_month = wrap_field_accessor(PeriodArrayMixin.days_in_month)
448437
daysinmonth = days_in_month
449438

439+
to_timestamp = wrap_array_method(PeriodArrayMixin.to_timestamp, True)
440+
450441
@property
451442
@Appender(PeriodArrayMixin.start_time.__doc__)
452443
def start_time(self):
@@ -461,11 +452,6 @@ def _mpl_repr(self):
461452
# how to represent ourselves to matplotlib
462453
return self.astype(object).values
463454

464-
@Appender(PeriodArrayMixin.to_timestamp.__doc__)
465-
def to_timestamp(self, freq=None, how='start'):
466-
result = PeriodArrayMixin.to_timestamp(self, freq=freq, how=how)
467-
return DatetimeIndex(result, name=self.name)
468-
469455
@property
470456
def inferred_type(self):
471457
# b/c data is represented as ints make sure we can't have ambiguous

0 commit comments

Comments
 (0)