diff --git a/pandas/_libs/tslibs/period.pyx b/pandas/_libs/tslibs/period.pyx index 1ad2688b70a03..7f5305065382c 100644 --- a/pandas/_libs/tslibs/period.pyx +++ b/pandas/_libs/tslibs/period.pyx @@ -1455,7 +1455,9 @@ def extract_ordinals(object[:] values, freq): ordinals[i] = p.ordinal if p.freqstr != freqstr: - msg = DIFFERENT_FREQ_INDEX.format(freqstr, p.freqstr) + msg = DIFFERENT_FREQ.format(cls="PeriodIndex", + own_freq=freqstr, + other_freq=p.freqstr) raise IncompatibleFrequency(msg) except AttributeError: @@ -1545,9 +1547,8 @@ cdef int64_t[:] localize_dt64arr_to_period(int64_t[:] stamps, return result -_DIFFERENT_FREQ = "Input has different freq={1} from Period(freq={0})" -DIFFERENT_FREQ_INDEX = ("Input has different freq={1} " - "from PeriodIndex(freq={0})") +DIFFERENT_FREQ = ("Input has different freq={other_freq} " + "from {cls}(freq={own_freq})") class IncompatibleFrequency(ValueError): @@ -1596,7 +1597,9 @@ cdef class _Period(object): def __richcmp__(self, other, op): if is_period_object(other): if other.freq != self.freq: - msg = _DIFFERENT_FREQ.format(self.freqstr, other.freqstr) + msg = DIFFERENT_FREQ.format(cls=type(self).__name__, + own_freq=self.freqstr, + other_freq=other.freqstr) raise IncompatibleFrequency(msg) return PyObject_RichCompareBool(self.ordinal, other.ordinal, op) elif other is NaT: @@ -1637,7 +1640,9 @@ cdef class _Period(object): if base == self.freq.rule_code: ordinal = self.ordinal + other.n return Period(ordinal=ordinal, freq=self.freq) - msg = _DIFFERENT_FREQ.format(self.freqstr, other.freqstr) + msg = DIFFERENT_FREQ.format(cls=type(self).__name__, + own_freq=self.freqstr, + other_freq=other.freqstr) raise IncompatibleFrequency(msg) else: # pragma no cover return NotImplemented @@ -1684,7 +1689,9 @@ cdef class _Period(object): return Period(ordinal=ordinal, freq=self.freq) elif is_period_object(other): if other.freq != self.freq: - msg = _DIFFERENT_FREQ.format(self.freqstr, other.freqstr) + msg = DIFFERENT_FREQ.format(cls=type(self).__name__, + own_freq=self.freqstr, + other_freq=other.freqstr) raise IncompatibleFrequency(msg) # GH 23915 - mul by base freq since __add__ is agnostic of n return (self.ordinal - other.ordinal) * self.freq.base diff --git a/pandas/core/arrays/datetimelike.py b/pandas/core/arrays/datetimelike.py index fa6941476522d..6d8e41900ce2d 100644 --- a/pandas/core/arrays/datetimelike.py +++ b/pandas/core/arrays/datetimelike.py @@ -7,7 +7,7 @@ from pandas._libs import NaT, algos, iNaT, lib from pandas._libs.tslibs.period import ( - DIFFERENT_FREQ_INDEX, IncompatibleFrequency, Period) + DIFFERENT_FREQ, IncompatibleFrequency, Period) from pandas._libs.tslibs.timedeltas import Timedelta, delta_to_nanoseconds from pandas._libs.tslibs.timestamps import ( RoundTo, maybe_integer_op_deprecated, round_nsint64) @@ -736,7 +736,9 @@ def _sub_period_array(self, other): raise ValueError("cannot subtract arrays/indices of " "unequal length") if self.freq != other.freq: - msg = DIFFERENT_FREQ_INDEX.format(self.freqstr, other.freqstr) + msg = DIFFERENT_FREQ.format(cls=type(self).__name__, + own_freq=self.freqstr, + other_freq=other.freqstr) raise IncompatibleFrequency(msg) new_values = checked_add_with_arr(self.asi8, -other.asi8, diff --git a/pandas/core/arrays/period.py b/pandas/core/arrays/period.py index 178efbe6f8376..60febc5f5636d 100644 --- a/pandas/core/arrays/period.py +++ b/pandas/core/arrays/period.py @@ -7,7 +7,7 @@ from pandas._libs.tslibs import NaT, iNaT, period as libperiod from pandas._libs.tslibs.fields import isleapyear_arr from pandas._libs.tslibs.period import ( - DIFFERENT_FREQ_INDEX, IncompatibleFrequency, Period, get_period_field_arr, + DIFFERENT_FREQ, IncompatibleFrequency, Period, get_period_field_arr, period_asfreq_arr) from pandas._libs.tslibs.timedeltas import Timedelta, delta_to_nanoseconds import pandas.compat as compat @@ -30,7 +30,7 @@ from pandas.core.missing import backfill_1d, pad_1d from pandas.tseries import frequencies -from pandas.tseries.offsets import Tick +from pandas.tseries.offsets import DateOffset, Tick, _delta_to_tick def _field_accessor(name, alias, docstring=None): @@ -166,8 +166,9 @@ def __init__(self, values, freq=None, dtype=None, copy=False): if isinstance(values, type(self)): if freq is not None and freq != values.freq: - msg = DIFFERENT_FREQ_INDEX.format(values.freq.freqstr, - freq.freqstr) + msg = DIFFERENT_FREQ.format(cls=type(self).__name__, + own_freq=values.freq.freqstr, + other_freq=freq.freqstr) raise IncompatibleFrequency(msg) values, freq = values._data, values.freq @@ -239,8 +240,7 @@ def _generate_range(cls, start, end, periods, freq, fields): def _check_compatible_with(self, other): if self.freqstr != other.freqstr: - msg = DIFFERENT_FREQ_INDEX.format(self.freqstr, other.freqstr) - raise IncompatibleFrequency(msg) + _raise_on_incompatible(self, other) # -------------------------------------------------------------------- # Data / Attributes @@ -372,15 +372,13 @@ def __setitem__( value = period_array(value) if self.freqstr != value.freqstr: - msg = DIFFERENT_FREQ_INDEX.format(self.freqstr, value.freqstr) - raise IncompatibleFrequency(msg) + _raise_on_incompatible(self, value) value = value.asi8 elif isinstance(value, Period): if self.freqstr != value.freqstr: - msg = DIFFERENT_FREQ_INDEX.format(self.freqstr, value.freqstr) - raise IncompatibleFrequency(msg) + _raise_on_incompatible(self, value) value = value.ordinal elif isna(value): @@ -696,8 +694,7 @@ def _add_offset(self, other): assert not isinstance(other, Tick) base = frequencies.get_base_alias(other.rule_code) if base != self.freq.rule_code: - msg = DIFFERENT_FREQ_INDEX.format(self.freqstr, other.freqstr) - raise IncompatibleFrequency(msg) + _raise_on_incompatible(self, other) # Note: when calling parent class's _add_timedeltalike_scalar, # it will call delta_to_nanoseconds(delta). Because delta here @@ -760,10 +757,7 @@ def _add_delta(self, other): """ if not isinstance(self.freq, Tick): # We cannot add timedelta-like to non-tick PeriodArray - raise IncompatibleFrequency("Input has different freq from " - "{cls}(freq={freqstr})" - .format(cls=type(self).__name__, - freqstr=self.freqstr)) + _raise_on_incompatible(self, other) new_ordinals = super(PeriodArray, self)._add_delta(other) return type(self)(new_ordinals, freq=self.freq) @@ -815,10 +809,7 @@ def _check_timedeltalike_freq_compat(self, other): # by which will be added to self. return delta - raise IncompatibleFrequency("Input has different freq from " - "{cls}(freq={freqstr})" - .format(cls=type(self).__name__, - freqstr=self.freqstr)) + _raise_on_incompatible(self, other) def _values_for_argsort(self): return self._data @@ -827,6 +818,34 @@ def _values_for_argsort(self): PeriodArray._add_comparison_ops() +def _raise_on_incompatible(left, right): + """ + Helper function to render a consistent error message when raising + IncompatibleFrequency. + + Parameters + ---------- + left : PeriodArray + right : DateOffset, Period, ndarray, or timedelta-like + + Raises + ------ + IncompatibleFrequency + """ + # GH#24283 error message format depends on whether right is scalar + if isinstance(right, np.ndarray): + other_freq = None + elif isinstance(right, (ABCPeriodIndex, PeriodArray, Period, DateOffset)): + other_freq = right.freqstr + else: + other_freq = _delta_to_tick(Timedelta(right)).freqstr + + msg = DIFFERENT_FREQ.format(cls=type(left).__name__, + own_freq=left.freqstr, + other_freq=other_freq) + raise IncompatibleFrequency(msg) + + # ------------------------------------------------------------------- # Constructor Helpers diff --git a/pandas/core/indexes/period.py b/pandas/core/indexes/period.py index dc1cb29c1ae59..7188c1e059125 100644 --- a/pandas/core/indexes/period.py +++ b/pandas/core/indexes/period.py @@ -7,7 +7,7 @@ from pandas._libs import index as libindex from pandas._libs.tslibs import NaT, iNaT, resolution from pandas._libs.tslibs.period import ( - DIFFERENT_FREQ_INDEX, IncompatibleFrequency, Period) + DIFFERENT_FREQ, IncompatibleFrequency, Period) from pandas.util._decorators import ( Appender, Substitution, cache_readonly, deprecate_kwarg) @@ -367,7 +367,10 @@ def _maybe_convert_timedelta(self, other): base = frequencies.get_base_alias(freqstr) if base == self.freq.rule_code: return other.n - msg = DIFFERENT_FREQ_INDEX.format(self.freqstr, other.freqstr) + + msg = DIFFERENT_FREQ.format(cls=type(self).__name__, + own_freq=self.freqstr, + other_freq=other.freqstr) raise IncompatibleFrequency(msg) elif is_integer(other): # integer is passed to .shift via @@ -376,9 +379,10 @@ def _maybe_convert_timedelta(self, other): return other # raise when input doesn't have freq - msg = "Input has different freq from {cls}(freq={freqstr})" - raise IncompatibleFrequency(msg.format(cls=type(self).__name__, - freqstr=self.freqstr)) + msg = DIFFERENT_FREQ.format(cls=type(self).__name__, + own_freq=self.freqstr, + other_freq=None) + raise IncompatibleFrequency(msg) # ------------------------------------------------------------------------ # Rendering Methods @@ -547,7 +551,9 @@ def astype(self, dtype, copy=True, how='start'): def searchsorted(self, value, side='left', sorter=None): if isinstance(value, Period): if value.freq != self.freq: - msg = DIFFERENT_FREQ_INDEX.format(self.freqstr, value.freqstr) + msg = DIFFERENT_FREQ.format(cls=type(self).__name__, + own_freq=self.freqstr, + other_freq=value.freqstr) raise IncompatibleFrequency(msg) value = value.ordinal elif isinstance(value, compat.string_types): @@ -631,7 +637,9 @@ def get_indexer(self, target, method=None, limit=None, tolerance=None): target = ensure_index(target) if hasattr(target, 'freq') and target.freq != self.freq: - msg = DIFFERENT_FREQ_INDEX.format(self.freqstr, target.freqstr) + msg = DIFFERENT_FREQ.format(cls=type(self).__name__, + own_freq=self.freqstr, + other_freq=target.freqstr) raise IncompatibleFrequency(msg) if isinstance(target, PeriodIndex): @@ -819,7 +827,9 @@ def _assert_can_do_setop(self, other): raise ValueError('can only call with other PeriodIndex-ed objects') if self.freq != other.freq: - msg = DIFFERENT_FREQ_INDEX.format(self.freqstr, other.freqstr) + msg = DIFFERENT_FREQ.format(cls=type(self).__name__, + own_freq=self.freqstr, + other_freq=other.freqstr) raise IncompatibleFrequency(msg) def _wrap_setop_result(self, other, result): diff --git a/pandas/tests/arithmetic/test_period.py b/pandas/tests/arithmetic/test_period.py index 9f436281de0a0..6b722f72fd2a8 100644 --- a/pandas/tests/arithmetic/test_period.py +++ b/pandas/tests/arithmetic/test_period.py @@ -206,7 +206,7 @@ def test_pi_cmp_nat_mismatched_freq_raises(self, freq): idx1 = PeriodIndex(['2011-01', '2011-02', 'NaT', '2011-05'], freq=freq) diff = PeriodIndex(['2011-02', '2011-01', '2011-04', 'NaT'], freq='4M') - msg = "Input has different freq=4M from PeriodIndex" + msg = "Input has different freq=4M from Period(Array|Index)" with pytest.raises(IncompatibleFrequency, match=msg): idx1 > diff @@ -1060,14 +1060,15 @@ def test_pi_offset_errors(self): # Series op is applied per Period instance, thus error is raised # from Period - msg = r"Input has different freq from Period.*?\(freq=D\)" for obj in [idx, ser]: + msg = r"Input has different freq=2H from Period.*?\(freq=D\)" with pytest.raises(IncompatibleFrequency, match=msg): obj + pd.offsets.Hour(2) with pytest.raises(IncompatibleFrequency, match=msg): pd.offsets.Hour(2) + obj + msg = r"Input has different freq=-2H from Period.*?\(freq=D\)" with pytest.raises(IncompatibleFrequency, match=msg): obj - pd.offsets.Hour(2) diff --git a/pandas/tests/indexes/period/test_indexing.py b/pandas/tests/indexes/period/test_indexing.py index 29b96604b7ea8..8e7d719dd4c84 100644 --- a/pandas/tests/indexes/period/test_indexing.py +++ b/pandas/tests/indexes/period/test_indexing.py @@ -577,7 +577,7 @@ def test_get_loc2(self): with pytest.raises(ValueError, match=msg): idx.get_loc('2000-01-10', method='nearest', tolerance='foo') - msg = 'Input has different freq from PeriodArray\\(freq=D\\)' + msg = 'Input has different freq=None from PeriodArray\\(freq=D\\)' with pytest.raises(ValueError, match=msg): idx.get_loc('2000-01-10', method='nearest', tolerance='1 hour') with pytest.raises(KeyError): @@ -607,7 +607,7 @@ def test_get_indexer2(self): tolerance='1 hour'), np.array([0, -1, 1], dtype=np.intp)) - msg = 'Input has different freq from PeriodArray\\(freq=H\\)' + msg = 'Input has different freq=None from PeriodArray\\(freq=H\\)' with pytest.raises(ValueError, match=msg): idx.get_indexer(target, 'nearest', tolerance='1 minute') @@ -626,7 +626,7 @@ def test_get_indexer2(self): np.timedelta64(1, 'M'), ] with pytest.raises( libperiod.IncompatibleFrequency, - match='Input has different freq from'): + match='Input has different freq=None from'): idx.get_indexer(target, 'nearest', tolerance=tol_bad) def test_indexing(self):