|
6 | 6 | timedelta, |
7 | 7 | timezone, |
8 | 8 | ) |
| 9 | +import re |
9 | 10 | import zoneinfo |
10 | 11 |
|
11 | 12 | import dateutil.tz |
@@ -1192,3 +1193,75 @@ def test_timestamp_iso8601_offset_out_of_bounds(tz): |
1192 | 1193 | msg = "Out of bounds nanosecond timestamp" |
1193 | 1194 | with pytest.raises(OutOfBoundsDatetime, match=msg): |
1194 | 1195 | Timestamp("2262-04-11T20:00:00.000000000-11:00", tz=tz) |
| 1196 | + |
| 1197 | + |
| 1198 | +# GH#66510 iNaT == INT64_MIN, one below Timestamp.min. A value that renders or |
| 1199 | +# shifts onto it is not NaT, but is indistinguishable from NaT once stored in a |
| 1200 | +# datetime64 array, so it has to be rejected instead. |
| 1201 | +SENTINEL_UTC = "1677-09-21 00:12:43.145224192" |
| 1202 | +SENTINEL_PLUS_1H = "1677-09-21 01:12:43.145224192" |
| 1203 | +PLUS_1H = timezone(timedelta(hours=1)) |
| 1204 | +MINUS_1H = timezone(timedelta(hours=-1)) |
| 1205 | + |
| 1206 | + |
| 1207 | +def test_constructor_naive_datetime_renders_nat_sentinel(): |
| 1208 | + # GH#66510 dtstruct -> int64 rendering lands on iNaT |
| 1209 | + msg = re.escape(f"Out of bounds nanosecond timestamp: {SENTINEL_UTC}") |
| 1210 | + with pytest.raises(OutOfBoundsDatetime, match=msg): |
| 1211 | + Timestamp(datetime(1677, 9, 21, 0, 12, 43, 145224), nanosecond=192) |
| 1212 | + |
| 1213 | + |
| 1214 | +def test_constructor_datetime64_tz_shift_hits_nat_sentinel(): |
| 1215 | + # GH#66510 np.datetime64 is treated as a wall time; localizing it lands on iNaT |
| 1216 | + msg = re.escape(f"Out of bounds nanosecond timestamp: {SENTINEL_PLUS_1H}") |
| 1217 | + with pytest.raises(OutOfBoundsDatetime, match=msg): |
| 1218 | + Timestamp(np.datetime64(SENTINEL_PLUS_1H.replace(" ", "T")), tz=PLUS_1H) |
| 1219 | + |
| 1220 | + |
| 1221 | +def test_constructor_aware_datetime_offset_hits_nat_sentinel(): |
| 1222 | + # GH#66510 subtracting the UTC offset lands on iNaT |
| 1223 | + msg = re.escape(f"Out of bounds nanosecond timestamp: {SENTINEL_PLUS_1H}") |
| 1224 | + with pytest.raises(OutOfBoundsDatetime, match=msg): |
| 1225 | + Timestamp( |
| 1226 | + datetime(1677, 9, 21, 1, 12, 43, 145224, tzinfo=PLUS_1H), nanosecond=192 |
| 1227 | + ) |
| 1228 | + |
| 1229 | + |
| 1230 | +@pytest.mark.parametrize( |
| 1231 | + "arg, kwargs", |
| 1232 | + [ |
| 1233 | + # the tz keyword shifts via tz_localize_to_utc_single |
| 1234 | + (SENTINEL_PLUS_1H, {"tz": PLUS_1H}), |
| 1235 | + # an embedded offset shifts via the checked_sub fastpath |
| 1236 | + (f"{SENTINEL_PLUS_1H}+01:00", {}), |
| 1237 | + ], |
| 1238 | +) |
| 1239 | +def test_constructor_str_shift_hits_nat_sentinel(arg, kwargs): |
| 1240 | + # GH#66510 both shift paths in convert_str_to_tsobject |
| 1241 | + msg = re.escape(f"Out of bounds nanosecond timestamp: {SENTINEL_PLUS_1H}") |
| 1242 | + with pytest.raises(OutOfBoundsDatetime, match=msg): |
| 1243 | + Timestamp(arg, **kwargs) |
| 1244 | + |
| 1245 | + |
| 1246 | +def test_constructor_aware_datetime_offset_overflow(): |
| 1247 | + # GH#66510 the offset subtraction used to raise a bare OverflowError |
| 1248 | + msg = re.escape("Out of bounds nanosecond timestamp: 2262-04-11 23:47:16.854775807") |
| 1249 | + tz = timezone(timedelta(hours=-23, minutes=-59)) |
| 1250 | + with pytest.raises(OutOfBoundsDatetime, match=msg): |
| 1251 | + Timestamp(datetime(2262, 4, 11, 23, 47, 16, 854775, tzinfo=tz), nanosecond=807) |
| 1252 | + |
| 1253 | + |
| 1254 | +def test_constructor_offset_shifts_off_nat_sentinel(): |
| 1255 | + # GH#66510 the wall time renders onto iNaT but the shift to UTC moves it back |
| 1256 | + # in bounds, so the rejection has to happen after the shift, not before |
| 1257 | + ts = Timestamp( |
| 1258 | + datetime(1677, 9, 21, 0, 12, 43, 145224, tzinfo=MINUS_1H), nanosecond=192 |
| 1259 | + ) |
| 1260 | + assert ts._value == -(2**63) + 3600 * 10**9 |
| 1261 | + |
| 1262 | + |
| 1263 | +def test_constructor_nat_sentinel_neighbours(): |
| 1264 | + # GH#66510 only the sentinel itself is out of bounds |
| 1265 | + assert Timestamp("1677-09-21 00:12:43.145224193")._value == -(2**63) + 1 |
| 1266 | + assert Timestamp.min._value == -(2**63) + 1 |
| 1267 | + assert Timestamp(NaT) is NaT |
0 commit comments