-
-
Notifications
You must be signed in to change notification settings - Fork 18k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
BUG: Fix PeriodIndex +/- TimedeltaIndex #23031
Changes from 11 commits
341212f
0704e53
97f6798
a4d2da2
01d3af6
9393f5d
0573f3f
e1ebe09
30d9f6b
9bac746
59748cd
eb252bf
8f5fd55
ed50b9f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,6 @@ | ||
# -*- coding: utf-8 -*- | ||
from datetime import timedelta | ||
import operator | ||
import warnings | ||
|
||
import numpy as np | ||
|
@@ -17,8 +18,8 @@ | |
from pandas.util._decorators import (cache_readonly, deprecate_kwarg) | ||
|
||
from pandas.core.dtypes.common import ( | ||
is_integer_dtype, is_float_dtype, is_period_dtype, | ||
is_datetime64_dtype) | ||
is_integer_dtype, is_float_dtype, is_period_dtype, is_timedelta64_dtype, | ||
is_datetime64_dtype, _TD_DTYPE) | ||
from pandas.core.dtypes.dtypes import PeriodDtype | ||
from pandas.core.dtypes.generic import ABCSeries | ||
|
||
|
@@ -355,24 +356,54 @@ def _add_offset(self, other): | |
return self._time_shift(other.n) | ||
|
||
def _add_delta_td(self, other): | ||
assert isinstance(self.freq, Tick) # checked by calling function | ||
assert isinstance(other, (timedelta, np.timedelta64, Tick)) | ||
nanos = delta_to_nanoseconds(other) | ||
own_offset = frequencies.to_offset(self.freq.rule_code) | ||
|
||
if isinstance(own_offset, Tick): | ||
offset_nanos = delta_to_nanoseconds(own_offset) | ||
if np.all(nanos % offset_nanos == 0): | ||
return self._time_shift(nanos // offset_nanos) | ||
delta = self._check_timedeltalike_freq_compat(other) | ||
|
||
# raise when input doesn't have freq | ||
raise IncompatibleFrequency("Input has different freq from " | ||
"{cls}(freq={freqstr})" | ||
.format(cls=type(self).__name__, | ||
freqstr=self.freqstr)) | ||
# Note: when calling parent class's _add_delta_td, it will call | ||
# delta_to_nanoseconds(delta). Because delta here is an integer, | ||
# delta_to_nanoseconds will return it unchanged. | ||
return DatetimeLikeArrayMixin._add_delta_td(self, delta) | ||
|
||
def _add_delta_tdi(self, other): | ||
assert isinstance(self.freq, Tick) # checked by calling function | ||
|
||
delta = self._check_timedeltalike_freq_compat(other) | ||
return self._addsub_int_array(delta, operator.add) | ||
|
||
def _add_delta(self, other): | ||
ordinal_delta = self._maybe_convert_timedelta(other) | ||
return self._time_shift(ordinal_delta) | ||
""" | ||
Add a timedelta-like, Tick, or TimedeltaIndex-like object | ||
to self. | ||
|
||
Parameters | ||
---------- | ||
other : {timedelta, np.timedelta64, Tick, | ||
TimedeltaIndex, ndarray[timedelta64]} | ||
|
||
Returns | ||
------- | ||
result : same type as self | ||
""" | ||
if not isinstance(self.freq, Tick): | ||
# We cannot add timedelta-like to non-tick PeriodArray | ||
raise IncompatibleFrequency("Input has different freq from " | ||
"{cls}(freq={freqstr})" | ||
.format(cls=type(self).__name__, | ||
freqstr=self.freqstr)) | ||
|
||
# TODO: standardize across datetimelike subclasses whether to return | ||
# i8 view or _shallow_copy | ||
if isinstance(other, (Tick, timedelta, np.timedelta64)): | ||
new_values = self._add_delta_td(other) | ||
return self._shallow_copy(new_values) | ||
elif is_timedelta64_dtype(other): | ||
# ndarray[timedelta64] or TimedeltaArray/index | ||
new_values = self._add_delta_tdi(other) | ||
return self._shallow_copy(new_values) | ||
else: # pragma: no cover | ||
raise TypeError(type(other).__name__) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looking at this and some of the other methods in the datetime/timedelta/period array mixins, I think some de-duplication can be done on the next pass |
||
|
||
@deprecate_kwarg(old_arg_name='n', new_arg_name='periods') | ||
def shift(self, periods): | ||
|
@@ -428,14 +459,9 @@ def _maybe_convert_timedelta(self, other): | |
other, (timedelta, np.timedelta64, Tick, np.ndarray)): | ||
offset = frequencies.to_offset(self.freq.rule_code) | ||
if isinstance(offset, Tick): | ||
if isinstance(other, np.ndarray): | ||
nanos = np.vectorize(delta_to_nanoseconds)(other) | ||
else: | ||
nanos = delta_to_nanoseconds(other) | ||
offset_nanos = delta_to_nanoseconds(offset) | ||
check = np.all(nanos % offset_nanos == 0) | ||
if check: | ||
return nanos // offset_nanos | ||
# _check_timedeltalike_freq_compat will raise if incompatible | ||
delta = self._check_timedeltalike_freq_compat(other) | ||
return delta | ||
elif isinstance(other, DateOffset): | ||
freqstr = other.rule_code | ||
base = frequencies.get_base_alias(freqstr) | ||
|
@@ -454,6 +480,38 @@ def _maybe_convert_timedelta(self, other): | |
raise IncompatibleFrequency(msg.format(cls=type(self).__name__, | ||
freqstr=self.freqstr)) | ||
|
||
def _check_timedeltalike_freq_compat(self, other): | ||
assert isinstance(self.freq, Tick) # checked by calling function | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can u add a doc string here |
||
own_offset = frequencies.to_offset(self.freq.rule_code) | ||
base_nanos = delta_to_nanoseconds(own_offset) | ||
|
||
if isinstance(other, (timedelta, np.timedelta64, Tick)): | ||
nanos = delta_to_nanoseconds(other) | ||
|
||
elif isinstance(other, np.ndarray): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can u comments these cases a bit |
||
assert other.dtype.kind == 'm' | ||
if other.dtype != _TD_DTYPE: | ||
# i.e. non-nano unit | ||
# TODO: disallow unit-less timedelta64 | ||
other = other.astype(_TD_DTYPE) | ||
nanos = other.view('i8') | ||
else: | ||
# TimedeltaArray/Index | ||
nanos = other.asi8 | ||
|
||
if np.all(nanos % base_nanos == 0): | ||
# nanos being added is an integer multiple of the | ||
# base-frequency to self.freq | ||
delta = nanos // base_nanos | ||
# delta is the integer (or integer-array) number of periods | ||
# by which will be added to self. | ||
return delta | ||
|
||
raise IncompatibleFrequency("Input has different freq from " | ||
"{cls}(freq={freqstr})" | ||
.format(cls=type(self).__name__, | ||
freqstr=self.freqstr)) | ||
|
||
|
||
PeriodArrayMixin._add_comparison_ops() | ||
PeriodArrayMixin._add_datetimelike_methods() | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -805,6 +805,7 @@ def assert_index_equal(left, right, exact='equiv', check_names=True, | |
Specify object name being compared, internally used to show appropriate | ||
assertion message | ||
""" | ||
__tracebackhide__ = True | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See #22643 |
||
|
||
def _check_types(l, r, obj='Index'): | ||
if exact: | ||
|
@@ -1048,6 +1049,8 @@ def assert_interval_array_equal(left, right, exact='equiv', | |
|
||
|
||
def raise_assert_detail(obj, message, left, right, diff=None): | ||
__tracebackhide__ = True | ||
|
||
if isinstance(left, np.ndarray): | ||
left = pprint_thing(left) | ||
elif is_categorical_dtype(left): | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Getting the _maybe_convert_timedelta call out of the arithmetic ops makes everything simpler