Skip to content

Commit 17cb27e

Browse files
jbrockmendelyehoshuadimarsky
authored andcommitted
ENH: implement pandas_timedelta_to_timedeltastruct for other resos (pandas-dev#46465)
* ENH: implement pandas_timedelta_to_timedeltastruct for other resos * idiomatic naming
1 parent 2485520 commit 17cb27e

File tree

4 files changed

+304
-9
lines changed

4 files changed

+304
-9
lines changed

pandas/_libs/tslibs/np_datetime.pyi

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ class OutOfBoundsDatetime(ValueError): ...
44

55
# only exposed for testing
66
def py_get_unit_from_dtype(dtype: np.dtype): ...
7+
def py_td64_to_tdstruct(td64: int, unit: int) -> dict: ...
78
def astype_overflowsafe(
89
arr: np.ndarray, dtype: np.dtype, copy: bool = ...
910
) -> np.ndarray: ...

pandas/_libs/tslibs/np_datetime.pyx

+8
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,14 @@ cdef inline void td64_to_tdstruct(int64_t td64,
189189
return
190190

191191

192+
# just exposed for testing at the moment
193+
def py_td64_to_tdstruct(int64_t td64, NPY_DATETIMEUNIT unit):
194+
cdef:
195+
pandas_timedeltastruct tds
196+
pandas_timedelta_to_timedeltastruct(td64, unit, &tds)
197+
return tds # <- returned as a dict to python
198+
199+
192200
cdef inline int64_t pydatetime_to_dt64(datetime val,
193201
npy_datetimestruct *dts):
194202
"""

pandas/_libs/tslibs/src/datetime/np_datetime.c

+229-9
Original file line numberDiff line numberDiff line change
@@ -712,19 +712,23 @@ void pandas_timedelta_to_timedeltastruct(npy_timedelta td,
712712
npy_int64 sfrac;
713713
npy_int64 ifrac;
714714
int sign;
715-
npy_int64 DAY_NS = 86400000000000LL;
715+
npy_int64 per_day;
716+
npy_int64 per_sec;
716717

717718
/* Initialize the output to all zeros */
718719
memset(out, 0, sizeof(pandas_timedeltastruct));
719720

720721
switch (base) {
721722
case NPY_FR_ns:
722723

724+
per_day = 86400000000000LL;
725+
per_sec = 1000LL * 1000LL * 1000LL;
726+
723727
// put frac in seconds
724-
if (td < 0 && td % (1000LL * 1000LL * 1000LL) != 0)
725-
frac = td / (1000LL * 1000LL * 1000LL) - 1;
728+
if (td < 0 && td % per_sec != 0)
729+
frac = td / per_sec - 1;
726730
else
727-
frac = td / (1000LL * 1000LL * 1000LL);
731+
frac = td / per_sec;
728732

729733
if (frac < 0) {
730734
sign = -1;
@@ -768,12 +772,12 @@ void pandas_timedelta_to_timedeltastruct(npy_timedelta td,
768772
}
769773

770774
sfrac = (out->hrs * 3600LL + out->min * 60LL
771-
+ out->sec) * (1000LL * 1000LL * 1000LL);
775+
+ out->sec) * per_sec;
772776

773777
if (sign < 0)
774778
out->days = -out->days;
775779

776-
ifrac = td - (out->days * DAY_NS + sfrac);
780+
ifrac = td - (out->days * per_day + sfrac);
777781

778782
if (ifrac != 0) {
779783
out->ms = ifrac / (1000LL * 1000LL);
@@ -786,17 +790,233 @@ void pandas_timedelta_to_timedeltastruct(npy_timedelta td,
786790
out->us = 0;
787791
out->ns = 0;
788792
}
793+
break;
794+
795+
case NPY_FR_us:
796+
797+
per_day = 86400000000LL;
798+
per_sec = 1000LL * 1000LL;
799+
800+
// put frac in seconds
801+
if (td < 0 && td % per_sec != 0)
802+
frac = td / per_sec - 1;
803+
else
804+
frac = td / per_sec;
805+
806+
if (frac < 0) {
807+
sign = -1;
808+
809+
// even fraction
810+
if ((-frac % 86400LL) != 0) {
811+
out->days = -frac / 86400LL + 1;
812+
frac += 86400LL * out->days;
813+
} else {
814+
frac = -frac;
815+
}
816+
} else {
817+
sign = 1;
818+
out->days = 0;
819+
}
820+
821+
if (frac >= 86400) {
822+
out->days += frac / 86400LL;
823+
frac -= out->days * 86400LL;
824+
}
825+
826+
if (frac >= 3600) {
827+
out->hrs = frac / 3600LL;
828+
frac -= out->hrs * 3600LL;
829+
} else {
830+
out->hrs = 0;
831+
}
832+
833+
if (frac >= 60) {
834+
out->min = frac / 60LL;
835+
frac -= out->min * 60LL;
836+
} else {
837+
out->min = 0;
838+
}
839+
840+
if (frac >= 0) {
841+
out->sec = frac;
842+
frac -= out->sec;
843+
} else {
844+
out->sec = 0;
845+
}
846+
847+
sfrac = (out->hrs * 3600LL + out->min * 60LL
848+
+ out->sec) * per_sec;
849+
850+
if (sign < 0)
851+
out->days = -out->days;
852+
853+
ifrac = td - (out->days * per_day + sfrac);
854+
855+
if (ifrac != 0) {
856+
out->ms = ifrac / 1000LL;
857+
ifrac -= out->ms * 1000LL;
858+
out->us = ifrac / 1L;
859+
ifrac -= out->us * 1L;
860+
out->ns = ifrac;
861+
} else {
862+
out->ms = 0;
863+
out->us = 0;
864+
out->ns = 0;
865+
}
866+
break;
867+
868+
case NPY_FR_ms:
869+
870+
per_day = 86400000LL;
871+
per_sec = 1000LL;
872+
873+
// put frac in seconds
874+
if (td < 0 && td % per_sec != 0)
875+
frac = td / per_sec - 1;
876+
else
877+
frac = td / per_sec;
878+
879+
if (frac < 0) {
880+
sign = -1;
881+
882+
// even fraction
883+
if ((-frac % 86400LL) != 0) {
884+
out->days = -frac / 86400LL + 1;
885+
frac += 86400LL * out->days;
886+
} else {
887+
frac = -frac;
888+
}
889+
} else {
890+
sign = 1;
891+
out->days = 0;
892+
}
893+
894+
if (frac >= 86400) {
895+
out->days += frac / 86400LL;
896+
frac -= out->days * 86400LL;
897+
}
898+
899+
if (frac >= 3600) {
900+
out->hrs = frac / 3600LL;
901+
frac -= out->hrs * 3600LL;
902+
} else {
903+
out->hrs = 0;
904+
}
905+
906+
if (frac >= 60) {
907+
out->min = frac / 60LL;
908+
frac -= out->min * 60LL;
909+
} else {
910+
out->min = 0;
911+
}
912+
913+
if (frac >= 0) {
914+
out->sec = frac;
915+
frac -= out->sec;
916+
} else {
917+
out->sec = 0;
918+
}
919+
920+
sfrac = (out->hrs * 3600LL + out->min * 60LL
921+
+ out->sec) * per_sec;
922+
923+
if (sign < 0)
924+
out->days = -out->days;
925+
926+
ifrac = td - (out->days * per_day + sfrac);
927+
928+
if (ifrac != 0) {
929+
out->ms = ifrac;
930+
out->us = 0;
931+
out->ns = 0;
932+
} else {
933+
out->ms = 0;
934+
out->us = 0;
935+
out->ns = 0;
936+
}
937+
break;
938+
939+
case NPY_FR_s:
940+
// special case where we can simplify many expressions bc per_sec=1
941+
942+
per_day = 86400000LL;
943+
per_sec = 1L;
944+
945+
// put frac in seconds
946+
if (td < 0 && td % per_sec != 0)
947+
frac = td / per_sec - 1;
948+
else
949+
frac = td / per_sec;
950+
951+
if (frac < 0) {
952+
sign = -1;
789953

790-
out->seconds = out->hrs * 3600 + out->min * 60 + out->sec;
791-
out->microseconds = out->ms * 1000 + out->us;
792-
out->nanoseconds = out->ns;
954+
// even fraction
955+
if ((-frac % 86400LL) != 0) {
956+
out->days = -frac / 86400LL + 1;
957+
frac += 86400LL * out->days;
958+
} else {
959+
frac = -frac;
960+
}
961+
} else {
962+
sign = 1;
963+
out->days = 0;
964+
}
965+
966+
if (frac >= 86400) {
967+
out->days += frac / 86400LL;
968+
frac -= out->days * 86400LL;
969+
}
970+
971+
if (frac >= 3600) {
972+
out->hrs = frac / 3600LL;
973+
frac -= out->hrs * 3600LL;
974+
} else {
975+
out->hrs = 0;
976+
}
977+
978+
if (frac >= 60) {
979+
out->min = frac / 60LL;
980+
frac -= out->min * 60LL;
981+
} else {
982+
out->min = 0;
983+
}
984+
985+
if (frac >= 0) {
986+
out->sec = frac;
987+
frac -= out->sec;
988+
} else {
989+
out->sec = 0;
990+
}
991+
992+
sfrac = (out->hrs * 3600LL + out->min * 60LL
993+
+ out->sec) * per_sec;
994+
995+
if (sign < 0)
996+
out->days = -out->days;
997+
998+
ifrac = td - (out->days * per_day + sfrac);
999+
1000+
if (ifrac != 0) {
1001+
out->ms = 0;
1002+
out->us = 0;
1003+
out->ns = 0;
1004+
} else {
1005+
out->ms = 0;
1006+
out->us = 0;
1007+
out->ns = 0;
1008+
}
7931009
break;
7941010

7951011
default:
7961012
PyErr_SetString(PyExc_RuntimeError,
7971013
"NumPy timedelta metadata is corrupted with "
7981014
"invalid base unit");
7991015
}
1016+
1017+
out->seconds = out->hrs * 3600 + out->min * 60 + out->sec;
1018+
out->microseconds = out->ms * 1000 + out->us;
1019+
out->nanoseconds = out->ns;
8001020
}
8011021

8021022

pandas/tests/tslibs/test_np_datetime.py

+66
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
OutOfBoundsDatetime,
66
astype_overflowsafe,
77
py_get_unit_from_dtype,
8+
py_td64_to_tdstruct,
89
)
910

1011
import pandas._testing as tm
@@ -44,6 +45,71 @@ def test_get_unit_from_dtype():
4445
assert py_get_unit_from_dtype(np.dtype("m8[as]")) == 13
4546

4647

48+
def test_td64_to_tdstruct():
49+
val = 12454636234 # arbitrary value
50+
51+
res1 = py_td64_to_tdstruct(val, 10) # ns
52+
exp1 = {
53+
"days": 0,
54+
"hrs": 0,
55+
"min": 0,
56+
"sec": 12,
57+
"ms": 454,
58+
"us": 636,
59+
"ns": 234,
60+
"seconds": 12,
61+
"microseconds": 454636,
62+
"nanoseconds": 234,
63+
}
64+
assert res1 == exp1
65+
66+
res2 = py_td64_to_tdstruct(val, 9) # us
67+
exp2 = {
68+
"days": 0,
69+
"hrs": 3,
70+
"min": 27,
71+
"sec": 34,
72+
"ms": 636,
73+
"us": 234,
74+
"ns": 0,
75+
"seconds": 12454,
76+
"microseconds": 636234,
77+
"nanoseconds": 0,
78+
}
79+
assert res2 == exp2
80+
81+
res3 = py_td64_to_tdstruct(val, 8) # ms
82+
exp3 = {
83+
"days": 144,
84+
"hrs": 3,
85+
"min": 37,
86+
"sec": 16,
87+
"ms": 234,
88+
"us": 0,
89+
"ns": 0,
90+
"seconds": 13036,
91+
"microseconds": 234000,
92+
"nanoseconds": 0,
93+
}
94+
assert res3 == exp3
95+
96+
# Note this out of bounds for nanosecond Timedelta
97+
res4 = py_td64_to_tdstruct(val, 7) # s
98+
exp4 = {
99+
"days": 144150,
100+
"hrs": 21,
101+
"min": 10,
102+
"sec": 34,
103+
"ms": 0,
104+
"us": 0,
105+
"ns": 0,
106+
"seconds": 76234,
107+
"microseconds": 0,
108+
"nanoseconds": 0,
109+
}
110+
assert res4 == exp4
111+
112+
47113
class TestAstypeOverflowSafe:
48114
def test_pass_non_dt64_array(self):
49115
# check that we raise, not segfault

0 commit comments

Comments
 (0)