Skip to content

Commit 96b036c

Browse files
authored
ENH: Timestamp.min/max/resolution support non-nano (pandas-dev#47720)
1 parent bd31d64 commit 96b036c

File tree

4 files changed

+105
-16
lines changed

4 files changed

+105
-16
lines changed

pandas/_libs/tslibs/np_datetime.pyx

+8-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
cimport cython
22
from cpython.datetime cimport (
3+
PyDateTime_CheckExact,
34
PyDateTime_DATE_GET_HOUR,
45
PyDateTime_DATE_GET_MICROSECOND,
56
PyDateTime_DATE_GET_MINUTE,
@@ -229,7 +230,13 @@ def py_td64_to_tdstruct(int64_t td64, NPY_DATETIMEUNIT unit):
229230

230231

231232
cdef inline void pydatetime_to_dtstruct(datetime dt, npy_datetimestruct *dts):
232-
dts.year = PyDateTime_GET_YEAR(dt)
233+
if PyDateTime_CheckExact(dt):
234+
dts.year = PyDateTime_GET_YEAR(dt)
235+
else:
236+
# We use dt.year instead of PyDateTime_GET_YEAR because with Timestamp
237+
# we override year such that PyDateTime_GET_YEAR is incorrect.
238+
dts.year = dt.year
239+
233240
dts.month = PyDateTime_GET_MONTH(dt)
234241
dts.day = PyDateTime_GET_DAY(dt)
235242
dts.hour = PyDateTime_DATE_GET_HOUR(dt)

pandas/_libs/tslibs/timestamps.pxd

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ cdef _Timestamp create_timestamp_from_ts(int64_t value,
2222

2323
cdef class _Timestamp(ABCTimestamp):
2424
cdef readonly:
25-
int64_t value, nanosecond
25+
int64_t value, nanosecond, year
2626
BaseOffset _freq
2727
NPY_DATETIMEUNIT _reso
2828

pandas/_libs/tslibs/timestamps.pyx

+67-14
Original file line numberDiff line numberDiff line change
@@ -144,12 +144,27 @@ cdef inline _Timestamp create_timestamp_from_ts(
144144
""" convenience routine to construct a Timestamp from its parts """
145145
cdef:
146146
_Timestamp ts_base
147-
148-
ts_base = _Timestamp.__new__(Timestamp, dts.year, dts.month,
147+
int64_t pass_year = dts.year
148+
149+
# We pass year=1970/1972 here and set year below because with non-nanosecond
150+
# resolution we may have datetimes outside of the stdlib pydatetime
151+
# implementation bounds, which would raise.
152+
# NB: this means the C-API macro PyDateTime_GET_YEAR is unreliable.
153+
if 1 <= pass_year <= 9999:
154+
# we are in-bounds for pydatetime
155+
pass
156+
elif ccalendar.is_leapyear(dts.year):
157+
pass_year = 1972
158+
else:
159+
pass_year = 1970
160+
161+
ts_base = _Timestamp.__new__(Timestamp, pass_year, dts.month,
149162
dts.day, dts.hour, dts.min,
150163
dts.sec, dts.us, tz, fold=fold)
164+
151165
ts_base.value = value
152166
ts_base._freq = freq
167+
ts_base.year = dts.year
153168
ts_base.nanosecond = dts.ps // 1000
154169
ts_base._reso = reso
155170

@@ -180,6 +195,40 @@ def integer_op_not_supported(obj):
180195
return TypeError(int_addsub_msg)
181196

182197

198+
class MinMaxReso:
199+
"""
200+
We need to define min/max/resolution on both the Timestamp _instance_
201+
and Timestamp class. On an instance, these depend on the object's _reso.
202+
On the class, we default to the values we would get with nanosecond _reso.
203+
204+
See also: timedeltas.MinMaxReso
205+
"""
206+
def __init__(self, name):
207+
self._name = name
208+
209+
def __get__(self, obj, type=None):
210+
cls = Timestamp
211+
if self._name == "min":
212+
val = np.iinfo(np.int64).min + 1
213+
elif self._name == "max":
214+
val = np.iinfo(np.int64).max
215+
else:
216+
assert self._name == "resolution"
217+
val = 1
218+
cls = Timedelta
219+
220+
if obj is None:
221+
# i.e. this is on the class, default to nanos
222+
return cls(val)
223+
elif self._name == "resolution":
224+
return Timedelta._from_value_and_reso(val, obj._reso)
225+
else:
226+
return Timestamp._from_value_and_reso(val, obj._reso, tz=None)
227+
228+
def __set__(self, obj, value):
229+
raise AttributeError(f"{self._name} is not settable.")
230+
231+
183232
# ----------------------------------------------------------------------
184233

185234
cdef class _Timestamp(ABCTimestamp):
@@ -189,6 +238,10 @@ cdef class _Timestamp(ABCTimestamp):
189238
dayofweek = _Timestamp.day_of_week
190239
dayofyear = _Timestamp.day_of_year
191240

241+
min = MinMaxReso("min")
242+
max = MinMaxReso("max")
243+
resolution = MinMaxReso("resolution") # GH#21336, GH#21365
244+
192245
cpdef void _set_freq(self, freq):
193246
# set the ._freq attribute without going through the constructor,
194247
# which would issue a warning
@@ -249,10 +302,12 @@ cdef class _Timestamp(ABCTimestamp):
249302
def __hash__(_Timestamp self):
250303
if self.nanosecond:
251304
return hash(self.value)
305+
if not (1 <= self.year <= 9999):
306+
# out of bounds for pydatetime
307+
return hash(self.value)
252308
if self.fold:
253309
return datetime.__hash__(self.replace(fold=0))
254310
return datetime.__hash__(self)
255-
# TODO(non-nano): what if we are out of bounds for pydatetime?
256311

257312
def __richcmp__(_Timestamp self, object other, int op):
258313
cdef:
@@ -969,6 +1024,9 @@ cdef class _Timestamp(ABCTimestamp):
9691024
"""
9701025
base_ts = "microseconds" if timespec == "nanoseconds" else timespec
9711026
base = super(_Timestamp, self).isoformat(sep=sep, timespec=base_ts)
1027+
# We need to replace the fake year 1970 with our real year
1028+
base = f"{self.year}-" + base.split("-", 1)[1]
1029+
9721030
if self.nanosecond == 0 and timespec != "nanoseconds":
9731031
return base
9741032

@@ -2318,29 +2376,24 @@ default 'raise'
23182376
Return the day of the week represented by the date.
23192377
Monday == 1 ... Sunday == 7.
23202378
"""
2321-
return super().isoweekday()
2379+
# same as super().isoweekday(), but that breaks because of how
2380+
# we have overriden year, see note in create_timestamp_from_ts
2381+
return self.weekday() + 1
23222382

23232383
def weekday(self):
23242384
"""
23252385
Return the day of the week represented by the date.
23262386
Monday == 0 ... Sunday == 6.
23272387
"""
2328-
return super().weekday()
2388+
# same as super().weekday(), but that breaks because of how
2389+
# we have overriden year, see note in create_timestamp_from_ts
2390+
return ccalendar.dayofweek(self.year, self.month, self.day)
23292391

23302392

23312393
# Aliases
23322394
Timestamp.weekofyear = Timestamp.week
23332395
Timestamp.daysinmonth = Timestamp.days_in_month
23342396

2335-
# Add the min and max fields at the class level
2336-
cdef int64_t _NS_UPPER_BOUND = np.iinfo(np.int64).max
2337-
cdef int64_t _NS_LOWER_BOUND = NPY_NAT + 1
2338-
2339-
# Resolution is in nanoseconds
2340-
Timestamp.min = Timestamp(_NS_LOWER_BOUND)
2341-
Timestamp.max = Timestamp(_NS_UPPER_BOUND)
2342-
Timestamp.resolution = Timedelta(nanoseconds=1) # GH#21336, GH#21365
2343-
23442397

23452398
# ----------------------------------------------------------------------
23462399
# Scalar analogues to functions in vectorized.pyx

pandas/tests/scalar/timestamp/test_timestamp.py

+29
Original file line numberDiff line numberDiff line change
@@ -1011,6 +1011,35 @@ def test_sub_timedeltalike_mismatched_reso(self, ts_tz):
10111011
# With a mismatched td64 as opposed to Timedelta
10121012
ts + np.timedelta64(1, "ns")
10131013

1014+
def test_min(self, ts):
1015+
assert ts.min <= ts
1016+
assert ts.min._reso == ts._reso
1017+
assert ts.min.value == NaT.value + 1
1018+
1019+
def test_max(self, ts):
1020+
assert ts.max >= ts
1021+
assert ts.max._reso == ts._reso
1022+
assert ts.max.value == np.iinfo(np.int64).max
1023+
1024+
def test_resolution(self, ts):
1025+
expected = Timedelta._from_value_and_reso(1, ts._reso)
1026+
result = ts.resolution
1027+
assert result == expected
1028+
assert result._reso == expected._reso
1029+
1030+
1031+
def test_timestamp_class_min_max_resolution():
1032+
# when accessed on the class (as opposed to an instance), we default
1033+
# to nanoseconds
1034+
assert Timestamp.min == Timestamp(NaT.value + 1)
1035+
assert Timestamp.min._reso == NpyDatetimeUnit.NPY_FR_ns.value
1036+
1037+
assert Timestamp.max == Timestamp(np.iinfo(np.int64).max)
1038+
assert Timestamp.max._reso == NpyDatetimeUnit.NPY_FR_ns.value
1039+
1040+
assert Timestamp.resolution == Timedelta(1)
1041+
assert Timestamp.resolution._reso == NpyDatetimeUnit.NPY_FR_ns.value
1042+
10141043

10151044
class TestAsUnit:
10161045
def test_as_unit(self):

0 commit comments

Comments
 (0)