Skip to content

Commit eba73e8

Browse files
committed
REF: make Tick entirely cdef
1 parent 2f93c6d commit eba73e8

File tree

4 files changed

+163
-148
lines changed

4 files changed

+163
-148
lines changed

pandas/_libs/tslibs/offsets.pyx

+147-29
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ from typing import Any
55
import warnings
66
from cpython.datetime cimport (PyDateTime_IMPORT,
77
PyDateTime_Check,
8+
PyDate_Check,
89
PyDelta_Check,
910
datetime, timedelta, date,
1011
time as dt_time)
@@ -35,6 +36,8 @@ from pandas._libs.tslibs.np_datetime cimport (
3536
from pandas._libs.tslibs.timezones cimport utc_pytz as UTC
3637
from pandas._libs.tslibs.tzconversion cimport tz_convert_single
3738

39+
from .timedeltas cimport delta_to_nanoseconds
40+
3841
# ---------------------------------------------------------------------
3942
# Constants
4043

@@ -87,19 +90,19 @@ for _d in DAYS:
8790
# Misc Helpers
8891

8992
cdef bint is_offset_object(object obj):
90-
return isinstance(obj, _BaseOffset)
93+
return isinstance(obj, BaseOffset)
9194

9295

9396
cdef bint is_tick_object(object obj):
94-
return isinstance(obj, _Tick)
97+
return isinstance(obj, Tick)
9598

9699

97100
cdef to_offset(object obj):
98101
"""
99102
Wrap pandas.tseries.frequencies.to_offset to keep centralize runtime
100103
imports
101104
"""
102-
if isinstance(obj, _BaseOffset):
105+
if isinstance(obj, BaseOffset):
103106
return obj
104107
from pandas.tseries.frequencies import to_offset
105108
return to_offset(obj)
@@ -167,10 +170,11 @@ def apply_wraps(func):
167170
def wrapper(self, other):
168171
if other is NaT:
169172
return NaT
170-
elif isinstance(other, (timedelta, BaseOffset)):
173+
elif isinstance(other, BaseOffset) or PyDelta_Check(other):
171174
# timedelta path
172175
return func(self, other)
173-
elif isinstance(other, (datetime, date)) or is_datetime64_object(other):
176+
elif is_datetime64_object(other) or PyDate_Check(other):
177+
# PyDate_Check includes date, datetime
174178
other = as_timestamp(other)
175179
else:
176180
# This will end up returning NotImplemented back in __add__
@@ -233,7 +237,6 @@ cdef _wrap_timedelta_result(result):
233237
"""
234238
if PyDelta_Check(result):
235239
# convert Timedelta back to a Tick
236-
from pandas.tseries.offsets import delta_to_tick
237240
return delta_to_tick(result)
238241

239242
return result
@@ -404,7 +407,7 @@ class ApplyTypeError(TypeError):
404407
# ---------------------------------------------------------------------
405408
# Base Classes
406409

407-
cdef class _BaseOffset:
410+
cdef class BaseOffset:
408411
"""
409412
Base class for DateOffset methods that are not overridden by subclasses
410413
and will (after pickle errors are resolved) go into a cdef class.
@@ -483,6 +486,9 @@ cdef class _BaseOffset:
483486
return type(self)(n=1, normalize=self.normalize, **self.kwds)
484487

485488
def __add__(self, other):
489+
if not isinstance(self, BaseOffset):
490+
# cython semantics; this is __radd__
491+
return other.__add__(self)
486492
try:
487493
return self.apply(other)
488494
except ApplyTypeError:
@@ -494,6 +500,9 @@ cdef class _BaseOffset:
494500
elif type(other) == type(self):
495501
return type(self)(self.n - other.n, normalize=self.normalize,
496502
**self.kwds)
503+
elif not isinstance(self, BaseOffset):
504+
# cython semantics, this is __rsub__
505+
return (-other).__add__(self)
497506
else: # pragma: no cover
498507
return NotImplemented
499508

@@ -512,6 +521,9 @@ cdef class _BaseOffset:
512521
elif is_integer_object(other):
513522
return type(self)(n=other * self.n, normalize=self.normalize,
514523
**self.kwds)
524+
elif not isinstance(self, BaseOffset):
525+
# cython semantics, this is __rmul__
526+
return other.__mul__(self)
515527
return NotImplemented
516528

517529
def __neg__(self):
@@ -661,8 +673,8 @@ cdef class _BaseOffset:
661673

662674
# ------------------------------------------------------------------
663675

664-
# Staticmethod so we can call from _Tick.__init__, will be unnecessary
665-
# once BaseOffset is a cdef class and is inherited by _Tick
676+
# Staticmethod so we can call from Tick.__init__, will be unnecessary
677+
# once BaseOffset is a cdef class and is inherited by Tick
666678
@staticmethod
667679
def _validate_n(n):
668680
"""
@@ -762,23 +774,7 @@ cdef class _BaseOffset:
762774
return self.n == 1
763775

764776

765-
class BaseOffset(_BaseOffset):
766-
# Here we add __rfoo__ methods that don't play well with cdef classes
767-
def __rmul__(self, other):
768-
return self.__mul__(other)
769-
770-
def __radd__(self, other):
771-
return self.__add__(other)
772-
773-
def __rsub__(self, other):
774-
return (-self).__add__(other)
775-
776-
777-
cdef class _Tick(_BaseOffset):
778-
"""
779-
dummy class to mix into tseries.offsets.Tick so that in tslibs.period we
780-
can do isinstance checks on _Tick and avoid importing tseries.offsets
781-
"""
777+
cdef class Tick(BaseOffset):
782778

783779
# ensure that reversed-ops with numpy scalars return NotImplemented
784780
__array_priority__ = 1000
@@ -797,13 +793,25 @@ cdef class _Tick(_BaseOffset):
797793
"Tick offset with `normalize=True` are not allowed."
798794
)
799795

796+
@classmethod
797+
def _from_name(cls, suffix=None):
798+
# default _from_name calls cls with no args
799+
if suffix:
800+
raise ValueError(f"Bad freq suffix {suffix}")
801+
return cls()
802+
803+
def _repr_attrs(self) -> str:
804+
# Since cdef classes have no __dict__, we need to override
805+
return ""
806+
800807
@property
801808
def delta(self):
802-
return self.n * self._inc
809+
from .timedeltas import Timedelta
810+
return self.n * Timedelta(self._nanos_inc)
803811

804812
@property
805813
def nanos(self) -> int64_t:
806-
return self.delta.value
814+
return self.n * self._nanos_inc
807815

808816
def is_on_offset(self, dt) -> bool:
809817
return True
@@ -841,13 +849,62 @@ cdef class _Tick(_BaseOffset):
841849
return self.delta.__gt__(other)
842850

843851
def __truediv__(self, other):
844-
if not isinstance(self, _Tick):
852+
if not isinstance(self, Tick):
845853
# cython semantics mean the args are sometimes swapped
846854
result = other.delta.__rtruediv__(self)
847855
else:
848856
result = self.delta.__truediv__(other)
849857
return _wrap_timedelta_result(result)
850858

859+
def __add__(self, other):
860+
if not isinstance(self, Tick):
861+
# cython semantics; this is __radd__
862+
return other.__add__(self)
863+
864+
if isinstance(other, Tick):
865+
if type(self) == type(other):
866+
return type(self)(self.n + other.n)
867+
else:
868+
return delta_to_tick(self.delta + other.delta)
869+
try:
870+
return self.apply(other)
871+
except ApplyTypeError:
872+
# Includes pd.Period
873+
return NotImplemented
874+
except OverflowError as err:
875+
raise OverflowError(
876+
f"the add operation between {self} and {other} will overflow"
877+
) from err
878+
879+
def apply(self, other):
880+
# Timestamp can handle tz and nano sec, thus no need to use apply_wraps
881+
if isinstance(other, ABCTimestamp):
882+
883+
# GH#15126
884+
# in order to avoid a recursive
885+
# call of __add__ and __radd__ if there is
886+
# an exception, when we call using the + operator,
887+
# we directly call the known method
888+
result = other.__add__(self)
889+
if result is NotImplemented:
890+
raise OverflowError
891+
return result
892+
elif other is NaT:
893+
return NaT
894+
elif is_datetime64_object(other) or PyDate_Check(other):
895+
# PyDate_Check includes date, datetime
896+
return as_timestamp(other) + self
897+
898+
if PyDelta_Check(other):
899+
return other + self.delta
900+
elif isinstance(other, type(self)):
901+
# TODO: this is reached in tests that specifically call apply,
902+
# but should not be reached "naturally" because __add__ should
903+
# catch this case first.
904+
return type(self)(self.n + other.n)
905+
906+
raise ApplyTypeError(f"Unhandled type: {type(other).__name__}")
907+
851908
# --------------------------------------------------------------------
852909
# Pickle Methods
853910

@@ -859,6 +916,67 @@ cdef class _Tick(_BaseOffset):
859916
self.normalize = False
860917

861918

919+
cdef class Day(Tick):
920+
_nanos_inc = 24 * 3600 * 1_000_000_000
921+
_prefix = "D"
922+
923+
924+
cdef class Hour(Tick):
925+
_nanos_inc = 3600 * 1_000_000_000
926+
_prefix = "H"
927+
928+
929+
cdef class Minute(Tick):
930+
_nanos_inc = 60 * 1_000_000_000
931+
_prefix = "T"
932+
933+
934+
cdef class Second(Tick):
935+
_nanos_inc = 1_000_000_000
936+
_prefix = "S"
937+
938+
939+
cdef class Milli(Tick):
940+
_nanos_inc = 1_000_000
941+
_prefix = "L"
942+
943+
944+
cdef class Micro(Tick):
945+
_nanos_inc = 1000
946+
_prefix = "U"
947+
948+
949+
cdef class Nano(Tick):
950+
_nanos_inc = 1
951+
_prefix = "N"
952+
953+
954+
def delta_to_tick(delta: timedelta) -> Tick:
955+
if delta.microseconds == 0 and getattr(delta, "nanoseconds", 0) == 0:
956+
# nanoseconds only for pd.Timedelta
957+
if delta.seconds == 0:
958+
return Day(delta.days)
959+
else:
960+
seconds = delta.days * 86400 + delta.seconds
961+
if seconds % 3600 == 0:
962+
return Hour(seconds / 3600)
963+
elif seconds % 60 == 0:
964+
return Minute(seconds / 60)
965+
else:
966+
return Second(seconds)
967+
else:
968+
nanos = delta_to_nanoseconds(delta)
969+
if nanos % 1_000_000 == 0:
970+
return Milli(nanos // 1_000_000)
971+
elif nanos % 1000 == 0:
972+
return Micro(nanos // 1000)
973+
else: # pragma: no cover
974+
return Nano(nanos)
975+
976+
977+
# --------------------------------------------------------------------
978+
979+
862980
class BusinessMixin:
863981
"""
864982
Mixin to business types to provide related functions.

pandas/core/arrays/period.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
period as libperiod,
1313
)
1414
from pandas._libs.tslibs.fields import isleapyear_arr
15+
from pandas._libs.tslibs.offsets import Tick, delta_to_tick
1516
from pandas._libs.tslibs.period import (
1617
DIFFERENT_FREQ,
1718
IncompatibleFrequency,
@@ -45,7 +46,7 @@
4546
import pandas.core.common as com
4647

4748
from pandas.tseries import frequencies
48-
from pandas.tseries.offsets import DateOffset, Tick, delta_to_tick
49+
from pandas.tseries.offsets import DateOffset
4950

5051

5152
def _field_accessor(name: str, alias: int, docstring=None):

pandas/tests/tseries/offsets/test_ticks.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
import numpy as np
88
import pytest
99

10+
from pandas._libs.tslibs.offsets import delta_to_tick
11+
1012
from pandas import Timedelta, Timestamp
1113
import pandas._testing as tm
1214

@@ -33,11 +35,11 @@ def test_apply_ticks():
3335
def test_delta_to_tick():
3436
delta = timedelta(3)
3537

36-
tick = offsets.delta_to_tick(delta)
38+
tick = delta_to_tick(delta)
3739
assert tick == offsets.Day(3)
3840

3941
td = Timedelta(nanoseconds=5)
40-
tick = offsets.delta_to_tick(td)
42+
tick = delta_to_tick(td)
4143
assert tick == Nano(5)
4244

4345

@@ -234,7 +236,7 @@ def test_tick_division(cls):
234236
assert not isinstance(result, cls)
235237
assert result.delta == off.delta / 1000
236238

237-
if cls._inc < Timedelta(seconds=1):
239+
if cls._nanos_inc < Timedelta(seconds=1).value:
238240
# Case where we end up with a bigger class
239241
result = off / 0.001
240242
assert isinstance(result, offsets.Tick)

0 commit comments

Comments
 (0)