Skip to content

Commit 6eb34f1

Browse files
jbrockmendelbrock
and
brock
authored
ENH: mul(Tick, float); simplify to_offset (#34486)
* ENH: mul(Tick, float); simplify to_offset * troubleshoot docbuild * whatsnew, comments Co-authored-by: brock <[email protected]>
1 parent 4fcc6a6 commit 6eb34f1

File tree

3 files changed

+77
-11
lines changed

3 files changed

+77
-11
lines changed

doc/source/whatsnew/v1.1.0.rst

+1
Original file line numberDiff line numberDiff line change
@@ -997,6 +997,7 @@ Other
997997
- Bug in :meth:`DataFrame.plot.scatter` caused an error when plotting variable marker sizes (:issue:`32904`)
998998
- :class:`IntegerArray` now implements the ``sum`` operation (:issue:`33172`)
999999
- Bug in :class:`Tick` comparisons raising ``TypeError`` when comparing against timedelta-like objects (:issue:`34088`)
1000+
- Bug in :class:`Tick` multiplication raising ``TypeError`` when multiplying by a float (:issue:`34486`)
10001001

10011002
.. ---------------------------------------------------------------------------
10021003

pandas/_libs/tslibs/offsets.pyx

+60-11
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,11 @@ cnp.import_array()
2525
from pandas._libs.properties import cache_readonly
2626

2727
from pandas._libs.tslibs cimport util
28-
from pandas._libs.tslibs.util cimport is_integer_object, is_datetime64_object
28+
from pandas._libs.tslibs.util cimport (
29+
is_integer_object,
30+
is_datetime64_object,
31+
is_float_object,
32+
)
2933

3034
from pandas._libs.tslibs.base cimport ABCTimestamp
3135

@@ -743,6 +747,25 @@ cdef class Tick(SingleConstructorOffset):
743747
"Tick offset with `normalize=True` are not allowed."
744748
)
745749

750+
# FIXME: Without making this cpdef, we get AttributeError when calling
751+
# from __mul__
752+
cpdef Tick _next_higher_resolution(Tick self):
753+
if type(self) is Day:
754+
return Hour(self.n * 24)
755+
if type(self) is Hour:
756+
return Minute(self.n * 60)
757+
if type(self) is Minute:
758+
return Second(self.n * 60)
759+
if type(self) is Second:
760+
return Milli(self.n * 1000)
761+
if type(self) is Milli:
762+
return Micro(self.n * 1000)
763+
if type(self) is Micro:
764+
return Nano(self.n * 1000)
765+
raise NotImplementedError(type(self))
766+
767+
# --------------------------------------------------------------------
768+
746769
def _repr_attrs(self) -> str:
747770
# Since cdef classes have no __dict__, we need to override
748771
return ""
@@ -791,6 +814,21 @@ cdef class Tick(SingleConstructorOffset):
791814
def __gt__(self, other):
792815
return self.delta.__gt__(other)
793816

817+
def __mul__(self, other):
818+
if not isinstance(self, Tick):
819+
# cython semantics, this is __rmul__
820+
return other.__mul__(self)
821+
if is_float_object(other):
822+
n = other * self.n
823+
# If the new `n` is an integer, we can represent it using the
824+
# same Tick subclass as self, otherwise we need to move up
825+
# to a higher-resolution subclass
826+
if np.isclose(n % 1, 0):
827+
return type(self)(int(n))
828+
new_self = self._next_higher_resolution()
829+
return new_self * other
830+
return BaseOffset.__mul__(self, other)
831+
794832
def __truediv__(self, other):
795833
if not isinstance(self, Tick):
796834
# cython semantics mean the args are sometimes swapped
@@ -3563,6 +3601,9 @@ cpdef to_offset(freq):
35633601
>>> to_offset(Hour())
35643602
<Hour>
35653603
"""
3604+
# TODO: avoid runtime imports
3605+
from pandas._libs.tslibs.timedeltas import Timedelta
3606+
35663607
if freq is None:
35673608
return None
35683609

@@ -3589,7 +3630,9 @@ cpdef to_offset(freq):
35893630
if split[-1] != "" and not split[-1].isspace():
35903631
# the last element must be blank
35913632
raise ValueError("last element must be blank")
3592-
for sep, stride, name in zip(split[0::4], split[1::4], split[2::4]):
3633+
3634+
tups = zip(split[0::4], split[1::4], split[2::4])
3635+
for n, (sep, stride, name) in enumerate(tups):
35933636
if sep != "" and not sep.isspace():
35943637
raise ValueError("separator must be spaces")
35953638
prefix = _lite_rule_alias.get(name) or name
@@ -3598,16 +3641,22 @@ cpdef to_offset(freq):
35983641
if not stride:
35993642
stride = 1
36003643

3601-
# TODO: avoid runtime import
3602-
from .resolution import Resolution, reso_str_bump_map
3644+
if prefix in {"D", "H", "T", "S", "L", "U", "N"}:
3645+
# For these prefixes, we have something like "3H" or
3646+
# "2.5T", so we can construct a Timedelta with the
3647+
# matching unit and get our offset from delta_to_tick
3648+
td = Timedelta(1, unit=prefix)
3649+
off = delta_to_tick(td)
3650+
offset = off * float(stride)
3651+
if n != 0:
3652+
# If n==0, then stride_sign is already incorporated
3653+
# into the offset
3654+
offset *= stride_sign
3655+
else:
3656+
stride = int(stride)
3657+
offset = _get_offset(name)
3658+
offset = offset * int(np.fabs(stride) * stride_sign)
36033659

3604-
if prefix in reso_str_bump_map:
3605-
stride, name = Resolution.get_stride_from_decimal(
3606-
float(stride), prefix
3607-
)
3608-
stride = int(stride)
3609-
offset = _get_offset(name)
3610-
offset = offset * int(np.fabs(stride) * stride_sign)
36113660
if delta is None:
36123661
delta = offset
36133662
else:

pandas/tests/tseries/offsets/test_ticks.py

+16
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,22 @@ def test_tick_division(cls):
244244
assert result.delta == off.delta / 0.001
245245

246246

247+
def test_tick_mul_float():
248+
off = Micro(2)
249+
250+
# Case where we retain type
251+
result = off * 1.5
252+
expected = Micro(3)
253+
assert result == expected
254+
assert isinstance(result, Micro)
255+
256+
# Case where we bump up to the next type
257+
result = off * 1.25
258+
expected = Nano(2500)
259+
assert result == expected
260+
assert isinstance(result, Nano)
261+
262+
247263
@pytest.mark.parametrize("cls", tick_classes)
248264
def test_tick_rdiv(cls):
249265
off = cls(10)

0 commit comments

Comments
 (0)