@@ -282,6 +282,7 @@ cpdef ndarray astype_overflowsafe(
282
282
ndarray values,
283
283
cnp.dtype dtype,
284
284
bint copy = True ,
285
+ bint round_ok = True ,
285
286
):
286
287
"""
287
288
Convert an ndarray with datetime64[X] to datetime64[Y]
@@ -314,20 +315,24 @@ cpdef ndarray astype_overflowsafe(
314
315
" datetime64/timedelta64 values and dtype must have a unit specified"
315
316
)
316
317
317
- if (< object > values).dtype.byteorder == " >" :
318
- # GH#29684 we incorrectly get OutOfBoundsDatetime if we dont swap
319
- values = values.astype(values.dtype.newbyteorder(" <" ))
320
-
321
318
if from_unit == to_unit:
322
319
# Check this before allocating result for perf, might save some memory
323
320
if copy:
324
321
return values.copy()
325
322
return values
326
323
327
324
elif from_unit > to_unit:
328
- # e.g. ns -> us, so there is no risk of overflow, so we can use
329
- # numpy's astype safely. Note there _is_ risk of truncation.
330
- return values.astype(dtype)
325
+ if round_ok:
326
+ # e.g. ns -> us, so there is no risk of overflow, so we can use
327
+ # numpy's astype safely. Note there _is_ risk of truncation.
328
+ return values.astype(dtype)
329
+ else :
330
+ iresult2 = astype_round_check(values.view(" i8" ), from_unit, to_unit)
331
+ return iresult2.view(dtype)
332
+
333
+ if (< object > values).dtype.byteorder == " >" :
334
+ # GH#29684 we incorrectly get OutOfBoundsDatetime if we dont swap
335
+ values = values.astype(values.dtype.newbyteorder(" <" ))
331
336
332
337
cdef:
333
338
ndarray i8values = values.view(" i8" )
@@ -356,10 +361,11 @@ cpdef ndarray astype_overflowsafe(
356
361
check_dts_bounds(& dts, to_unit)
357
362
except OutOfBoundsDatetime as err:
358
363
if is_td:
359
- tdval = np.timedelta64(value).view(values.dtype)
364
+ from_abbrev = np.datetime_data(values.dtype)[0 ]
365
+ np_val = np.timedelta64(value, from_abbrev)
360
366
msg = (
361
- " Cannot convert {tdval } to {dtype} without overflow"
362
- .format(tdval = str (tdval ), dtype = str (dtype))
367
+ " Cannot convert {np_val } to {dtype} without overflow"
368
+ .format(np_val = str (np_val ), dtype = str (dtype))
363
369
)
364
370
raise OutOfBoundsTimedelta(msg) from err
365
371
else :
@@ -453,6 +459,52 @@ cdef int op_to_op_code(op):
453
459
return Py_GT
454
460
455
461
462
+ cdef ndarray astype_round_check(
463
+ ndarray i8values,
464
+ NPY_DATETIMEUNIT from_unit,
465
+ NPY_DATETIMEUNIT to_unit
466
+ ):
467
+ # cases with from_unit > to_unit, e.g. ns->us, raise if the conversion
468
+ # involves truncation, e.g. 1500ns->1us
469
+ cdef:
470
+ Py_ssize_t i, N = i8values.size
471
+
472
+ # equiv: iresult = np.empty((<object>i8values).shape, dtype="i8")
473
+ ndarray iresult = cnp.PyArray_EMPTY(
474
+ i8values.ndim, i8values.shape, cnp.NPY_INT64, 0
475
+ )
476
+ cnp.broadcast mi = cnp.PyArray_MultiIterNew2(iresult, i8values)
477
+
478
+ # Note the arguments to_unit, from unit are swapped vs how they
479
+ # are passed when going to a higher-frequency reso.
480
+ int64_t mult = get_conversion_factor(to_unit, from_unit)
481
+ int64_t value, mod
482
+
483
+ for i in range (N):
484
+ # Analogous to: item = i8values[i]
485
+ value = (< int64_t* > cnp.PyArray_MultiIter_DATA(mi, 1 ))[0 ]
486
+
487
+ if value == NPY_DATETIME_NAT:
488
+ new_value = NPY_DATETIME_NAT
489
+ else :
490
+ new_value, mod = divmod (value, mult)
491
+ if mod != 0 :
492
+ # TODO: avoid runtime import
493
+ from pandas._libs.tslibs.dtypes import npy_unit_to_abbrev
494
+ from_abbrev = npy_unit_to_abbrev(from_unit)
495
+ to_abbrev = npy_unit_to_abbrev(to_unit)
496
+ raise ValueError (
497
+ f" Cannot losslessly cast '{value} {from_abbrev}' to {to_abbrev}"
498
+ )
499
+
500
+ # Analogous to: iresult[i] = new_value
501
+ (< int64_t* > cnp.PyArray_MultiIter_DATA(mi, 0 ))[0 ] = new_value
502
+
503
+ cnp.PyArray_MultiIter_NEXT(mi)
504
+
505
+ return iresult
506
+
507
+
456
508
@ cython.overflowcheck (True )
457
509
cdef int64_t get_conversion_factor(NPY_DATETIMEUNIT from_unit, NPY_DATETIMEUNIT to_unit) except ? - 1 :
458
510
"""
0 commit comments