Skip to content

Commit 87da500

Browse files
authored
ENH: Timestamp.replace support non-nano (pandas-dev#47312)
1 parent b74dc5c commit 87da500

File tree

6 files changed

+94
-52
lines changed

6 files changed

+94
-52
lines changed

pandas/_libs/tslibs/conversion.pxd

+2-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ cdef _TSObject convert_to_tsobject(object ts, tzinfo tz, str unit,
2727
int32_t nanos=*)
2828

2929
cdef _TSObject convert_datetime_to_tsobject(datetime ts, tzinfo tz,
30-
int32_t nanos=*)
30+
int32_t nanos=*,
31+
NPY_DATETIMEUNIT reso=*)
3132

3233
cdef int64_t get_datetime64_nanos(object val) except? -1
3334

pandas/_libs/tslibs/conversion.pyx

+36-19
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ from cpython.datetime cimport (
3131
import_datetime()
3232

3333
from pandas._libs.tslibs.base cimport ABCTimestamp
34+
from pandas._libs.tslibs.dtypes cimport periods_per_second
3435
from pandas._libs.tslibs.np_datetime cimport (
3536
NPY_DATETIMEUNIT,
3637
NPY_FR_ns,
@@ -40,11 +41,14 @@ from pandas._libs.tslibs.np_datetime cimport (
4041
dtstruct_to_dt64,
4142
get_datetime64_unit,
4243
get_datetime64_value,
44+
get_implementation_bounds,
4345
get_unit_from_dtype,
4446
npy_datetime,
4547
npy_datetimestruct,
48+
npy_datetimestruct_to_datetime,
4649
pandas_datetime_to_datetimestruct,
4750
pydatetime_to_dt64,
51+
pydatetime_to_dtstruct,
4852
string_to_dts,
4953
)
5054

@@ -307,11 +311,15 @@ cdef maybe_localize_tso(_TSObject obj, tzinfo tz, NPY_DATETIMEUNIT reso):
307311
if obj.value != NPY_NAT:
308312
# check_overflows needs to run after _localize_tso
309313
check_dts_bounds(&obj.dts, reso)
310-
check_overflows(obj)
314+
check_overflows(obj, reso)
311315

312316

313-
cdef _TSObject convert_datetime_to_tsobject(datetime ts, tzinfo tz,
314-
int32_t nanos=0):
317+
cdef _TSObject convert_datetime_to_tsobject(
318+
datetime ts,
319+
tzinfo tz,
320+
int32_t nanos=0,
321+
NPY_DATETIMEUNIT reso=NPY_FR_ns,
322+
):
315323
"""
316324
Convert a datetime (or Timestamp) input `ts`, along with optional timezone
317325
object `tz` to a _TSObject.
@@ -327,13 +335,15 @@ cdef _TSObject convert_datetime_to_tsobject(datetime ts, tzinfo tz,
327335
timezone for the timezone-aware output
328336
nanos : int32_t, default is 0
329337
nanoseconds supplement the precision of the datetime input ts
338+
reso : NPY_DATETIMEUNIT, default NPY_FR_ns
330339
331340
Returns
332341
-------
333342
obj : _TSObject
334343
"""
335344
cdef:
336345
_TSObject obj = _TSObject()
346+
int64_t pps
337347

338348
obj.fold = ts.fold
339349
if tz is not None:
@@ -342,34 +352,35 @@ cdef _TSObject convert_datetime_to_tsobject(datetime ts, tzinfo tz,
342352
if ts.tzinfo is not None:
343353
# Convert the current timezone to the passed timezone
344354
ts = ts.astimezone(tz)
345-
obj.value = pydatetime_to_dt64(ts, &obj.dts)
355+
pydatetime_to_dtstruct(ts, &obj.dts)
346356
obj.tzinfo = ts.tzinfo
347357
elif not is_utc(tz):
348358
ts = _localize_pydatetime(ts, tz)
349-
obj.value = pydatetime_to_dt64(ts, &obj.dts)
359+
pydatetime_to_dtstruct(ts, &obj.dts)
350360
obj.tzinfo = ts.tzinfo
351361
else:
352362
# UTC
353-
obj.value = pydatetime_to_dt64(ts, &obj.dts)
363+
pydatetime_to_dtstruct(ts, &obj.dts)
354364
obj.tzinfo = tz
355365
else:
356-
obj.value = pydatetime_to_dt64(ts, &obj.dts)
366+
pydatetime_to_dtstruct(ts, &obj.dts)
357367
obj.tzinfo = ts.tzinfo
358368

359-
if obj.tzinfo is not None and not is_utc(obj.tzinfo):
360-
offset = get_utcoffset(obj.tzinfo, ts)
361-
obj.value -= int(offset.total_seconds() * 1e9)
362-
363369
if isinstance(ts, ABCTimestamp):
364-
obj.value += <int64_t>ts.nanosecond
365370
obj.dts.ps = ts.nanosecond * 1000
366371

367372
if nanos:
368-
obj.value += nanos
369373
obj.dts.ps = nanos * 1000
370374

371-
check_dts_bounds(&obj.dts)
372-
check_overflows(obj)
375+
obj.value = npy_datetimestruct_to_datetime(reso, &obj.dts)
376+
377+
if obj.tzinfo is not None and not is_utc(obj.tzinfo):
378+
offset = get_utcoffset(obj.tzinfo, ts)
379+
pps = periods_per_second(reso)
380+
obj.value -= int(offset.total_seconds() * pps)
381+
382+
check_dts_bounds(&obj.dts, reso)
383+
check_overflows(obj, reso)
373384
return obj
374385

375386

@@ -401,7 +412,7 @@ cdef _TSObject _create_tsobject_tz_using_offset(npy_datetimestruct dts,
401412
obj.tzinfo = pytz.FixedOffset(tzoffset)
402413
obj.value = tz_localize_to_utc_single(value, obj.tzinfo)
403414
if tz is None:
404-
check_overflows(obj)
415+
check_overflows(obj, NPY_FR_ns)
405416
return obj
406417

407418
cdef:
@@ -515,13 +526,14 @@ cdef _TSObject _convert_str_to_tsobject(object ts, tzinfo tz, str unit,
515526
return convert_datetime_to_tsobject(dt, tz)
516527

517528

518-
cdef inline check_overflows(_TSObject obj):
529+
cdef inline check_overflows(_TSObject obj, NPY_DATETIMEUNIT reso=NPY_FR_ns):
519530
"""
520531
Check that we haven't silently overflowed in timezone conversion
521532
522533
Parameters
523534
----------
524535
obj : _TSObject
536+
reso : NPY_DATETIMEUNIT, default NPY_FR_ns
525537
526538
Returns
527539
-------
@@ -532,15 +544,20 @@ cdef inline check_overflows(_TSObject obj):
532544
OutOfBoundsDatetime
533545
"""
534546
# GH#12677
535-
if obj.dts.year == 1677:
547+
cdef:
548+
npy_datetimestruct lb, ub
549+
550+
get_implementation_bounds(reso, &lb, &ub)
551+
552+
if obj.dts.year == lb.year:
536553
if not (obj.value < 0):
537554
from pandas._libs.tslibs.timestamps import Timestamp
538555
fmt = (f"{obj.dts.year}-{obj.dts.month:02d}-{obj.dts.day:02d} "
539556
f"{obj.dts.hour:02d}:{obj.dts.min:02d}:{obj.dts.sec:02d}")
540557
raise OutOfBoundsDatetime(
541558
f"Converting {fmt} underflows past {Timestamp.min}"
542559
)
543-
elif obj.dts.year == 2262:
560+
elif obj.dts.year == ub.year:
544561
if not (obj.value > 0):
545562
from pandas._libs.tslibs.timestamps import Timestamp
546563
fmt = (f"{obj.dts.year}-{obj.dts.month:02d}-{obj.dts.day:02d} "

pandas/_libs/tslibs/np_datetime.pxd

+4
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ cdef int64_t dtstruct_to_dt64(npy_datetimestruct* dts) nogil
7979
cdef void dt64_to_dtstruct(int64_t dt64, npy_datetimestruct* out) nogil
8080

8181
cdef int64_t pydatetime_to_dt64(datetime val, npy_datetimestruct *dts)
82+
cdef void pydatetime_to_dtstruct(datetime dt, npy_datetimestruct *dts)
8283
cdef int64_t pydate_to_dt64(date val, npy_datetimestruct *dts)
8384
cdef void pydate_to_dtstruct(date val, npy_datetimestruct *dts)
8485

@@ -104,3 +105,6 @@ cpdef cnp.ndarray astype_overflowsafe(
104105
)
105106

106107
cdef bint cmp_dtstructs(npy_datetimestruct* left, npy_datetimestruct* right, int op)
108+
cdef get_implementation_bounds(
109+
NPY_DATETIMEUNIT reso, npy_datetimestruct *lower, npy_datetimestruct *upper
110+
)

pandas/_libs/tslibs/np_datetime.pyx

+33-25
Original file line numberDiff line numberDiff line change
@@ -167,30 +167,34 @@ class OutOfBoundsTimedelta(ValueError):
167167
pass
168168

169169

170+
cdef get_implementation_bounds(NPY_DATETIMEUNIT reso, npy_datetimestruct *lower, npy_datetimestruct *upper):
171+
if reso == NPY_FR_ns:
172+
upper[0] = _NS_MAX_DTS
173+
lower[0] = _NS_MIN_DTS
174+
elif reso == NPY_FR_us:
175+
upper[0] = _US_MAX_DTS
176+
lower[0] = _US_MIN_DTS
177+
elif reso == NPY_FR_ms:
178+
upper[0] = _MS_MAX_DTS
179+
lower[0] = _MS_MIN_DTS
180+
elif reso == NPY_FR_s:
181+
upper[0] = _S_MAX_DTS
182+
lower[0] = _S_MIN_DTS
183+
elif reso == NPY_FR_m:
184+
upper[0] = _M_MAX_DTS
185+
lower[0] = _M_MIN_DTS
186+
else:
187+
raise NotImplementedError(reso)
188+
189+
170190
cdef check_dts_bounds(npy_datetimestruct *dts, NPY_DATETIMEUNIT unit=NPY_FR_ns):
171191
"""Raises OutOfBoundsDatetime if the given date is outside the range that
172192
can be represented by nanosecond-resolution 64-bit integers."""
173193
cdef:
174194
bint error = False
175195
npy_datetimestruct cmp_upper, cmp_lower
176196

177-
if unit == NPY_FR_ns:
178-
cmp_upper = _NS_MAX_DTS
179-
cmp_lower = _NS_MIN_DTS
180-
elif unit == NPY_FR_us:
181-
cmp_upper = _US_MAX_DTS
182-
cmp_lower = _US_MIN_DTS
183-
elif unit == NPY_FR_ms:
184-
cmp_upper = _MS_MAX_DTS
185-
cmp_lower = _MS_MIN_DTS
186-
elif unit == NPY_FR_s:
187-
cmp_upper = _S_MAX_DTS
188-
cmp_lower = _S_MIN_DTS
189-
elif unit == NPY_FR_m:
190-
cmp_upper = _M_MAX_DTS
191-
cmp_lower = _M_MIN_DTS
192-
else:
193-
raise NotImplementedError(unit)
197+
get_implementation_bounds(unit, &cmp_lower, &cmp_upper)
194198

195199
if cmp_npy_datetimestruct(dts, &cmp_lower) == -1:
196200
error = True
@@ -229,19 +233,23 @@ def py_td64_to_tdstruct(int64_t td64, NPY_DATETIMEUNIT unit):
229233
return tds # <- returned as a dict to python
230234

231235

236+
cdef inline void pydatetime_to_dtstruct(datetime dt, npy_datetimestruct *dts):
237+
dts.year = PyDateTime_GET_YEAR(dt)
238+
dts.month = PyDateTime_GET_MONTH(dt)
239+
dts.day = PyDateTime_GET_DAY(dt)
240+
dts.hour = PyDateTime_DATE_GET_HOUR(dt)
241+
dts.min = PyDateTime_DATE_GET_MINUTE(dt)
242+
dts.sec = PyDateTime_DATE_GET_SECOND(dt)
243+
dts.us = PyDateTime_DATE_GET_MICROSECOND(dt)
244+
dts.ps = dts.as = 0
245+
246+
232247
cdef inline int64_t pydatetime_to_dt64(datetime val,
233248
npy_datetimestruct *dts):
234249
"""
235250
Note we are assuming that the datetime object is timezone-naive.
236251
"""
237-
dts.year = PyDateTime_GET_YEAR(val)
238-
dts.month = PyDateTime_GET_MONTH(val)
239-
dts.day = PyDateTime_GET_DAY(val)
240-
dts.hour = PyDateTime_DATE_GET_HOUR(val)
241-
dts.min = PyDateTime_DATE_GET_MINUTE(val)
242-
dts.sec = PyDateTime_DATE_GET_SECOND(val)
243-
dts.us = PyDateTime_DATE_GET_MICROSECOND(val)
244-
dts.ps = dts.as = 0
252+
pydatetime_to_dtstruct(val, dts)
245253
return dtstruct_to_dt64(dts)
246254

247255

pandas/_libs/tslibs/timestamps.pyx

+8-7
Original file line numberDiff line numberDiff line change
@@ -2203,9 +2203,6 @@ default 'raise'
22032203
datetime ts_input
22042204
tzinfo_type tzobj
22052205

2206-
if self._reso != NPY_FR_ns:
2207-
raise NotImplementedError(self._reso)
2208-
22092206
# set to naive if needed
22102207
tzobj = self.tzinfo
22112208
value = self.value
@@ -2218,7 +2215,7 @@ default 'raise'
22182215
value = tz_convert_from_utc_single(value, tzobj, reso=self._reso)
22192216

22202217
# setup components
2221-
dt64_to_dtstruct(value, &dts)
2218+
pandas_datetime_to_datetimestruct(value, self._reso, &dts)
22222219
dts.ps = self.nanosecond * 1000
22232220

22242221
# replace
@@ -2265,12 +2262,16 @@ default 'raise'
22652262
'fold': fold}
22662263
ts_input = datetime(**kwargs)
22672264

2268-
ts = convert_datetime_to_tsobject(ts_input, tzobj)
2265+
ts = convert_datetime_to_tsobject(ts_input, tzobj, nanos=0, reso=self._reso)
2266+
# TODO: passing nanos=dts.ps // 1000 causes a RecursionError in
2267+
# TestTimestampConstructors.test_constructor; not clear why
22692268
value = ts.value + (dts.ps // 1000)
22702269
if value != NPY_NAT:
2271-
check_dts_bounds(&dts)
2270+
check_dts_bounds(&dts, self._reso)
22722271

2273-
return create_timestamp_from_ts(value, dts, tzobj, self._freq, fold)
2272+
return create_timestamp_from_ts(
2273+
value, dts, tzobj, self._freq, fold, reso=self._reso
2274+
)
22742275

22752276
def to_julian_date(self) -> np.float64:
22762277
"""

pandas/tests/scalar/timestamp/test_unary_ops.py

+11
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
iNaT,
2020
to_offset,
2121
)
22+
from pandas._libs.tslibs.dtypes import NpyDatetimeUnit
2223
from pandas._libs.tslibs.period import INVALID_FREQ_ERR_MSG
2324
import pandas.util._test_decorators as td
2425

@@ -338,6 +339,16 @@ def checker(res, ts, nanos):
338339
# --------------------------------------------------------------
339340
# Timestamp.replace
340341

342+
def test_replace_non_nano(self):
343+
ts = Timestamp._from_value_and_reso(
344+
91514880000000000, NpyDatetimeUnit.NPY_FR_us.value, None
345+
)
346+
assert ts.to_pydatetime() == datetime(4869, 12, 28)
347+
348+
result = ts.replace(year=4900)
349+
assert result._reso == ts._reso
350+
assert result.to_pydatetime() == datetime(4900, 12, 28)
351+
341352
def test_replace_naive(self):
342353
# GH#14621, GH#7825
343354
ts = Timestamp("2016-01-01 09:00:00")

0 commit comments

Comments
 (0)