@@ -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)
@@ -161,10 +164,11 @@ def apply_wraps(func):
161
164
162
165
if other is NaT:
163
166
return NaT
164
- elif isinstance (other, (timedelta, BaseOffset)):
167
+ elif isinstance (other, BaseOffset) or PyDelta_Check(other ):
165
168
# timedelta path
166
169
return func(self , other)
167
- elif isinstance (other, (datetime, date)) or is_datetime64_object(other):
170
+ elif is_datetime64_object(other) or PyDate_Check(other):
171
+ # PyDate_Check includes date, datetime
168
172
other = Timestamp(other)
169
173
else :
170
174
# This will end up returning NotImplemented back in __add__
@@ -227,7 +231,6 @@ cdef _wrap_timedelta_result(result):
227
231
"""
228
232
if PyDelta_Check(result):
229
233
# convert Timedelta back to a Tick
230
- from pandas.tseries.offsets import delta_to_tick
231
234
return delta_to_tick(result)
232
235
233
236
return result
@@ -398,7 +401,7 @@ class ApplyTypeError(TypeError):
398
401
# ---------------------------------------------------------------------
399
402
# Base Classes
400
403
401
- cdef class _BaseOffset :
404
+ cdef class BaseOffset :
402
405
"""
403
406
Base class for DateOffset methods that are not overridden by subclasses
404
407
and will (after pickle errors are resolved) go into a cdef class.
@@ -477,6 +480,9 @@ cdef class _BaseOffset:
477
480
return type (self )(n = 1 , normalize = self .normalize, ** self .kwds)
478
481
479
482
def __add__ (self , other ):
483
+ if not isinstance (self , BaseOffset):
484
+ # cython semantics; this is __radd__
485
+ return other.__add__ (self )
480
486
try :
481
487
return self .apply(other)
482
488
except ApplyTypeError:
@@ -488,6 +494,9 @@ cdef class _BaseOffset:
488
494
elif type (other) == type (self ):
489
495
return type (self )(self .n - other.n, normalize = self .normalize,
490
496
** self .kwds)
497
+ elif not isinstance (self , BaseOffset):
498
+ # cython semantics, this is __rsub__
499
+ return (- other).__add__(self )
491
500
else : # pragma: no cover
492
501
return NotImplemented
493
502
@@ -506,6 +515,9 @@ cdef class _BaseOffset:
506
515
elif is_integer_object(other):
507
516
return type (self )(n = other * self .n, normalize = self .normalize,
508
517
** self .kwds)
518
+ elif not isinstance (self , BaseOffset):
519
+ # cython semantics, this is __rmul__
520
+ return other.__mul__ (self )
509
521
return NotImplemented
510
522
511
523
def __neg__ (self ):
@@ -657,8 +669,8 @@ cdef class _BaseOffset:
657
669
658
670
# ------------------------------------------------------------------
659
671
660
- # Staticmethod so we can call from _Tick .__init__, will be unnecessary
661
- # once BaseOffset is a cdef class and is inherited by _Tick
672
+ # Staticmethod so we can call from Tick .__init__, will be unnecessary
673
+ # once BaseOffset is a cdef class and is inherited by Tick
662
674
@staticmethod
663
675
def _validate_n (n ):
664
676
"""
@@ -758,24 +770,7 @@ cdef class _BaseOffset:
758
770
return self.n == 1
759
771
760
772
761
- class BaseOffset(_BaseOffset ):
762
- # Here we add __rfoo__ methods that don't play well with cdef classes
763
- def __rmul__ (self , other ):
764
- return self .__mul__ (other)
765
-
766
- def __radd__ (self , other ):
767
- return self .__add__ (other)
768
-
769
- def __rsub__ (self , other ):
770
- return (- self ).__add__(other)
771
-
772
-
773
- cdef class _Tick(_BaseOffset):
774
- """
775
- dummy class to mix into tseries.offsets.Tick so that in tslibs.period we
776
- can do isinstance checks on _Tick and avoid importing tseries.offsets
777
- """
778
-
773
+ cdef class Tick(BaseOffset ):
779
774
# ensure that reversed-ops with numpy scalars return NotImplemented
780
775
__array_priority__ = 1000
781
776
_adjust_dst = False
@@ -793,13 +788,25 @@ cdef class _Tick(_BaseOffset):
793
788
" Tick offset with `normalize=True` are not allowed."
794
789
)
795
790
791
+ @classmethod
792
+ def _from_name (cls , suffix = None ):
793
+ # default _from_name calls cls with no args
794
+ if suffix:
795
+ raise ValueError (f" Bad freq suffix {suffix}" )
796
+ return cls ()
797
+
798
+ def _repr_attrs (self ) -> str:
799
+ # Since cdef classes have no __dict__ , we need to override
800
+ return ""
801
+
796
802
@property
797
803
def delta(self ):
798
- return self .n * self ._inc
804
+ from .timedeltas import Timedelta
805
+ return self .n * Timedelta(self ._nanos_inc)
799
806
800
807
@property
801
808
def nanos (self ) -> int64_t:
802
- return self.delta. value
809
+ return self.n * self. _nanos_inc
803
810
804
811
def is_on_offset(self , dt ) -> bool:
805
812
return True
@@ -837,13 +844,63 @@ cdef class _Tick(_BaseOffset):
837
844
return self .delta.__gt__ (other)
838
845
839
846
def __truediv__ (self , other ):
840
- if not isinstance (self , _Tick ):
847
+ if not isinstance (self , Tick ):
841
848
# cython semantics mean the args are sometimes swapped
842
849
result = other.delta.__rtruediv__ (self )
843
850
else :
844
851
result = self .delta.__truediv__ (other)
845
852
return _wrap_timedelta_result(result)
846
853
854
+ def __add__ (self , other ):
855
+ if not isinstance (self , Tick):
856
+ # cython semantics; this is __radd__
857
+ return other.__add__ (self )
858
+
859
+ if isinstance (other, Tick):
860
+ if type (self ) == type (other):
861
+ return type (self )(self .n + other.n)
862
+ else :
863
+ return delta_to_tick(self .delta + other.delta)
864
+ try :
865
+ return self .apply(other)
866
+ except ApplyTypeError:
867
+ # Includes pd.Period
868
+ return NotImplemented
869
+ except OverflowError as err:
870
+ raise OverflowError (
871
+ f" the add operation between {self} and {other} will overflow"
872
+ ) from err
873
+
874
+ def apply (self , other ):
875
+ # Timestamp can handle tz and nano sec, thus no need to use apply_wraps
876
+ if isinstance (other, ABCTimestamp):
877
+
878
+ # GH#15126
879
+ # in order to avoid a recursive
880
+ # call of __add__ and __radd__ if there is
881
+ # an exception, when we call using the + operator,
882
+ # we directly call the known method
883
+ result = other.__add__ (self )
884
+ if result is NotImplemented :
885
+ raise OverflowError
886
+ return result
887
+ elif other is NaT:
888
+ return NaT
889
+ elif is_datetime64_object(other) or PyDate_Check(other):
890
+ # PyDate_Check includes date, datetime
891
+ from pandas import Timestamp
892
+ return Timestamp(other) + self
893
+
894
+ if PyDelta_Check(other):
895
+ return other + self .delta
896
+ elif isinstance (other, type (self )):
897
+ # TODO: this is reached in tests that specifically call apply,
898
+ # but should not be reached "naturally" because __add__ should
899
+ # catch this case first.
900
+ return type (self )(self .n + other.n)
901
+
902
+ raise ApplyTypeError(f" Unhandled type: {type(other).__name__}" )
903
+
847
904
# --------------------------------------------------------------------
848
905
# Pickle Methods
849
906
@@ -855,6 +912,67 @@ cdef class _Tick(_BaseOffset):
855
912
self .normalize = False
856
913
857
914
915
+ cdef class Day(Tick):
916
+ _nanos_inc = 24 * 3600 * 1 _000_000_000
917
+ _prefix = " D"
918
+
919
+
920
+ cdef class Hour(Tick):
921
+ _nanos_inc = 3600 * 1 _000_000_000
922
+ _prefix = " H"
923
+
924
+
925
+ cdef class Minute(Tick):
926
+ _nanos_inc = 60 * 1 _000_000_000
927
+ _prefix = " T"
928
+
929
+
930
+ cdef class Second(Tick):
931
+ _nanos_inc = 1 _000_000_000
932
+ _prefix = " S"
933
+
934
+
935
+ cdef class Milli(Tick):
936
+ _nanos_inc = 1 _000_000
937
+ _prefix = " L"
938
+
939
+
940
+ cdef class Micro(Tick):
941
+ _nanos_inc = 1000
942
+ _prefix = " U"
943
+
944
+
945
+ cdef class Nano(Tick):
946
+ _nanos_inc = 1
947
+ _prefix = " N"
948
+
949
+
950
+ def delta_to_tick (delta: timedelta ) -> Tick:
951
+ if delta.microseconds == 0 and getattr(delta , "nanoseconds", 0) == 0:
952
+ # nanoseconds only for pd.Timedelta
953
+ if delta.seconds == 0:
954
+ return Day(delta.days )
955
+ else:
956
+ seconds = delta.days * 86400 + delta.seconds
957
+ if seconds % 3600 == 0:
958
+ return Hour(seconds / 3600)
959
+ elif seconds % 60 == 0:
960
+ return Minute(seconds / 60)
961
+ else:
962
+ return Second(seconds )
963
+ else:
964
+ nanos = delta_to_nanoseconds(delta)
965
+ if nanos % 1_000_000 == 0:
966
+ return Milli(nanos // 1_000_000)
967
+ elif nanos % 1000 == 0:
968
+ return Micro(nanos // 1000)
969
+ else: # pragma: no cover
970
+ return Nano(nanos )
971
+
972
+
973
+ # --------------------------------------------------------------------
974
+
975
+
858
976
class BusinessMixin(BaseOffset ):
859
977
"""
860
978
Mixin to business types to provide related functions.
0 commit comments