diff --git a/pandas/_libs/tslibs/period.pyx b/pandas/_libs/tslibs/period.pyx index fd905248c4558..82a17a6c42018 100644 --- a/pandas/_libs/tslibs/period.pyx +++ b/pandas/_libs/tslibs/period.pyx @@ -812,6 +812,47 @@ cdef int64_t get_period_ordinal(npy_datetimestruct *dts, int freq) noexcept nogi unit = freq_group_code_to_npy_unit(freq) return npy_datetimestruct_to_datetime(unit, dts) +cdef int64_t _period_ordinal_safe(npy_datetimestruct *dts, int freq) except? -1: + """ + Safe variant of get_period_ordinal used by the Python API (period_ordinal). + + It mirrors get_period_ordinal's logic but is allowed to raise Python + exceptions instead of leaking OverflowError from numpy's datetime code. + """ + cdef: + int64_t unix_date + int freq_group, fmonth + NPY_DATETIMEUNIT unit + + freq_group = get_freq_group(freq) + + try: + if freq_group == FR_ANN: + fmonth = get_anchor_month(freq, freq_group) + return dts_to_year_ordinal(dts, fmonth) + + elif freq_group == FR_QTR: + fmonth = get_anchor_month(freq, freq_group) + return dts_to_qtr_ordinal(dts, fmonth) + + elif freq_group == FR_WK: + unix_date = npy_datetimestruct_to_datetime(NPY_FR_D, dts) + return unix_date_to_week(unix_date, freq - FR_WK) + + elif freq == FR_BUS: + unix_date = npy_datetimestruct_to_datetime(NPY_FR_D, dts) + return DtoB(dts, 0, unix_date) + + unit = freq_group_code_to_npy_unit(freq) + return npy_datetimestruct_to_datetime(unit, dts) + + except OverflowError as err: + # Translate low-level overflow into a user-facing OutOfBoundsDatetime. + fmt = dts_to_iso_string(dts) + raise OutOfBoundsDatetime( + f"Out of bounds datetime for Period with freq {freq}: {fmt}" + ) from err + cdef void get_date_info(int64_t ordinal, int freq, npy_datetimestruct *dts) noexcept nogil: @@ -1150,7 +1191,8 @@ cpdef int64_t period_ordinal(int y, int m, int d, int h, int min, dts.sec = s dts.us = us dts.ps = ps - return get_period_ordinal(&dts, freq) + # return get_period_ordinal(&dts, freq) + return _period_ordinal_safe(&dts, freq) cdef int64_t period_ordinal_to_dt64(int64_t ordinal, int freq) except? -1: diff --git a/pandas/tests/tslibs/test_period.py b/pandas/tests/tslibs/test_period.py index 715e2d3da88db..34beee91c8115 100644 --- a/pandas/tests/tslibs/test_period.py +++ b/pandas/tests/tslibs/test_period.py @@ -2,6 +2,7 @@ import pytest from pandas._libs.tslibs import ( + OutOfBoundsDatetime, iNaT, to_offset, ) @@ -12,6 +13,7 @@ period_ordinal, ) +import pandas as pd import pandas._testing as tm @@ -121,3 +123,11 @@ def test_get_period_field_array_raises_on_out_of_range(): msg = "Buffer dtype mismatch, expected 'const int64_t' but got 'double'" with pytest.raises(ValueError, match=msg): get_period_field_arr(-1, np.empty(1), 0) + + +def test_period_from_overflow_timestamp_raises(): + # Construct a deliberately broken Timestamp + ts = pd.Timestamp(pd.Timestamp.min.value, unit="us") + + with pytest.raises(OutOfBoundsDatetime): + pd.Period(ts, freq="us")