diff --git a/pandas/_libs/tslibs/frequencies.pxd b/pandas/_libs/tslibs/frequencies.pxd index d6bae78576f50..c96661aaab443 100644 --- a/pandas/_libs/tslibs/frequencies.pxd +++ b/pandas/_libs/tslibs/frequencies.pxd @@ -1,3 +1,5 @@ +cdef dict attrname_to_abbrevs + cpdef str get_rule_month(object source, str default=*) cpdef get_freq_code(freqstr) diff --git a/pandas/_libs/tslibs/frequencies.pyx b/pandas/_libs/tslibs/frequencies.pyx index c1f10b3dda612..60f750da92091 100644 --- a/pandas/_libs/tslibs/frequencies.pyx +++ b/pandas/_libs/tslibs/frequencies.pyx @@ -124,8 +124,50 @@ _lite_rule_alias = { _dont_uppercase = {'MS', 'ms'} +# Map attribute-name resolutions to resolution abbreviations +_attrname_to_abbrevs = { + "year": "A", + "quarter": "Q", + "month": "M", + "day": "D", + "hour": "H", + "minute": "T", + "second": "S", + "millisecond": "L", + "microsecond": "U", + "nanosecond": "N", +} +cdef dict attrname_to_abbrevs = _attrname_to_abbrevs + + # ---------------------------------------------------------------------- +def get_freq_group(freq) -> int: + """ + Return frequency code group of given frequency str or offset. + + Examples + -------- + >>> get_freq_group('W-MON') + 4000 + + >>> get_freq_group('W-FRI') + 4000 + """ + if is_offset_object(freq): + freq = freq.rule_code + + if isinstance(freq, str): + freq = attrname_to_abbrevs.get(freq, freq) + base, mult = get_freq_code(freq) + freq = base + elif isinstance(freq, int): + pass + else: + raise ValueError('input must be str, offset or int') + return (freq // 1000) * 1000 + + cpdef get_freq_code(freqstr): """ Return freq str or tuple to freq code and stride (mult) diff --git a/pandas/_libs/tslibs/period.pyx b/pandas/_libs/tslibs/period.pyx index c5be5b1d96469..8af467c3b0950 100644 --- a/pandas/_libs/tslibs/period.pyx +++ b/pandas/_libs/tslibs/period.pyx @@ -53,6 +53,7 @@ from pandas._libs.tslibs.ccalendar cimport ( ) from pandas._libs.tslibs.ccalendar cimport c_MONTH_NUMBERS from pandas._libs.tslibs.frequencies cimport ( + attrname_to_abbrevs, get_base_alias, get_freq_code, get_freq_str, @@ -60,7 +61,6 @@ from pandas._libs.tslibs.frequencies cimport ( get_to_timestamp_base, ) from pandas._libs.tslibs.parsing import parse_time_string -from pandas._libs.tslibs.resolution import Resolution from pandas._libs.tslibs.nattype cimport ( _nat_scalar_rules, NPY_NAT, @@ -708,6 +708,8 @@ cdef char* c_strftime(npy_datetimestruct *dts, char *fmt): # Conversion between date_info and npy_datetimestruct cdef inline int get_freq_group(int freq) nogil: + # Note: this is equivalent to libfrequencies.get_freq_group, specialized + # to integer argument. return (freq // 1000) * 1000 @@ -2431,7 +2433,7 @@ class Period(_Period): if freq is None: try: - freq = Resolution.get_freq(reso) + freq = attrname_to_abbrevs[reso] except KeyError: raise ValueError(f"Invalid frequency or could not " f"infer: {reso}") diff --git a/pandas/_libs/tslibs/resolution.pyx b/pandas/_libs/tslibs/resolution.pyx index 80a3466f5a1aa..3d76483f76600 100644 --- a/pandas/_libs/tslibs/resolution.pyx +++ b/pandas/_libs/tslibs/resolution.pyx @@ -1,11 +1,11 @@ import numpy as np from numpy cimport ndarray, int64_t, int32_t -from pandas._libs.tslibs.util cimport get_nat, is_offset_object +from pandas._libs.tslibs.util cimport get_nat from pandas._libs.tslibs.np_datetime cimport ( npy_datetimestruct, dt64_to_dtstruct) -from pandas._libs.tslibs.frequencies cimport get_freq_code +from pandas._libs.tslibs.frequencies cimport attrname_to_abbrevs from pandas._libs.tslibs.timezones cimport ( is_utc, is_tzlocal, maybe_get_tz, get_dst_info) from pandas._libs.tslibs.ccalendar cimport get_days_in_month @@ -25,6 +25,7 @@ cdef: int RESO_HR = 5 int RESO_DAY = 6 + # ---------------------------------------------------------------------- cpdef resolution(const int64_t[:] stamps, tz=None): @@ -106,31 +107,6 @@ cdef inline int _reso_stamp(npy_datetimestruct *dts): return RESO_DAY -def get_freq_group(freq) -> int: - """ - Return frequency code group of given frequency str or offset. - - Examples - -------- - >>> get_freq_group('W-MON') - 4000 - - >>> get_freq_group('W-FRI') - 4000 - """ - if is_offset_object(freq): - freq = freq.rule_code - - if isinstance(freq, str): - base, mult = get_freq_code(freq) - freq = base - elif isinstance(freq, int): - pass - else: - raise ValueError('input must be str, offset or int') - return (freq // 1000) * 1000 - - class Resolution: # Note: cython won't allow us to reference the cdef versions at the @@ -163,7 +139,7 @@ class Resolution: RESO_HR: 60, RESO_DAY: 24} - _reso_str_bump_map = { + reso_str_bump_map = { 'D': 'H', 'H': 'T', 'T': 'S', @@ -174,19 +150,7 @@ class Resolution: _str_reso_map = {v: k for k, v in _reso_str_map.items()} - _reso_freq_map = { - 'year': 'A', - 'quarter': 'Q', - 'month': 'M', - 'day': 'D', - 'hour': 'H', - 'minute': 'T', - 'second': 'S', - 'millisecond': 'L', - 'microsecond': 'U', - 'nanosecond': 'N'} - - _freq_reso_map = {v: k for k, v in _reso_freq_map.items()} + _freq_reso_map = {v: k for k, v in attrname_to_abbrevs.items()} @classmethod def get_str(cls, reso: int) -> str: @@ -215,30 +179,6 @@ class Resolution: """ return cls._str_reso_map.get(resostr, cls.RESO_DAY) - @classmethod - def get_freq_group(cls, resostr: str) -> int: - """ - Return frequency str against resolution str. - - Examples - -------- - >>> f.Resolution.get_freq_group('day') - 4000 - """ - return get_freq_group(cls.get_freq(resostr)) - - @classmethod - def get_freq(cls, resostr: str) -> str: - """ - Return frequency str against resolution str. - - Examples - -------- - >>> f.Resolution.get_freq('day') - 'D' - """ - return cls._reso_freq_map[resostr] - @classmethod def get_str_from_freq(cls, freq: str) -> str: """ @@ -303,7 +243,7 @@ class Resolution: ) next_value = cls._reso_mult_map[start_reso] * value - next_name = cls._reso_str_bump_map[freq] + next_name = cls.reso_str_bump_map[freq] return cls.get_stride_from_decimal(next_value, next_name) diff --git a/pandas/core/arrays/datetimelike.py b/pandas/core/arrays/datetimelike.py index 145654805cc6b..708b0ea4da96d 100644 --- a/pandas/core/arrays/datetimelike.py +++ b/pandas/core/arrays/datetimelike.py @@ -6,6 +6,7 @@ import numpy as np from pandas._libs import NaT, NaTType, Period, Timestamp, algos, iNaT, lib +from pandas._libs.tslibs.resolution import Resolution from pandas._libs.tslibs.timedeltas import delta_to_nanoseconds from pandas._libs.tslibs.timestamps import ( RoundTo, @@ -1094,14 +1095,14 @@ def inferred_freq(self): @property # NB: override with cache_readonly in immutable subclasses def _resolution(self): - return frequencies.Resolution.get_reso_from_freq(self.freqstr) + return Resolution.get_reso_from_freq(self.freqstr) @property # NB: override with cache_readonly in immutable subclasses def resolution(self) -> str: """ Returns day, hour, minute, second, millisecond or microsecond """ - return frequencies.Resolution.get_str(self._resolution) + return Resolution.get_str(self._resolution) @classmethod def _validate_frequency(cls, index, freq, **kwargs): diff --git a/pandas/core/indexes/base.py b/pandas/core/indexes/base.py index 8751cca97c18c..d9828707b6164 100644 --- a/pandas/core/indexes/base.py +++ b/pandas/core/indexes/base.py @@ -54,7 +54,6 @@ ABCCategorical, ABCDataFrame, ABCDatetimeIndex, - ABCIntervalIndex, ABCMultiIndex, ABCPandasArray, ABCPeriodIndex, diff --git a/pandas/core/indexes/datetimes.py b/pandas/core/indexes/datetimes.py index 5a89c45a3e425..9cdd27143d3cb 100644 --- a/pandas/core/indexes/datetimes.py +++ b/pandas/core/indexes/datetimes.py @@ -5,8 +5,9 @@ import numpy as np -from pandas._libs import NaT, Period, Timestamp, index as libindex, lib, tslib as libts -from pandas._libs.tslibs import fields, parsing, timezones +from pandas._libs import NaT, Period, Timestamp, index as libindex, lib, tslib +from pandas._libs.tslibs import fields, parsing, resolution as libresolution, timezones +from pandas._libs.tslibs.frequencies import get_freq_group from pandas._typing import DtypeObj, Label from pandas.util._decorators import cache_readonly @@ -28,7 +29,7 @@ from pandas.core.indexes.extension import inherit_names import pandas.core.tools.datetimes as tools -from pandas.tseries.frequencies import Resolution, to_offset +from pandas.tseries.frequencies import to_offset from pandas.tseries.offsets import prefix_mapping @@ -323,7 +324,7 @@ def _is_comparable_dtype(self, dtype: DtypeObj) -> bool: def _mpl_repr(self): # how to represent ourselves to matplotlib - return libts.ints_to_pydatetime(self.asi8, self.tz) + return tslib.ints_to_pydatetime(self.asi8, self.tz) @property def _formatter_func(self): @@ -500,7 +501,7 @@ def _parsed_string_to_bounds(self, reso: str, parsed: datetime): if reso not in valid_resos: raise KeyError - grp = Resolution.get_freq_group(reso) + grp = get_freq_group(reso) per = Period(parsed, freq=(grp, 1)) start, end = per.start_time, per.end_time @@ -525,7 +526,7 @@ def _validate_partial_date_slice(self, reso: str): if ( self.is_monotonic and reso in ["day", "hour", "minute", "second"] - and self._resolution >= Resolution.get_reso(reso) + and self._resolution >= libresolution.Resolution.get_reso(reso) ): # These resolution/monotonicity validations came from GH3931, # GH3452 and GH2369. diff --git a/pandas/core/indexes/period.py b/pandas/core/indexes/period.py index be243d7014233..68fe709ba56d8 100644 --- a/pandas/core/indexes/period.py +++ b/pandas/core/indexes/period.py @@ -5,7 +5,8 @@ from pandas._libs import index as libindex from pandas._libs.lib import no_default -from pandas._libs.tslibs import Period, resolution +from pandas._libs.tslibs import Period +from pandas._libs.tslibs.frequencies import get_freq_group from pandas._libs.tslibs.parsing import parse_time_string from pandas._typing import DtypeObj, Label from pandas.util._decorators import Appender, cache_readonly, doc @@ -501,8 +502,8 @@ def get_loc(self, key, method=None, tolerance=None): # A string with invalid format raise KeyError(f"Cannot interpret '{key}' as period") from err - grp = resolution.Resolution.get_freq_group(reso) - freqn = resolution.get_freq_group(self.freq) + grp = get_freq_group(reso) + freqn = get_freq_group(self.freq) # _get_string_slice will handle cases where grp < freqn assert grp >= freqn @@ -573,13 +574,13 @@ def _parsed_string_to_bounds(self, reso: str, parsed: datetime): if reso not in ["year", "month", "quarter", "day", "hour", "minute", "second"]: raise KeyError(reso) - grp = resolution.Resolution.get_freq_group(reso) + grp = get_freq_group(reso) iv = Period(parsed, freq=(grp, 1)) return (iv.asfreq(self.freq, how="start"), iv.asfreq(self.freq, how="end")) def _validate_partial_date_slice(self, reso: str): - grp = resolution.Resolution.get_freq_group(reso) - freqn = resolution.get_freq_group(self.freq) + grp = get_freq_group(reso) + freqn = get_freq_group(self.freq) if not grp < freqn: # TODO: we used to also check for diff --git a/pandas/plotting/_matplotlib/converter.py b/pandas/plotting/_matplotlib/converter.py index 132cbdb160bec..1358ddf7005a3 100644 --- a/pandas/plotting/_matplotlib/converter.py +++ b/pandas/plotting/_matplotlib/converter.py @@ -11,8 +11,7 @@ import numpy as np from pandas._libs import lib, tslibs -from pandas._libs.tslibs import resolution -from pandas._libs.tslibs.frequencies import FreqGroup, get_freq_code +from pandas._libs.tslibs.frequencies import FreqGroup, get_freq_code, get_freq_group from pandas.core.dtypes.common import ( is_datetime64_ns_dtype, @@ -550,7 +549,7 @@ def _daily_finder(vmin, vmax, freq): elif freq == FreqGroup.FR_DAY: periodsperyear = 365 periodspermonth = 28 - elif resolution.get_freq_group(freq) == FreqGroup.FR_WK: + elif get_freq_group(freq) == FreqGroup.FR_WK: periodsperyear = 52 periodspermonth = 3 else: # pragma: no cover @@ -888,7 +887,7 @@ def _annual_finder(vmin, vmax, freq): def get_finder(freq): if isinstance(freq, str): freq = get_freq_code(freq)[0] - fgroup = resolution.get_freq_group(freq) + fgroup = get_freq_group(freq) if fgroup == FreqGroup.FR_ANN: return _annual_finder diff --git a/pandas/tests/tseries/frequencies/test_freq_code.py b/pandas/tests/tseries/frequencies/test_freq_code.py index 273e03925dd36..1c51ad0c45238 100644 --- a/pandas/tests/tseries/frequencies/test_freq_code.py +++ b/pandas/tests/tseries/frequencies/test_freq_code.py @@ -1,7 +1,14 @@ import pytest -from pandas._libs.tslibs import frequencies as libfrequencies, resolution -from pandas._libs.tslibs.frequencies import FreqGroup, _period_code_map, get_freq_code +from pandas._libs.tslibs.frequencies import ( + FreqGroup, + _attrname_to_abbrevs, + _period_code_map, + get_freq_code, + get_freq_group, + get_to_timestamp_base, +) +from pandas._libs.tslibs.resolution import Resolution as _reso import pandas.tseries.offsets as offsets @@ -60,14 +67,14 @@ def test_freq_code_match(period_code_item): ], ) def test_freq_group(freqstr, expected): - assert resolution.get_freq_group(freqstr) == expected + assert get_freq_group(freqstr) == expected def test_freq_group_match(period_code_item): freqstr, code = period_code_item - str_group = resolution.get_freq_group(freqstr) - code_group = resolution.get_freq_group(code) + str_group = get_freq_group(freqstr) + code_group = get_freq_group(code) assert str_group == code_group == code // 1000 * 1000 @@ -77,14 +84,11 @@ def test_freq_group_match(period_code_item): [("D", "D"), ("W", "D"), ("M", "D"), ("S", "S"), ("T", "S"), ("H", "S")], ) def test_get_to_timestamp_base(freqstr, exp_freqstr): - tsb = libfrequencies.get_to_timestamp_base + tsb = get_to_timestamp_base assert tsb(get_freq_code(freqstr)[0]) == get_freq_code(exp_freqstr)[0] -_reso = resolution.Resolution - - @pytest.mark.parametrize( "freqstr,expected", [ @@ -106,13 +110,13 @@ def test_get_str_from_freq(freqstr, expected): @pytest.mark.parametrize("freq", ["A", "Q", "M", "D", "H", "T", "S", "L", "U", "N"]) def test_get_freq_roundtrip(freq): - result = _reso.get_freq(_reso.get_str_from_freq(freq)) + result = _attrname_to_abbrevs[_reso.get_str_from_freq(freq)] assert freq == result @pytest.mark.parametrize("freq", ["D", "H", "T", "S", "L", "U"]) def test_get_freq_roundtrip2(freq): - result = _reso.get_freq(_reso.get_str(_reso.get_reso_from_freq(freq))) + result = _attrname_to_abbrevs[_reso.get_str(_reso.get_reso_from_freq(freq))] assert freq == result diff --git a/pandas/tseries/frequencies.py b/pandas/tseries/frequencies.py index d95ffd5b0876d..f907c5570bd18 100644 --- a/pandas/tseries/frequencies.py +++ b/pandas/tseries/frequencies.py @@ -11,8 +11,7 @@ from pandas._libs.tslibs.fields import build_field_sarray import pandas._libs.tslibs.frequencies as libfreqs from pandas._libs.tslibs.offsets import _offset_to_period_map -import pandas._libs.tslibs.resolution as libresolution -from pandas._libs.tslibs.resolution import Resolution +from pandas._libs.tslibs.resolution import Resolution, month_position_check from pandas._libs.tslibs.timezones import UTC from pandas._libs.tslibs.tzconversion import tz_convert from pandas.util._decorators import cache_readonly @@ -159,7 +158,7 @@ def to_offset(freq) -> Optional[DateOffset]: stride_sign = -1 if stride.startswith("-") else 1 if not stride: stride = 1 - if prefix in Resolution._reso_str_bump_map.keys(): + if prefix in Resolution.reso_str_bump_map: stride, name = Resolution.get_stride_from_decimal( float(stride), prefix ) @@ -403,7 +402,7 @@ def rep_stamp(self): return Timestamp(self.i8values[0]) def month_position_check(self): - return libresolution.month_position_check(self.fields, self.index.dayofweek) + return month_position_check(self.fields, self.index.dayofweek) @cache_readonly def mdiffs(self):