diff --git a/pandas/core/indexes/datetimelike.py b/pandas/core/indexes/datetimelike.py index 7afcb4f5b6978..b953bbba8f78c 100644 --- a/pandas/core/indexes/datetimelike.py +++ b/pandas/core/indexes/datetimelike.py @@ -282,7 +282,7 @@ def _summary(self, name=None) -> str: # -------------------------------------------------------------------- # Indexing Methods - def _validate_partial_date_slice(self, reso: Resolution): + def _can_partial_date_slice(self, reso: Resolution) -> bool: raise NotImplementedError def _parsed_string_to_bounds(self, reso: Resolution, parsed): @@ -317,7 +317,8 @@ def _partial_date_slice( ------- slice or ndarray[intp] """ - self._validate_partial_date_slice(reso) + if not self._can_partial_date_slice(reso): + raise ValueError t1, t2 = self._parsed_string_to_bounds(reso, parsed) vals = self._data._ndarray diff --git a/pandas/core/indexes/datetimes.py b/pandas/core/indexes/datetimes.py index afc9019ba3a60..2a2993dedb25d 100644 --- a/pandas/core/indexes/datetimes.py +++ b/pandas/core/indexes/datetimes.py @@ -603,7 +603,7 @@ def _parsed_string_to_bounds(self, reso: Resolution, parsed: datetime): end = end.tz_localize(self.tz) return start, end - def _validate_partial_date_slice(self, reso: Resolution): + def _can_partial_date_slice(self, reso: Resolution) -> bool: assert isinstance(reso, Resolution), (type(reso), reso) if ( self.is_monotonic @@ -614,12 +614,13 @@ def _validate_partial_date_slice(self, reso: Resolution): # GH3452 and GH2369. # See also GH14826 - raise KeyError + return False if reso.attrname == "microsecond": # _partial_date_slice doesn't allow microsecond resolution, but # _parsed_string_to_bounds allows it. - raise KeyError + return False + return True def _deprecate_mismatched_indexing(self, key) -> None: # GH#36148 @@ -663,14 +664,22 @@ def get_loc(self, key, method=None, tolerance=None): key = self._maybe_cast_for_get_loc(key) elif isinstance(key, str): + try: - return self._get_string_slice(key) - except (TypeError, KeyError, ValueError, OverflowError): - pass + parsed, reso = self._parse_with_reso(key) + except ValueError as err: + raise KeyError(key) from err + if self._can_partial_date_slice(reso): + try: + return self._partial_date_slice(reso, parsed) + except KeyError as err: + if method is None: + raise KeyError(key) from err try: key = self._maybe_cast_for_get_loc(key) except ValueError as err: + # FIXME: we get here because parse_with_reso doesn't raise on "t2m" raise KeyError(key) from err elif isinstance(key, timedelta): diff --git a/pandas/core/indexes/period.py b/pandas/core/indexes/period.py index 2a3a1dce7f585..306695d99bead 100644 --- a/pandas/core/indexes/period.py +++ b/pandas/core/indexes/period.py @@ -15,14 +15,11 @@ ) from pandas._libs.tslibs import ( BaseOffset, + NaT, Period, Resolution, Tick, ) -from pandas._libs.tslibs.parsing import ( - DateParseError, - parse_time_string, -) from pandas._typing import ( Dtype, DtypeObj, @@ -35,6 +32,7 @@ pandas_dtype, ) from pandas.core.dtypes.dtypes import PeriodDtype +from pandas.core.dtypes.missing import is_valid_na_for_dtype from pandas.core.arrays.period import ( PeriodArray, @@ -409,43 +407,50 @@ def get_loc(self, key, method=None, tolerance=None): orig_key = key self._check_indexing_error(key) - if isinstance(key, str): - try: - loc = self._get_string_slice(key) - return loc - except (TypeError, ValueError): - pass + if is_valid_na_for_dtype(key, self.dtype): + key = NaT + + elif isinstance(key, str): try: - asdt, reso_str = parse_time_string(key, self.freq) - except (ValueError, DateParseError) as err: + parsed, reso = self._parse_with_reso(key) + except ValueError as err: # A string with invalid format raise KeyError(f"Cannot interpret '{key}' as period") from err - reso = Resolution.from_attrname(reso_str) + if self._can_partial_date_slice(reso): + try: + return self._partial_date_slice(reso, parsed) + except KeyError as err: + # TODO: pass if method is not None, like DTI does? + raise KeyError(key) from err if reso == self.dtype.resolution: # the reso < self.dtype.resolution case goes through _get_string_slice - key = Period(asdt, freq=self.freq) + key = Period(parsed, freq=self.freq) loc = self.get_loc(key, method=method, tolerance=tolerance) + # Recursing instead of falling through matters for the exception + # message in test_get_loc3 (though not clear if that really matters) return loc elif method is None: raise KeyError(key) else: - key = asdt + key = Period(parsed, freq=self.freq) - elif is_integer(key): - # Period constructor will cast to string, which we dont want - raise KeyError(key) elif isinstance(key, Period) and key.freq != self.freq: raise KeyError(key) - - try: - key = Period(key, freq=self.freq) - except ValueError as err: - # we cannot construct the Period - raise KeyError(orig_key) from err + elif isinstance(key, Period): + pass + elif isinstance(key, datetime): + try: + key = Period(key, freq=self.freq) + except ValueError as err: + # we cannot construct the Period + raise KeyError(orig_key) from err + else: + # in particular integer, which Period constructor would cast to string + raise KeyError(key) try: return Index.get_loc(self, key, method, tolerance) @@ -496,7 +501,7 @@ def _parsed_string_to_bounds(self, reso: Resolution, parsed: datetime): iv = Period(parsed, freq=grp.value) return (iv.asfreq(self.freq, how="start"), iv.asfreq(self.freq, how="end")) - def _validate_partial_date_slice(self, reso: Resolution): + def _can_partial_date_slice(self, reso: Resolution) -> bool: assert isinstance(reso, Resolution), (type(reso), reso) grp = reso.freq_group freqn = self.dtype.freq_group_code @@ -505,7 +510,9 @@ def _validate_partial_date_slice(self, reso: Resolution): # TODO: we used to also check for # reso in ["day", "hour", "minute", "second"] # why is that check not needed? - raise ValueError + return False + + return True def period_range(