np_datetime.pyx 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629
  1. cimport cython
  2. from cpython.datetime cimport (
  3. PyDateTime_CheckExact,
  4. PyDateTime_DATE_GET_HOUR,
  5. PyDateTime_DATE_GET_MICROSECOND,
  6. PyDateTime_DATE_GET_MINUTE,
  7. PyDateTime_DATE_GET_SECOND,
  8. PyDateTime_GET_DAY,
  9. PyDateTime_GET_MONTH,
  10. PyDateTime_GET_YEAR,
  11. import_datetime,
  12. )
  13. from cpython.object cimport (
  14. Py_EQ,
  15. Py_GE,
  16. Py_GT,
  17. Py_LE,
  18. Py_LT,
  19. Py_NE,
  20. )
  21. import_datetime()
  22. import numpy as np
  23. cimport numpy as cnp
  24. cnp.import_array()
  25. from numpy cimport (
  26. int64_t,
  27. ndarray,
  28. uint8_t,
  29. )
  30. from pandas._libs.tslibs.util cimport get_c_string_buf_and_size
  31. cdef extern from "src/datetime/np_datetime.h":
  32. int cmp_npy_datetimestruct(npy_datetimestruct *a,
  33. npy_datetimestruct *b)
  34. # AS, FS, PS versions exist but are not imported because they are not used.
  35. npy_datetimestruct _NS_MIN_DTS, _NS_MAX_DTS
  36. npy_datetimestruct _US_MIN_DTS, _US_MAX_DTS
  37. npy_datetimestruct _MS_MIN_DTS, _MS_MAX_DTS
  38. npy_datetimestruct _S_MIN_DTS, _S_MAX_DTS
  39. npy_datetimestruct _M_MIN_DTS, _M_MAX_DTS
  40. PyArray_DatetimeMetaData get_datetime_metadata_from_dtype(cnp.PyArray_Descr *dtype)
  41. cdef extern from "src/datetime/np_datetime_strings.h":
  42. int parse_iso_8601_datetime(const char *str, int len, int want_exc,
  43. npy_datetimestruct *out,
  44. NPY_DATETIMEUNIT *out_bestunit,
  45. int *out_local, int *out_tzoffset,
  46. const char *format, int format_len,
  47. FormatRequirement exact)
  48. # ----------------------------------------------------------------------
  49. # numpy object inspection
  50. cdef npy_datetime get_datetime64_value(object obj) nogil:
  51. """
  52. returns the int64 value underlying scalar numpy datetime64 object
  53. Note that to interpret this as a datetime, the corresponding unit is
  54. also needed. That can be found using `get_datetime64_unit`.
  55. """
  56. return (<PyDatetimeScalarObject*>obj).obval
  57. cdef npy_timedelta get_timedelta64_value(object obj) nogil:
  58. """
  59. returns the int64 value underlying scalar numpy timedelta64 object
  60. """
  61. return (<PyTimedeltaScalarObject*>obj).obval
  62. cdef NPY_DATETIMEUNIT get_datetime64_unit(object obj) nogil:
  63. """
  64. returns the unit part of the dtype for a numpy datetime64 object.
  65. """
  66. return <NPY_DATETIMEUNIT>(<PyDatetimeScalarObject*>obj).obmeta.base
  67. cdef NPY_DATETIMEUNIT get_unit_from_dtype(cnp.dtype dtype):
  68. # NB: caller is responsible for ensuring this is *some* datetime64 or
  69. # timedelta64 dtype, otherwise we can segfault
  70. cdef:
  71. cnp.PyArray_Descr* descr = <cnp.PyArray_Descr*>dtype
  72. PyArray_DatetimeMetaData meta
  73. meta = get_datetime_metadata_from_dtype(descr)
  74. return meta.base
  75. def py_get_unit_from_dtype(dtype):
  76. # for testing get_unit_from_dtype; adds 896 bytes to the .so file.
  77. return get_unit_from_dtype(dtype)
  78. def is_unitless(dtype: cnp.dtype) -> bool:
  79. """
  80. Check if a datetime64 or timedelta64 dtype has no attached unit.
  81. """
  82. if dtype.type_num not in [cnp.NPY_DATETIME, cnp.NPY_TIMEDELTA]:
  83. raise ValueError("is_unitless dtype must be datetime64 or timedelta64")
  84. cdef:
  85. NPY_DATETIMEUNIT unit = get_unit_from_dtype(dtype)
  86. return unit == NPY_DATETIMEUNIT.NPY_FR_GENERIC
  87. # ----------------------------------------------------------------------
  88. # Comparison
  89. cdef bint cmp_dtstructs(
  90. npy_datetimestruct* left, npy_datetimestruct* right, int op
  91. ):
  92. cdef:
  93. int cmp_res
  94. cmp_res = cmp_npy_datetimestruct(left, right)
  95. if op == Py_EQ:
  96. return cmp_res == 0
  97. if op == Py_NE:
  98. return cmp_res != 0
  99. if op == Py_GT:
  100. return cmp_res == 1
  101. if op == Py_LT:
  102. return cmp_res == -1
  103. if op == Py_GE:
  104. return cmp_res == 1 or cmp_res == 0
  105. else:
  106. # i.e. op == Py_LE
  107. return cmp_res == -1 or cmp_res == 0
  108. cdef bint cmp_scalar(int64_t lhs, int64_t rhs, int op) except -1:
  109. """
  110. cmp_scalar is a more performant version of PyObject_RichCompare
  111. typed for int64_t arguments.
  112. """
  113. if op == Py_EQ:
  114. return lhs == rhs
  115. elif op == Py_NE:
  116. return lhs != rhs
  117. elif op == Py_LT:
  118. return lhs < rhs
  119. elif op == Py_LE:
  120. return lhs <= rhs
  121. elif op == Py_GT:
  122. return lhs > rhs
  123. elif op == Py_GE:
  124. return lhs >= rhs
  125. class OutOfBoundsDatetime(ValueError):
  126. """
  127. Raised when the datetime is outside the range that can be represented.
  128. """
  129. pass
  130. class OutOfBoundsTimedelta(ValueError):
  131. """
  132. Raised when encountering a timedelta value that cannot be represented.
  133. Representation should be within a timedelta64[ns].
  134. """
  135. # Timedelta analogue to OutOfBoundsDatetime
  136. pass
  137. cdef get_implementation_bounds(
  138. NPY_DATETIMEUNIT reso,
  139. npy_datetimestruct *lower,
  140. npy_datetimestruct *upper,
  141. ):
  142. if reso == NPY_FR_ns:
  143. upper[0] = _NS_MAX_DTS
  144. lower[0] = _NS_MIN_DTS
  145. elif reso == NPY_FR_us:
  146. upper[0] = _US_MAX_DTS
  147. lower[0] = _US_MIN_DTS
  148. elif reso == NPY_FR_ms:
  149. upper[0] = _MS_MAX_DTS
  150. lower[0] = _MS_MIN_DTS
  151. elif reso == NPY_FR_s:
  152. upper[0] = _S_MAX_DTS
  153. lower[0] = _S_MIN_DTS
  154. elif reso == NPY_FR_m:
  155. upper[0] = _M_MAX_DTS
  156. lower[0] = _M_MIN_DTS
  157. else:
  158. raise NotImplementedError(reso)
  159. cdef check_dts_bounds(npy_datetimestruct *dts, NPY_DATETIMEUNIT unit=NPY_FR_ns):
  160. """Raises OutOfBoundsDatetime if the given date is outside the range that
  161. can be represented by nanosecond-resolution 64-bit integers."""
  162. cdef:
  163. bint error = False
  164. npy_datetimestruct cmp_upper, cmp_lower
  165. get_implementation_bounds(unit, &cmp_lower, &cmp_upper)
  166. if cmp_npy_datetimestruct(dts, &cmp_lower) == -1:
  167. error = True
  168. elif cmp_npy_datetimestruct(dts, &cmp_upper) == 1:
  169. error = True
  170. if error:
  171. fmt = (f"{dts.year}-{dts.month:02d}-{dts.day:02d} "
  172. f"{dts.hour:02d}:{dts.min:02d}:{dts.sec:02d}")
  173. # TODO: "nanosecond" in the message assumes NPY_FR_ns
  174. raise OutOfBoundsDatetime(f"Out of bounds nanosecond timestamp: {fmt}")
  175. # ----------------------------------------------------------------------
  176. # Conversion
  177. # just exposed for testing at the moment
  178. def py_td64_to_tdstruct(int64_t td64, NPY_DATETIMEUNIT unit):
  179. cdef:
  180. pandas_timedeltastruct tds
  181. pandas_timedelta_to_timedeltastruct(td64, unit, &tds)
  182. return tds # <- returned as a dict to python
  183. cdef void pydatetime_to_dtstruct(datetime dt, npy_datetimestruct *dts):
  184. if PyDateTime_CheckExact(dt):
  185. dts.year = PyDateTime_GET_YEAR(dt)
  186. else:
  187. # We use dt.year instead of PyDateTime_GET_YEAR because with Timestamp
  188. # we override year such that PyDateTime_GET_YEAR is incorrect.
  189. dts.year = dt.year
  190. dts.month = PyDateTime_GET_MONTH(dt)
  191. dts.day = PyDateTime_GET_DAY(dt)
  192. dts.hour = PyDateTime_DATE_GET_HOUR(dt)
  193. dts.min = PyDateTime_DATE_GET_MINUTE(dt)
  194. dts.sec = PyDateTime_DATE_GET_SECOND(dt)
  195. dts.us = PyDateTime_DATE_GET_MICROSECOND(dt)
  196. dts.ps = dts.as = 0
  197. cdef int64_t pydatetime_to_dt64(datetime val,
  198. npy_datetimestruct *dts,
  199. NPY_DATETIMEUNIT reso=NPY_FR_ns):
  200. """
  201. Note we are assuming that the datetime object is timezone-naive.
  202. """
  203. pydatetime_to_dtstruct(val, dts)
  204. return npy_datetimestruct_to_datetime(reso, dts)
  205. cdef void pydate_to_dtstruct(date val, npy_datetimestruct *dts):
  206. dts.year = PyDateTime_GET_YEAR(val)
  207. dts.month = PyDateTime_GET_MONTH(val)
  208. dts.day = PyDateTime_GET_DAY(val)
  209. dts.hour = dts.min = dts.sec = dts.us = 0
  210. dts.ps = dts.as = 0
  211. return
  212. cdef int64_t pydate_to_dt64(
  213. date val, npy_datetimestruct *dts, NPY_DATETIMEUNIT reso=NPY_FR_ns
  214. ):
  215. pydate_to_dtstruct(val, dts)
  216. return npy_datetimestruct_to_datetime(reso, dts)
  217. cdef int string_to_dts(
  218. str val,
  219. npy_datetimestruct* dts,
  220. NPY_DATETIMEUNIT* out_bestunit,
  221. int* out_local,
  222. int* out_tzoffset,
  223. bint want_exc,
  224. format: str | None=None,
  225. bint exact=True,
  226. ) except? -1:
  227. cdef:
  228. Py_ssize_t length
  229. const char* buf
  230. Py_ssize_t format_length
  231. const char* format_buf
  232. FormatRequirement format_requirement
  233. buf = get_c_string_buf_and_size(val, &length)
  234. if format is None:
  235. format_buf = b""
  236. format_length = 0
  237. format_requirement = INFER_FORMAT
  238. else:
  239. format_buf = get_c_string_buf_and_size(format, &format_length)
  240. format_requirement = <FormatRequirement>exact
  241. return parse_iso_8601_datetime(buf, length, want_exc,
  242. dts, out_bestunit, out_local, out_tzoffset,
  243. format_buf, format_length,
  244. format_requirement)
  245. cpdef ndarray astype_overflowsafe(
  246. ndarray values,
  247. cnp.dtype dtype,
  248. bint copy=True,
  249. bint round_ok=True,
  250. bint is_coerce=False,
  251. ):
  252. """
  253. Convert an ndarray with datetime64[X] to datetime64[Y]
  254. or timedelta64[X] to timedelta64[Y],
  255. raising on overflow.
  256. """
  257. if values.descr.type_num == dtype.type_num == cnp.NPY_DATETIME:
  258. # i.e. dtype.kind == "M"
  259. dtype_name = "datetime64"
  260. elif values.descr.type_num == dtype.type_num == cnp.NPY_TIMEDELTA:
  261. # i.e. dtype.kind == "m"
  262. dtype_name = "timedelta64"
  263. else:
  264. raise TypeError(
  265. "astype_overflowsafe values.dtype and dtype must be either "
  266. "both-datetime64 or both-timedelta64."
  267. )
  268. cdef:
  269. NPY_DATETIMEUNIT from_unit = get_unit_from_dtype(values.dtype)
  270. NPY_DATETIMEUNIT to_unit = get_unit_from_dtype(dtype)
  271. if from_unit == NPY_DATETIMEUNIT.NPY_FR_GENERIC:
  272. raise TypeError(f"{dtype_name} values must have a unit specified")
  273. if to_unit == NPY_DATETIMEUNIT.NPY_FR_GENERIC:
  274. # without raising explicitly here, we end up with a SystemError
  275. # built-in function [...] returned a result with an error
  276. raise ValueError(
  277. f"{dtype_name} dtype must have a unit specified"
  278. )
  279. if from_unit == to_unit:
  280. # Check this before allocating result for perf, might save some memory
  281. if copy:
  282. return values.copy()
  283. return values
  284. elif from_unit > to_unit:
  285. if round_ok:
  286. # e.g. ns -> us, so there is no risk of overflow, so we can use
  287. # numpy's astype safely. Note there _is_ risk of truncation.
  288. return values.astype(dtype)
  289. else:
  290. iresult2 = astype_round_check(values.view("i8"), from_unit, to_unit)
  291. return iresult2.view(dtype)
  292. if (<object>values).dtype.byteorder == ">":
  293. # GH#29684 we incorrectly get OutOfBoundsDatetime if we dont swap
  294. values = values.astype(values.dtype.newbyteorder("<"))
  295. cdef:
  296. ndarray i8values = values.view("i8")
  297. # equiv: result = np.empty((<object>values).shape, dtype="i8")
  298. ndarray iresult = cnp.PyArray_EMPTY(
  299. values.ndim, values.shape, cnp.NPY_INT64, 0
  300. )
  301. cnp.broadcast mi = cnp.PyArray_MultiIterNew2(iresult, i8values)
  302. Py_ssize_t i, N = values.size
  303. int64_t value, new_value
  304. npy_datetimestruct dts
  305. bint is_td = dtype.type_num == cnp.NPY_TIMEDELTA
  306. for i in range(N):
  307. # Analogous to: item = values[i]
  308. value = (<int64_t*>cnp.PyArray_MultiIter_DATA(mi, 1))[0]
  309. if value == NPY_DATETIME_NAT:
  310. new_value = NPY_DATETIME_NAT
  311. else:
  312. pandas_datetime_to_datetimestruct(value, from_unit, &dts)
  313. try:
  314. check_dts_bounds(&dts, to_unit)
  315. except OutOfBoundsDatetime as err:
  316. if is_coerce:
  317. new_value = NPY_DATETIME_NAT
  318. elif is_td:
  319. from_abbrev = np.datetime_data(values.dtype)[0]
  320. np_val = np.timedelta64(value, from_abbrev)
  321. msg = (
  322. "Cannot convert {np_val} to {dtype} without overflow"
  323. .format(np_val=str(np_val), dtype=str(dtype))
  324. )
  325. raise OutOfBoundsTimedelta(msg) from err
  326. else:
  327. raise
  328. else:
  329. new_value = npy_datetimestruct_to_datetime(to_unit, &dts)
  330. # Analogous to: iresult[i] = new_value
  331. (<int64_t*>cnp.PyArray_MultiIter_DATA(mi, 0))[0] = new_value
  332. cnp.PyArray_MultiIter_NEXT(mi)
  333. return iresult.view(dtype)
  334. # TODO: try to upstream this fix to numpy
  335. def compare_mismatched_resolutions(ndarray left, ndarray right, op):
  336. """
  337. Overflow-safe comparison of timedelta64/datetime64 with mismatched resolutions.
  338. >>> left = np.array([500], dtype="M8[Y]")
  339. >>> right = np.array([0], dtype="M8[ns]")
  340. >>> left < right # <- wrong!
  341. array([ True])
  342. """
  343. if left.dtype.kind != right.dtype.kind or left.dtype.kind not in ["m", "M"]:
  344. raise ValueError("left and right must both be timedelta64 or both datetime64")
  345. cdef:
  346. int op_code = op_to_op_code(op)
  347. NPY_DATETIMEUNIT left_unit = get_unit_from_dtype(left.dtype)
  348. NPY_DATETIMEUNIT right_unit = get_unit_from_dtype(right.dtype)
  349. # equiv: result = np.empty((<object>left).shape, dtype="bool")
  350. ndarray result = cnp.PyArray_EMPTY(
  351. left.ndim, left.shape, cnp.NPY_BOOL, 0
  352. )
  353. ndarray lvalues = left.view("i8")
  354. ndarray rvalues = right.view("i8")
  355. cnp.broadcast mi = cnp.PyArray_MultiIterNew3(result, lvalues, rvalues)
  356. int64_t lval, rval
  357. bint res_value
  358. Py_ssize_t i, N = left.size
  359. npy_datetimestruct ldts, rdts
  360. for i in range(N):
  361. # Analogous to: lval = lvalues[i]
  362. lval = (<int64_t*>cnp.PyArray_MultiIter_DATA(mi, 1))[0]
  363. # Analogous to: rval = rvalues[i]
  364. rval = (<int64_t*>cnp.PyArray_MultiIter_DATA(mi, 2))[0]
  365. if lval == NPY_DATETIME_NAT or rval == NPY_DATETIME_NAT:
  366. res_value = op_code == Py_NE
  367. else:
  368. pandas_datetime_to_datetimestruct(lval, left_unit, &ldts)
  369. pandas_datetime_to_datetimestruct(rval, right_unit, &rdts)
  370. res_value = cmp_dtstructs(&ldts, &rdts, op_code)
  371. # Analogous to: result[i] = res_value
  372. (<uint8_t*>cnp.PyArray_MultiIter_DATA(mi, 0))[0] = res_value
  373. cnp.PyArray_MultiIter_NEXT(mi)
  374. return result
  375. import operator
  376. cdef int op_to_op_code(op):
  377. # TODO: should exist somewhere?
  378. if op is operator.eq:
  379. return Py_EQ
  380. if op is operator.ne:
  381. return Py_NE
  382. if op is operator.le:
  383. return Py_LE
  384. if op is operator.lt:
  385. return Py_LT
  386. if op is operator.ge:
  387. return Py_GE
  388. if op is operator.gt:
  389. return Py_GT
  390. cdef ndarray astype_round_check(
  391. ndarray i8values,
  392. NPY_DATETIMEUNIT from_unit,
  393. NPY_DATETIMEUNIT to_unit
  394. ):
  395. # cases with from_unit > to_unit, e.g. ns->us, raise if the conversion
  396. # involves truncation, e.g. 1500ns->1us
  397. cdef:
  398. Py_ssize_t i, N = i8values.size
  399. # equiv: iresult = np.empty((<object>i8values).shape, dtype="i8")
  400. ndarray iresult = cnp.PyArray_EMPTY(
  401. i8values.ndim, i8values.shape, cnp.NPY_INT64, 0
  402. )
  403. cnp.broadcast mi = cnp.PyArray_MultiIterNew2(iresult, i8values)
  404. # Note the arguments to_unit, from unit are swapped vs how they
  405. # are passed when going to a higher-frequency reso.
  406. int64_t mult = get_conversion_factor(to_unit, from_unit)
  407. int64_t value, mod
  408. for i in range(N):
  409. # Analogous to: item = i8values[i]
  410. value = (<int64_t*>cnp.PyArray_MultiIter_DATA(mi, 1))[0]
  411. if value == NPY_DATETIME_NAT:
  412. new_value = NPY_DATETIME_NAT
  413. else:
  414. new_value, mod = divmod(value, mult)
  415. if mod != 0:
  416. # TODO: avoid runtime import
  417. from pandas._libs.tslibs.dtypes import npy_unit_to_abbrev
  418. from_abbrev = npy_unit_to_abbrev(from_unit)
  419. to_abbrev = npy_unit_to_abbrev(to_unit)
  420. raise ValueError(
  421. f"Cannot losslessly cast '{value} {from_abbrev}' to {to_abbrev}"
  422. )
  423. # Analogous to: iresult[i] = new_value
  424. (<int64_t*>cnp.PyArray_MultiIter_DATA(mi, 0))[0] = new_value
  425. cnp.PyArray_MultiIter_NEXT(mi)
  426. return iresult
  427. @cython.overflowcheck(True)
  428. cdef int64_t get_conversion_factor(
  429. NPY_DATETIMEUNIT from_unit,
  430. NPY_DATETIMEUNIT to_unit
  431. ) except? -1:
  432. """
  433. Find the factor by which we need to multiply to convert from from_unit to to_unit.
  434. """
  435. if (
  436. from_unit == NPY_DATETIMEUNIT.NPY_FR_GENERIC
  437. or to_unit == NPY_DATETIMEUNIT.NPY_FR_GENERIC
  438. ):
  439. raise ValueError("unit-less resolutions are not supported")
  440. if from_unit > to_unit:
  441. raise ValueError
  442. if from_unit == to_unit:
  443. return 1
  444. if from_unit == NPY_DATETIMEUNIT.NPY_FR_W:
  445. return 7 * get_conversion_factor(NPY_DATETIMEUNIT.NPY_FR_D, to_unit)
  446. elif from_unit == NPY_DATETIMEUNIT.NPY_FR_D:
  447. return 24 * get_conversion_factor(NPY_DATETIMEUNIT.NPY_FR_h, to_unit)
  448. elif from_unit == NPY_DATETIMEUNIT.NPY_FR_h:
  449. return 60 * get_conversion_factor(NPY_DATETIMEUNIT.NPY_FR_m, to_unit)
  450. elif from_unit == NPY_DATETIMEUNIT.NPY_FR_m:
  451. return 60 * get_conversion_factor(NPY_DATETIMEUNIT.NPY_FR_s, to_unit)
  452. elif from_unit == NPY_DATETIMEUNIT.NPY_FR_s:
  453. return 1000 * get_conversion_factor(NPY_DATETIMEUNIT.NPY_FR_ms, to_unit)
  454. elif from_unit == NPY_DATETIMEUNIT.NPY_FR_ms:
  455. return 1000 * get_conversion_factor(NPY_DATETIMEUNIT.NPY_FR_us, to_unit)
  456. elif from_unit == NPY_DATETIMEUNIT.NPY_FR_us:
  457. return 1000 * get_conversion_factor(NPY_DATETIMEUNIT.NPY_FR_ns, to_unit)
  458. elif from_unit == NPY_DATETIMEUNIT.NPY_FR_ns:
  459. return 1000 * get_conversion_factor(NPY_DATETIMEUNIT.NPY_FR_ps, to_unit)
  460. elif from_unit == NPY_DATETIMEUNIT.NPY_FR_ps:
  461. return 1000 * get_conversion_factor(NPY_DATETIMEUNIT.NPY_FR_fs, to_unit)
  462. elif from_unit == NPY_DATETIMEUNIT.NPY_FR_fs:
  463. return 1000 * get_conversion_factor(NPY_DATETIMEUNIT.NPY_FR_as, to_unit)
  464. cdef int64_t convert_reso(
  465. int64_t value,
  466. NPY_DATETIMEUNIT from_reso,
  467. NPY_DATETIMEUNIT to_reso,
  468. bint round_ok,
  469. ) except? -1:
  470. cdef:
  471. int64_t res_value, mult, div, mod
  472. if from_reso == to_reso:
  473. return value
  474. elif to_reso < from_reso:
  475. # e.g. ns -> us, no risk of overflow, but can be lossy rounding
  476. mult = get_conversion_factor(to_reso, from_reso)
  477. div, mod = divmod(value, mult)
  478. if mod > 0 and not round_ok:
  479. raise ValueError("Cannot losslessly convert units")
  480. # Note that when mod > 0, we follow np.timedelta64 in always
  481. # rounding down.
  482. res_value = div
  483. elif (
  484. from_reso == NPY_FR_Y
  485. or from_reso == NPY_FR_M
  486. or to_reso == NPY_FR_Y
  487. or to_reso == NPY_FR_M
  488. ):
  489. # Converting by multiplying isn't _quite_ right bc the number of
  490. # seconds in a month/year isn't fixed.
  491. res_value = _convert_reso_with_dtstruct(value, from_reso, to_reso)
  492. else:
  493. # e.g. ns -> us, risk of overflow, but no risk of lossy rounding
  494. mult = get_conversion_factor(from_reso, to_reso)
  495. with cython.overflowcheck(True):
  496. # Note: caller is responsible for re-raising as OutOfBoundsTimedelta
  497. res_value = value * mult
  498. return res_value
  499. cdef int64_t _convert_reso_with_dtstruct(
  500. int64_t value,
  501. NPY_DATETIMEUNIT from_unit,
  502. NPY_DATETIMEUNIT to_unit,
  503. ) except? -1:
  504. cdef:
  505. npy_datetimestruct dts
  506. pandas_datetime_to_datetimestruct(value, from_unit, &dts)
  507. check_dts_bounds(&dts, to_unit)
  508. return npy_datetimestruct_to_datetime(to_unit, &dts)