-
-
Notifications
You must be signed in to change notification settings - Fork 18.4k
ENH: mul(Tick, float); simplify to_offset #34486
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
Changes from all commits
599914a
5c61312
db87b76
c844352
ea127ba
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 |
---|---|---|
|
@@ -25,7 +25,11 @@ cnp.import_array() | |
from pandas._libs.properties import cache_readonly | ||
|
||
from pandas._libs.tslibs cimport util | ||
from pandas._libs.tslibs.util cimport is_integer_object, is_datetime64_object | ||
from pandas._libs.tslibs.util cimport ( | ||
is_integer_object, | ||
is_datetime64_object, | ||
is_float_object, | ||
) | ||
|
||
from pandas._libs.tslibs.base cimport ABCTimestamp | ||
|
||
|
@@ -743,6 +747,25 @@ cdef class Tick(SingleConstructorOffset): | |
"Tick offset with `normalize=True` are not allowed." | ||
) | ||
|
||
# FIXME: Without making this cpdef, we get AttributeError when calling | ||
# from __mul__ | ||
cpdef Tick _next_higher_resolution(Tick self): | ||
if type(self) is Day: | ||
return Hour(self.n * 24) | ||
if type(self) is Hour: | ||
return Minute(self.n * 60) | ||
if type(self) is Minute: | ||
return Second(self.n * 60) | ||
if type(self) is Second: | ||
return Milli(self.n * 1000) | ||
if type(self) is Milli: | ||
return Micro(self.n * 1000) | ||
if type(self) is Micro: | ||
return Nano(self.n * 1000) | ||
raise NotImplementedError(type(self)) | ||
|
||
# -------------------------------------------------------------------- | ||
|
||
def _repr_attrs(self) -> str: | ||
# Since cdef classes have no __dict__, we need to override | ||
return "" | ||
|
@@ -791,6 +814,21 @@ cdef class Tick(SingleConstructorOffset): | |
def __gt__(self, other): | ||
return self.delta.__gt__(other) | ||
|
||
def __mul__(self, other): | ||
if not isinstance(self, Tick): | ||
# cython semantics, this is __rmul__ | ||
return other.__mul__(self) | ||
if is_float_object(other): | ||
n = other * self.n | ||
# If the new `n` is an integer, we can represent it using the | ||
# same Tick subclass as self, otherwise we need to move up | ||
# to a higher-resolution subclass | ||
if np.isclose(n % 1, 0): | ||
return type(self)(int(n)) | ||
new_self = self._next_higher_resolution() | ||
return new_self * other | ||
return BaseOffset.__mul__(self, other) | ||
|
||
def __truediv__(self, other): | ||
if not isinstance(self, Tick): | ||
# cython semantics mean the args are sometimes swapped | ||
|
@@ -3563,6 +3601,9 @@ cpdef to_offset(freq): | |
>>> to_offset(Hour()) | ||
<Hour> | ||
""" | ||
# TODO: avoid runtime imports | ||
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. really really prefer absolute imports 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. sure. im more concerned about getting rid of the runtime imports |
||
from pandas._libs.tslibs.timedeltas import Timedelta | ||
|
||
if freq is None: | ||
return None | ||
|
||
|
@@ -3589,7 +3630,9 @@ cpdef to_offset(freq): | |
if split[-1] != "" and not split[-1].isspace(): | ||
# the last element must be blank | ||
raise ValueError("last element must be blank") | ||
for sep, stride, name in zip(split[0::4], split[1::4], split[2::4]): | ||
|
||
tups = zip(split[0::4], split[1::4], split[2::4]) | ||
for n, (sep, stride, name) in enumerate(tups): | ||
if sep != "" and not sep.isspace(): | ||
raise ValueError("separator must be spaces") | ||
prefix = _lite_rule_alias.get(name) or name | ||
|
@@ -3598,16 +3641,22 @@ cpdef to_offset(freq): | |
if not stride: | ||
stride = 1 | ||
|
||
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 you give some comments on what is being done here |
||
# TODO: avoid runtime import | ||
from .resolution import Resolution, reso_str_bump_map | ||
if prefix in {"D", "H", "T", "S", "L", "U", "N"}: | ||
# For these prefixes, we have something like "3H" or | ||
# "2.5T", so we can construct a Timedelta with the | ||
# matching unit and get our offset from delta_to_tick | ||
td = Timedelta(1, unit=prefix) | ||
off = delta_to_tick(td) | ||
offset = off * float(stride) | ||
if n != 0: | ||
# If n==0, then stride_sign is already incorporated | ||
# into the offset | ||
offset *= stride_sign | ||
else: | ||
stride = int(stride) | ||
offset = _get_offset(name) | ||
offset = offset * int(np.fabs(stride) * stride_sign) | ||
|
||
if prefix in reso_str_bump_map: | ||
stride, name = Resolution.get_stride_from_decimal( | ||
float(stride), prefix | ||
) | ||
stride = int(stride) | ||
offset = _get_offset(name) | ||
offset = offset * int(np.fabs(stride) * stride_sign) | ||
if delta is None: | ||
delta = offset | ||
else: | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -244,6 +244,22 @@ def test_tick_division(cls): | |
assert result.delta == off.delta / 0.001 | ||
|
||
|
||
def test_tick_mul_float(): | ||
off = Micro(2) | ||
|
||
# Case where we retain type | ||
result = off * 1.5 | ||
expected = Micro(3) | ||
assert result == expected | ||
assert isinstance(result, Micro) | ||
|
||
# Case where we bump up to the next type | ||
result = off * 1.25 | ||
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. was this buggy before? need a whatsnew note if so 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. ATM it raises TypeError for any float, will add whatsnew |
||
expected = Nano(2500) | ||
assert result == expected | ||
assert isinstance(result, Nano) | ||
|
||
|
||
@pytest.mark.parametrize("cls", tick_classes) | ||
def test_tick_rdiv(cls): | ||
off = cls(10) | ||
|
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.
wouldn't this be better as a module level routine?
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.
i dont think so. it uses
self.n
, so really makes sense as a method