@@ -5,6 +5,7 @@ from typing import Any
5
5
import warnings
6
6
from cpython.datetime cimport (PyDateTime_IMPORT,
7
7
PyDateTime_Check,
8
+ PyDate_Check,
8
9
PyDelta_Check,
9
10
datetime, timedelta, date,
10
11
time as dt_time)
@@ -35,6 +36,8 @@ from pandas._libs.tslibs.np_datetime cimport (
35
36
from pandas._libs.tslibs.timezones cimport utc_pytz as UTC
36
37
from pandas._libs.tslibs.tzconversion cimport tz_convert_single
37
38
39
+ from .timedeltas cimport delta_to_nanoseconds
40
+
38
41
# ---------------------------------------------------------------------
39
42
# Constants
40
43
@@ -87,19 +90,19 @@ for _d in DAYS:
87
90
# Misc Helpers
88
91
89
92
cdef bint is_offset_object(object obj):
90
- return isinstance (obj, _BaseOffset )
93
+ return isinstance (obj, BaseOffset )
91
94
92
95
93
96
cdef bint is_tick_object(object obj):
94
- return isinstance (obj, _Tick )
97
+ return isinstance (obj, Tick )
95
98
96
99
97
100
cdef to_offset(object obj):
98
101
"""
99
102
Wrap pandas.tseries.frequencies.to_offset to keep centralize runtime
100
103
imports
101
104
"""
102
- if isinstance (obj, _BaseOffset ):
105
+ if isinstance (obj, BaseOffset ):
103
106
return obj
104
107
from pandas.tseries.frequencies import to_offset
105
108
return to_offset(obj)
@@ -167,10 +170,11 @@ def apply_wraps(func):
167
170
def wrapper (self , other ):
168
171
if other is NaT:
169
172
return NaT
170
- elif isinstance (other, (timedelta, BaseOffset)):
173
+ elif isinstance (other, BaseOffset) or PyDelta_Check(other ):
171
174
# timedelta path
172
175
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
174
178
other = as_timestamp(other)
175
179
else :
176
180
# This will end up returning NotImplemented back in __add__
@@ -233,7 +237,6 @@ cdef _wrap_timedelta_result(result):
233
237
"""
234
238
if PyDelta_Check(result):
235
239
# convert Timedelta back to a Tick
236
- from pandas.tseries.offsets import delta_to_tick
237
240
return delta_to_tick(result)
238
241
239
242
return result
@@ -404,7 +407,7 @@ class ApplyTypeError(TypeError):
404
407
# ---------------------------------------------------------------------
405
408
# Base Classes
406
409
407
- cdef class _BaseOffset :
410
+ cdef class BaseOffset :
408
411
"""
409
412
Base class for DateOffset methods that are not overridden by subclasses
410
413
and will (after pickle errors are resolved) go into a cdef class.
@@ -483,6 +486,9 @@ cdef class _BaseOffset:
483
486
return type (self )(n = 1 , normalize = self .normalize, ** self .kwds)
484
487
485
488
def __add__ (self , other ):
489
+ if not isinstance (self , BaseOffset):
490
+ # cython semantics; this is __radd__
491
+ return other.__add__ (self )
486
492
try :
487
493
return self .apply(other)
488
494
except ApplyTypeError:
@@ -494,6 +500,9 @@ cdef class _BaseOffset:
494
500
elif type (other) == type (self ):
495
501
return type (self )(self .n - other.n, normalize = self .normalize,
496
502
** self .kwds)
503
+ elif not isinstance (self , BaseOffset):
504
+ # cython semantics, this is __rsub__
505
+ return (- other).__add__(self )
497
506
else : # pragma: no cover
498
507
return NotImplemented
499
508
@@ -512,6 +521,9 @@ cdef class _BaseOffset:
512
521
elif is_integer_object(other):
513
522
return type (self )(n = other * self .n, normalize = self .normalize,
514
523
** self .kwds)
524
+ elif not isinstance (self , BaseOffset):
525
+ # cython semantics, this is __rmul__
526
+ return other.__mul__ (self )
515
527
return NotImplemented
516
528
517
529
def __neg__ (self ):
@@ -661,8 +673,8 @@ cdef class _BaseOffset:
661
673
662
674
# ------------------------------------------------------------------
663
675
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
666
678
@staticmethod
667
679
def _validate_n (n ):
668
680
"""
@@ -762,23 +774,7 @@ cdef class _BaseOffset:
762
774
return self.n == 1
763
775
764
776
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 ):
782
778
783
779
# ensure that reversed-ops with numpy scalars return NotImplemented
784
780
__array_priority__ = 1000
@@ -797,13 +793,25 @@ cdef class _Tick(_BaseOffset):
797
793
" Tick offset with `normalize=True` are not allowed."
798
794
)
799
795
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
+
800
807
@property
801
808
def delta(self ):
802
- return self .n * self ._inc
809
+ from .timedeltas import Timedelta
810
+ return self .n * Timedelta(self ._nanos_inc)
803
811
804
812
@property
805
813
def nanos (self ) -> int64_t:
806
- return self.delta. value
814
+ return self.n * self. _nanos_inc
807
815
808
816
def is_on_offset(self , dt ) -> bool:
809
817
return True
@@ -841,13 +849,62 @@ cdef class _Tick(_BaseOffset):
841
849
return self .delta.__gt__ (other)
842
850
843
851
def __truediv__ (self , other ):
844
- if not isinstance (self , _Tick ):
852
+ if not isinstance (self , Tick ):
845
853
# cython semantics mean the args are sometimes swapped
846
854
result = other.delta.__rtruediv__ (self )
847
855
else :
848
856
result = self .delta.__truediv__ (other)
849
857
return _wrap_timedelta_result(result)
850
858
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
+
851
908
# --------------------------------------------------------------------
852
909
# Pickle Methods
853
910
@@ -859,6 +916,67 @@ cdef class _Tick(_BaseOffset):
859
916
self .normalize = False
860
917
861
918
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
+
862
980
class BusinessMixin:
863
981
"""
864
982
Mixin to business types to provide related functions.
0 commit comments