@@ -41,7 +41,6 @@ from pandas._libs.missing cimport checknull_with_nat_and_na
4141from pandas._libs.tslibs.base cimport ABCTimestamp
4242from pandas._libs.tslibs.conversion cimport (
4343 cast_from_unit,
44- precision_from_unit,
4544)
4645from pandas._libs.tslibs.dtypes cimport (
4746 c_DEPR_UNITS,
@@ -290,68 +289,6 @@ cpdef int64_t delta_to_nanoseconds(
290289 ) from err
291290
292291
293- @ cython.overflowcheck (True )
294- cdef object ensure_td64ns(object ts):
295- """
296- Overflow-safe implementation of td64.astype("m8[ns]")
297-
298- Parameters
299- ----------
300- ts : np.timedelta64
301-
302- Returns
303- -------
304- np.timedelta64[ns]
305- """
306- cdef:
307- NPY_DATETIMEUNIT td64_unit
308- int64_t td64_value, mult
309-
310- td64_unit = get_datetime64_unit(ts)
311- if (
312- td64_unit != NPY_DATETIMEUNIT.NPY_FR_ns
313- and td64_unit != NPY_DATETIMEUNIT.NPY_FR_GENERIC
314- ):
315-
316- td64_value = cnp.get_timedelta64_value(ts)
317-
318- mult = precision_from_unit(td64_unit)[0 ]
319- try :
320- # NB: cython#1381 this cannot be *=
321- td64_value = td64_value * mult
322- except OverflowError as err:
323- raise OutOfBoundsTimedelta(ts) from err
324-
325- return np.timedelta64(td64_value, " ns" )
326-
327- return ts
328-
329-
330- cdef convert_to_timedelta64(object ts, str unit):
331- """
332- Convert an incoming object to a timedelta64 if possible.
333- Before calling, unit must be standardized to avoid repeated unit conversion
334-
335- Handle these types of objects:
336- - timedelta/Timedelta
337-
338- Return a timedelta64[ns] object
339- """
340- # Caller is responsible for checking unit not in ["Y", "y", "M"]
341- if isinstance (ts, _Timedelta):
342- # already in the proper format
343- if ts._creso != NPY_FR_ns:
344- ts = ts.as_unit(" ns" ).asm8
345- else :
346- ts = np.timedelta64(ts._value, " ns" )
347-
348- elif PyDelta_Check(ts):
349- ts = np.timedelta64(delta_to_nanoseconds(ts), " ns" )
350- elif not cnp.is_timedelta64_object(ts):
351- raise TypeError (f" Invalid type for timedelta scalar: {type(ts)}" )
352- return ts.astype(" timedelta64[ns]" )
353-
354-
355292cdef _numeric_to_td64ns(object item, str unit):
356293 # caller is responsible for checking
357294 # assert unit not in ["Y", "y", "M"]
@@ -370,10 +307,34 @@ cdef _numeric_to_td64ns(object item, str unit):
370307 return ts
371308
372309
310+ # TODO: de-duplicate with DatetimeParseState
311+ cdef class ResoState:
312+ cdef:
313+ NPY_DATETIMEUNIT creso
314+ bint creso_ever_changed
315+
316+ def __cinit__ (self , NPY_DATETIMEUNIT creso ):
317+ self .creso = creso
318+ self .creso_ever_changed = False
319+
320+ cdef bint update_creso(self , NPY_DATETIMEUNIT item_reso) noexcept:
321+ # Return a bool indicating whether we bumped to a higher resolution
322+ if self .creso == NPY_DATETIMEUNIT.NPY_FR_GENERIC:
323+ self .creso = item_reso
324+ elif item_reso > self .creso:
325+ self .creso = item_reso
326+ self .creso_ever_changed = True
327+ return True
328+ return False
329+
330+
373331@ cython.boundscheck (False )
374332@ cython.wraparound (False )
375333def array_to_timedelta64 (
376- ndarray values , str unit = None , str errors = " raise"
334+ ndarray values ,
335+ str unit = None ,
336+ str errors = " raise" ,
337+ NPY_DATETIMEUNIT creso = NPY_DATETIMEUNIT.NPY_FR_GENERIC,
377338) -> ndarray:
378339 # values is object-dtype , may be 2D
379340 """
@@ -395,6 +356,10 @@ def array_to_timedelta64(
395356 cnp.broadcast mi = cnp.PyArray_MultiIterNew2(result, values)
396357 cnp.flatiter it
397358 str parsed_unit = parse_timedelta_unit(unit or " ns" )
359+ NPY_DATETIMEUNIT item_reso
360+ ResoState state = ResoState(creso)
361+ bint infer_reso = creso == NPY_DATETIMEUNIT.NPY_FR_GENERIC
362+ ndarray iresult = result.view(" i8" )
398363
399364 if values.descr.type_num != cnp.NPY_OBJECT:
400365 # raise here otherwise we segfault below
@@ -422,18 +387,58 @@ def array_to_timedelta64(
422387 ival = NPY_NAT
423388
424389 elif cnp.is_timedelta64_object(item):
425- td64ns_obj = ensure_td64ns(item)
426- ival = cnp.get_timedelta64_value(td64ns_obj)
390+ # TODO: de-duplicate this with Timedelta.__new__
391+ ival = cnp.get_timedelta64_value(item)
392+ dt64_reso = get_datetime64_unit(item)
393+ if not (
394+ is_supported_unit(dt64_reso) or
395+ dt64_reso in [
396+ NPY_DATETIMEUNIT.NPY_FR_m,
397+ NPY_DATETIMEUNIT.NPY_FR_h,
398+ NPY_DATETIMEUNIT.NPY_FR_D,
399+ NPY_DATETIMEUNIT.NPY_FR_W,
400+ NPY_DATETIMEUNIT.NPY_FR_GENERIC
401+ ]
402+ ):
403+ err = npy_unit_to_abbrev(dt64_reso)
404+ raise ValueError (
405+ f" Unit {err} is not supported. "
406+ " Only unambiguous timedelta values durations are supported. "
407+ " Allowed units are 'W', 'D', 'h', 'm', 's', 'ms', 'us', 'ns'" )
408+
409+ item_reso = get_supported_reso(dt64_reso)
410+ state.update_creso(item_reso)
411+ if infer_reso:
412+ creso = state.creso
413+ if dt64_reso != NPY_DATETIMEUNIT.NPY_FR_GENERIC:
414+ try :
415+ ival = convert_reso(
416+ ival,
417+ dt64_reso,
418+ creso,
419+ round_ok = True ,
420+ )
421+ except (OverflowError , OutOfBoundsDatetime) as err:
422+ raise OutOfBoundsTimedelta(item) from err
423+ else :
424+ # e.g. NaT
425+ pass
427426
428427 elif isinstance (item, _Timedelta):
429- if item._creso != NPY_FR_ns:
430- ival = item.as_unit(" ns" )._value
431- else :
432- ival = item._value
428+ item_reso = item._creso
429+ state.update_creso(item_reso)
430+ if infer_reso:
431+ creso = state.creso
432+
433+ ival = (< _Timedelta> item)._as_creso(creso)._value
433434
434435 elif PyDelta_Check(item):
435436 # i.e. isinstance(item, timedelta)
436- ival = delta_to_nanoseconds(item)
437+ item_reso = NPY_DATETIMEUNIT.NPY_FR_us
438+ state.update_creso(item_reso)
439+ if infer_reso:
440+ creso = state.creso
441+ ival = delta_to_nanoseconds(item, reso = creso)
437442
438443 elif isinstance (item, str ):
439444 if (
@@ -444,13 +449,27 @@ def array_to_timedelta64(
444449 else :
445450 ival = parse_timedelta_string(item)
446451
452+ item_reso = NPY_FR_ns
453+ state.update_creso(item_reso)
454+ if infer_reso:
455+ creso = state.creso
456+
447457 elif is_tick_object(item):
448- ival = item.nanos
458+ item_reso = get_supported_reso(item._creso)
459+ state.update_creso(item_reso)
460+ if infer_reso:
461+ creso = state.creso
462+ ival = delta_to_nanoseconds(item, reso = creso)
449463
450464 elif is_integer_object(item) or is_float_object(item):
451465 td64ns_obj = _numeric_to_td64ns(item, parsed_unit)
452466 ival = cnp.get_timedelta64_value(td64ns_obj)
453467
468+ item_reso = NPY_FR_ns
469+ state.update_creso(item_reso)
470+ if infer_reso:
471+ creso = state.creso
472+
454473 else :
455474 raise TypeError (f" Invalid type for timedelta scalar: {type(item)}" )
456475
@@ -468,6 +487,29 @@ def array_to_timedelta64(
468487
469488 cnp.PyArray_MultiIter_NEXT(mi)
470489
490+ if infer_reso:
491+ if state.creso_ever_changed:
492+ # We encountered mismatched resolutions, need to re-parse with
493+ # the correct one.
494+ return array_to_timedelta64(
495+ values,
496+ unit = unit,
497+ errors = errors,
498+ creso = state.creso,
499+ )
500+ elif creso == NPY_DATETIMEUNIT.NPY_FR_GENERIC:
501+ # i.e. we never encountered anything non-NaT, default to "s". This
502+ # ensures that insert and concat-like operations with NaT
503+ # do not upcast units
504+ result = iresult.view(" m8[s]" )
505+ else :
506+ # Otherwise we can use the single reso that we encountered and avoid
507+ # a second pass.
508+ abbrev = npy_unit_to_abbrev(state.creso)
509+ result = iresult.view(f" m8[{abbrev}]" )
510+ else :
511+ abbrev = npy_unit_to_abbrev(creso)
512+ result = result.view(f" m8[{abbrev}]" )
471513 return result
472514
473515
0 commit comments