@@ -45,13 +45,19 @@ from pandas._libs.tslibs.nattype cimport (
45
45
)
46
46
from pandas._libs.tslibs.np_datetime cimport (
47
47
NPY_DATETIMEUNIT,
48
+ NPY_FR_ns,
49
+ cmp_dtstructs,
48
50
cmp_scalar,
49
51
get_datetime64_unit,
50
52
get_timedelta64_value,
53
+ npy_datetimestruct,
54
+ pandas_datetime_to_datetimestruct,
55
+ pandas_timedelta_to_timedeltastruct,
51
56
pandas_timedeltastruct,
52
- td64_to_tdstruct,
53
57
)
58
+
54
59
from pandas._libs.tslibs.np_datetime import OutOfBoundsTimedelta
60
+
55
61
from pandas._libs.tslibs.offsets cimport is_tick_object
56
62
from pandas._libs.tslibs.util cimport (
57
63
is_array,
@@ -176,7 +182,9 @@ cpdef int64_t delta_to_nanoseconds(delta) except? -1:
176
182
if is_tick_object(delta):
177
183
return delta.nanos
178
184
if isinstance (delta, _Timedelta):
179
- return delta.value
185
+ if delta._reso == NPY_FR_ns:
186
+ return delta.value
187
+ raise NotImplementedError (delta._reso)
180
188
181
189
if is_timedelta64_object(delta):
182
190
return get_timedelta64_value(ensure_td64ns(delta))
@@ -251,6 +259,8 @@ cdef convert_to_timedelta64(object ts, str unit):
251
259
return np.timedelta64(NPY_NAT, " ns" )
252
260
elif isinstance (ts, _Timedelta):
253
261
# already in the proper format
262
+ if ts._reso != NPY_FR_ns:
263
+ raise NotImplementedError
254
264
ts = np.timedelta64(ts.value, " ns" )
255
265
elif is_timedelta64_object(ts):
256
266
ts = ensure_td64ns(ts)
@@ -643,7 +653,8 @@ cdef bint _validate_ops_compat(other):
643
653
644
654
def _op_unary_method (func , name ):
645
655
def f (self ):
646
- return Timedelta(func(self .value), unit = ' ns' )
656
+ new_value = func(self .value)
657
+ return _timedelta_from_value_and_reso(new_value, self ._reso)
647
658
f.__name__ = name
648
659
return f
649
660
@@ -688,7 +699,17 @@ def _binary_op_method_timedeltalike(op, name):
688
699
if other is NaT:
689
700
# e.g. if original other was timedelta64('NaT')
690
701
return NaT
691
- return Timedelta(op(self .value, other.value), unit = ' ns' )
702
+
703
+ if self ._reso != other._reso:
704
+ raise NotImplementedError
705
+
706
+ res = op(self .value, other.value)
707
+ if res == NPY_NAT:
708
+ # e.g. test_implementation_limits
709
+ # TODO: more generally could do an overflowcheck in op?
710
+ return NaT
711
+
712
+ return _timedelta_from_value_and_reso(res, reso = self ._reso)
692
713
693
714
f.__name__ = name
694
715
return f
@@ -818,6 +839,38 @@ cdef _to_py_int_float(v):
818
839
raise TypeError (f" Invalid type {type(v)}. Must be int or float." )
819
840
820
841
842
+ def _timedelta_unpickle (value , reso ):
843
+ return _timedelta_from_value_and_reso(value, reso)
844
+
845
+
846
+ cdef _timedelta_from_value_and_reso(int64_t value, NPY_DATETIMEUNIT reso):
847
+ # Could make this a classmethod if/when cython supports cdef classmethods
848
+ cdef:
849
+ _Timedelta td_base
850
+
851
+ if reso == NPY_FR_ns:
852
+ td_base = _Timedelta.__new__ (Timedelta, microseconds = int (value) // 1000 )
853
+ elif reso == NPY_DATETIMEUNIT.NPY_FR_us:
854
+ td_base = _Timedelta.__new__ (Timedelta, microseconds = int (value))
855
+ elif reso == NPY_DATETIMEUNIT.NPY_FR_ms:
856
+ td_base = _Timedelta.__new__ (Timedelta, milliseconds = int (value))
857
+ elif reso == NPY_DATETIMEUNIT.NPY_FR_s:
858
+ td_base = _Timedelta.__new__ (Timedelta, seconds = int (value))
859
+ elif reso == NPY_DATETIMEUNIT.NPY_FR_m:
860
+ td_base = _Timedelta.__new__ (Timedelta, minutes = int (value))
861
+ elif reso == NPY_DATETIMEUNIT.NPY_FR_h:
862
+ td_base = _Timedelta.__new__ (Timedelta, hours = int (value))
863
+ elif reso == NPY_DATETIMEUNIT.NPY_FR_D:
864
+ td_base = _Timedelta.__new__ (Timedelta, days = int (value))
865
+ else :
866
+ raise NotImplementedError (reso)
867
+
868
+ td_base.value = value
869
+ td_base._is_populated = 0
870
+ td_base._reso = reso
871
+ return td_base
872
+
873
+
821
874
# Similar to Timestamp/datetime, this is a construction requirement for
822
875
# timedeltas that we need to do object instantiation in python. This will
823
876
# serve as a C extension type that shadows the Python class, where we do any
@@ -827,6 +880,7 @@ cdef class _Timedelta(timedelta):
827
880
# int64_t value # nanoseconds
828
881
# bint _is_populated # are my components populated
829
882
# int64_t _d, _h, _m, _s, _ms, _us, _ns
883
+ # NPY_DATETIMEUNIT _reso
830
884
831
885
# higher than np.ndarray and np.matrix
832
886
__array_priority__ = 100
@@ -853,6 +907,11 @@ cdef class _Timedelta(timedelta):
853
907
854
908
def __hash__(_Timedelta self ):
855
909
if self ._has_ns():
910
+ # Note: this does *not* satisfy the invariance
911
+ # td1 == td2 \\Rightarrow hash(td1) == hash(td2)
912
+ # if td1 and td2 have different _resos. timedelta64 also has this
913
+ # non-invariant behavior.
914
+ # see GH#44504
856
915
return hash (self .value)
857
916
else :
858
917
return timedelta.__hash__ (self )
@@ -890,10 +949,30 @@ cdef class _Timedelta(timedelta):
890
949
else :
891
950
return NotImplemented
892
951
893
- return cmp_scalar(self .value, ots.value, op)
952
+ if self ._reso == ots._reso:
953
+ return cmp_scalar(self .value, ots.value, op)
954
+ return self ._compare_mismatched_resos(ots, op)
955
+
956
+ # TODO: re-use/share with Timestamp
957
+ cdef inline bint _compare_mismatched_resos(self , _Timedelta other, op):
958
+ # Can't just dispatch to numpy as they silently overflow and get it wrong
959
+ cdef:
960
+ npy_datetimestruct dts_self
961
+ npy_datetimestruct dts_other
962
+
963
+ # dispatch to the datetimestruct utils instead of writing new ones!
964
+ pandas_datetime_to_datetimestruct(self .value, self ._reso, & dts_self)
965
+ pandas_datetime_to_datetimestruct(other.value, other._reso, & dts_other)
966
+ return cmp_dtstructs(& dts_self, & dts_other, op)
894
967
895
968
cdef bint _has_ns(self ):
896
- return self .value % 1000 != 0
969
+ if self ._reso == NPY_FR_ns:
970
+ return self .value % 1000 != 0
971
+ elif self ._reso < NPY_FR_ns:
972
+ # i.e. seconds, millisecond, microsecond
973
+ return False
974
+ else :
975
+ raise NotImplementedError (self ._reso)
897
976
898
977
cdef _ensure_components(_Timedelta self ):
899
978
"""
@@ -905,7 +984,7 @@ cdef class _Timedelta(timedelta):
905
984
cdef:
906
985
pandas_timedeltastruct tds
907
986
908
- td64_to_tdstruct (self .value, & tds)
987
+ pandas_timedelta_to_timedeltastruct (self .value, self ._reso , & tds)
909
988
self ._d = tds.days
910
989
self ._h = tds.hrs
911
990
self ._m = tds.min
@@ -937,13 +1016,24 @@ cdef class _Timedelta(timedelta):
937
1016
-----
938
1017
Any nanosecond resolution will be lost.
939
1018
"""
940
- return timedelta(microseconds = int (self .value) / 1000 )
1019
+ if self ._reso == NPY_FR_ns:
1020
+ return timedelta(microseconds = int (self .value) / 1000 )
1021
+
1022
+ # TODO(@WillAyd): is this the right way to use components?
1023
+ self ._ensure_components()
1024
+ return timedelta(
1025
+ days = self ._d, seconds = self ._seconds, microseconds = self ._microseconds
1026
+ )
941
1027
942
1028
def to_timedelta64 (self ) -> np.timedelta64:
943
1029
"""
944
1030
Return a numpy.timedelta64 object with 'ns' precision.
945
1031
"""
946
- return np.timedelta64(self.value , 'ns')
1032
+ cdef:
1033
+ str abbrev = npy_unit_to_abbrev(self ._reso)
1034
+ # TODO: way to create a np.timedelta64 obj with the reso directly
1035
+ # instead of having to get the abbrev?
1036
+ return np.timedelta64(self.value , abbrev )
947
1037
948
1038
def to_numpy(self , dtype = None , copy = False ) -> np.timedelta64:
949
1039
"""
@@ -1054,7 +1144,7 @@ cdef class _Timedelta(timedelta):
1054
1144
>>> td.asm8
1055
1145
numpy.timedelta64(42,'ns')
1056
1146
"""
1057
- return np.int64( self.value ).view('m8[ns]' )
1147
+ return self.to_timedelta64( )
1058
1148
1059
1149
@property
1060
1150
def resolution_string(self ) -> str:
@@ -1258,6 +1348,14 @@ cdef class _Timedelta(timedelta):
1258
1348
f' H{components.minutes}M{seconds}S' )
1259
1349
return tpl
1260
1350
1351
+ # ----------------------------------------------------------------
1352
+ # Constructors
1353
+
1354
+ @classmethod
1355
+ def _from_value_and_reso(cls , int64_t value , NPY_DATETIMEUNIT reso ):
1356
+ # exposing as classmethod for testing
1357
+ return _timedelta_from_value_and_reso(value, reso)
1358
+
1261
1359
1262
1360
# Python front end to C extension type _Timedelta
1263
1361
# This serves as the box for timedelta64
@@ -1413,19 +1511,21 @@ class Timedelta(_Timedelta):
1413
1511
if value == NPY_NAT:
1414
1512
return NaT
1415
1513
1416
- # make timedelta happy
1417
- td_base = _Timedelta.__new__ (cls , microseconds = int (value) // 1000 )
1418
- td_base.value = value
1419
- td_base._is_populated = 0
1420
- return td_base
1514
+ return _timedelta_from_value_and_reso(value, NPY_FR_ns)
1421
1515
1422
1516
def __setstate__ (self , state ):
1423
- (value) = state
1517
+ if len (state) == 1 :
1518
+ # older pickle, only supported nanosecond
1519
+ value = state[0 ]
1520
+ reso = NPY_FR_ns
1521
+ else :
1522
+ value, reso = state
1424
1523
self .value = value
1524
+ self ._reso = reso
1425
1525
1426
1526
def __reduce__ (self ):
1427
- object_state = self .value,
1428
- return (Timedelta , object_state)
1527
+ object_state = self .value, self ._reso
1528
+ return (_timedelta_unpickle , object_state)
1429
1529
1430
1530
@ cython.cdivision (True )
1431
1531
def _round (self , freq , mode ):
@@ -1496,7 +1596,14 @@ class Timedelta(_Timedelta):
1496
1596
1497
1597
def __mul__ (self , other ):
1498
1598
if is_integer_object(other) or is_float_object(other):
1499
- return Timedelta(other * self .value, unit = ' ns' )
1599
+ if util.is_nan(other):
1600
+ # np.nan * timedelta -> np.timedelta64("NaT"), in this case NaT
1601
+ return NaT
1602
+
1603
+ return _timedelta_from_value_and_reso(
1604
+ < int64_t> (other * self .value),
1605
+ reso = self ._reso,
1606
+ )
1500
1607
1501
1608
elif is_array(other):
1502
1609
# ndarray-like
0 commit comments