@@ -22,7 +22,10 @@ import_datetime()
2222cimport numpy as cnp
2323
2424cnp.import_array()
25- from numpy cimport int64_t
25+ from numpy cimport (
26+ int64_t,
27+ ndarray,
28+ )
2629
2730from pandas._libs.tslibs.util cimport get_c_string_buf_and_size
2831
@@ -36,7 +39,12 @@ cdef extern from "src/datetime/np_datetime.h":
3639 pandas_timedeltastruct * result
3740 ) nogil
3841
42+ # AS, FS, PS versions exist but are not imported because they are not used.
3943 npy_datetimestruct _NS_MIN_DTS, _NS_MAX_DTS
44+ npy_datetimestruct _US_MIN_DTS, _US_MAX_DTS
45+ npy_datetimestruct _MS_MIN_DTS, _MS_MAX_DTS
46+ npy_datetimestruct _S_MIN_DTS, _S_MAX_DTS
47+ npy_datetimestruct _M_MIN_DTS, _M_MAX_DTS
4048
4149 PyArray_DatetimeMetaData get_datetime_metadata_from_dtype(cnp.PyArray_Descr * dtype);
4250
@@ -119,22 +127,40 @@ class OutOfBoundsDatetime(ValueError):
119127 pass
120128
121129
122- cdef inline check_dts_bounds(npy_datetimestruct * dts):
130+ cdef check_dts_bounds(npy_datetimestruct * dts, NPY_DATETIMEUNIT unit = NPY_FR_ns ):
123131 """ Raises OutOfBoundsDatetime if the given date is outside the range that
124132 can be represented by nanosecond-resolution 64-bit integers."""
125133 cdef:
126134 bint error = False
127-
128- if (dts.year <= 1677 and
129- cmp_npy_datetimestruct(dts, & _NS_MIN_DTS) == - 1 ):
135+ npy_datetimestruct cmp_upper, cmp_lower
136+
137+ if unit == NPY_FR_ns:
138+ cmp_upper = _NS_MAX_DTS
139+ cmp_lower = _NS_MIN_DTS
140+ elif unit == NPY_FR_us:
141+ cmp_upper = _US_MAX_DTS
142+ cmp_lower = _US_MIN_DTS
143+ elif unit == NPY_FR_ms:
144+ cmp_upper = _MS_MAX_DTS
145+ cmp_lower = _MS_MIN_DTS
146+ elif unit == NPY_FR_s:
147+ cmp_upper = _S_MAX_DTS
148+ cmp_lower = _S_MIN_DTS
149+ elif unit == NPY_FR_m:
150+ cmp_upper = _M_MAX_DTS
151+ cmp_lower = _M_MIN_DTS
152+ else :
153+ raise NotImplementedError (unit)
154+
155+ if cmp_npy_datetimestruct(dts, & cmp_lower) == - 1 :
130156 error = True
131- elif (dts.year >= 2262 and
132- cmp_npy_datetimestruct(dts, & _NS_MAX_DTS) == 1 ):
157+ elif cmp_npy_datetimestruct(dts, & cmp_upper) == 1 :
133158 error = True
134159
135160 if error:
136161 fmt = (f' {dts.year}-{dts.month:02d}-{dts.day:02d} '
137162 f' {dts.hour:02d}:{dts.min:02d}:{dts.sec:02d}' )
163+ # TODO: "nanosecond" in the message assumes NPY_FR_ns
138164 raise OutOfBoundsDatetime(f' Out of bounds nanosecond timestamp: {fmt}' )
139165
140166
@@ -202,3 +228,68 @@ cdef inline int _string_to_dts(str val, npy_datetimestruct* dts,
202228 buf = get_c_string_buf_and_size(val, & length)
203229 return parse_iso_8601_datetime(buf, length, want_exc,
204230 dts, out_local, out_tzoffset)
231+
232+
233+ cpdef ndarray astype_overflowsafe(
234+ ndarray values,
235+ cnp.dtype dtype,
236+ bint copy = True ,
237+ ):
238+ """
239+ Convert an ndarray with datetime64[X] to datetime64[Y], raising on overflow.
240+ """
241+ if values.descr.type_num != cnp.NPY_DATETIME:
242+ # aka values.dtype.kind != "M"
243+ raise TypeError (" astype_overflowsafe values must have datetime64 dtype" )
244+ if dtype.type_num != cnp.NPY_DATETIME:
245+ raise TypeError (" astype_overflowsafe dtype must be datetime64" )
246+
247+ cdef:
248+ NPY_DATETIMEUNIT from_unit = get_unit_from_dtype(values.dtype)
249+ NPY_DATETIMEUNIT to_unit = get_unit_from_dtype(dtype)
250+
251+ if (
252+ from_unit == NPY_DATETIMEUNIT.NPY_FR_GENERIC
253+ or to_unit == NPY_DATETIMEUNIT.NPY_FR_GENERIC
254+ ):
255+ # without raising explicitly here, we end up with a SystemError
256+ # built-in function [...] returned a result with an error
257+ raise ValueError (" datetime64 values and dtype must have a unit specified" )
258+
259+ if from_unit == to_unit:
260+ # Check this before allocating result for perf, might save some memory
261+ if copy:
262+ return values.copy()
263+ return values
264+
265+ cdef:
266+ ndarray i8values = values.view(" i8" )
267+
268+ # equiv: result = np.empty((<object>values).shape, dtype="i8")
269+ ndarray iresult = cnp.PyArray_EMPTY(
270+ values.ndim, values.shape, cnp.NPY_INT64, 0
271+ )
272+
273+ cnp.broadcast mi = cnp.PyArray_MultiIterNew2(iresult, i8values)
274+ cnp.flatiter it
275+ Py_ssize_t i, N = values.size
276+ int64_t value, new_value
277+ npy_datetimestruct dts
278+
279+ for i in range (N):
280+ # Analogous to: item = values[i]
281+ value = (< int64_t* > cnp.PyArray_MultiIter_DATA(mi, 1 ))[0 ]
282+
283+ if value == NPY_DATETIME_NAT:
284+ new_value = NPY_DATETIME_NAT
285+ else :
286+ pandas_datetime_to_datetimestruct(value, from_unit, & dts)
287+ check_dts_bounds(& dts, to_unit)
288+ new_value = npy_datetimestruct_to_datetime(to_unit, & dts)
289+
290+ # Analogous to: iresult[i] = new_value
291+ (< int64_t* > cnp.PyArray_MultiIter_DATA(mi, 0 ))[0 ] = new_value
292+
293+ cnp.PyArray_MultiIter_NEXT(mi)
294+
295+ return iresult.view(dtype)
0 commit comments