diff --git a/pandas/_libs/tslibs/offsets.pyx b/pandas/_libs/tslibs/offsets.pyx index b804ed883e693..7f7dd62540387 100644 --- a/pandas/_libs/tslibs/offsets.pyx +++ b/pandas/_libs/tslibs/offsets.pyx @@ -3598,9 +3598,10 @@ cpdef to_offset(freq): if not stride: stride = 1 - from .resolution import Resolution # TODO: avoid runtime import + # TODO: avoid runtime import + from .resolution import Resolution, reso_str_bump_map - if prefix in Resolution.reso_str_bump_map: + if prefix in reso_str_bump_map: stride, name = Resolution.get_stride_from_decimal( float(stride), prefix ) diff --git a/pandas/_libs/tslibs/resolution.pyx b/pandas/_libs/tslibs/resolution.pyx index 2133573ee7554..b3fc1e32f68e8 100644 --- a/pandas/_libs/tslibs/resolution.pyx +++ b/pandas/_libs/tslibs/resolution.pyx @@ -1,3 +1,5 @@ +from enum import Enum + import numpy as np from numpy cimport ndarray, int64_t, int32_t @@ -25,10 +27,46 @@ cdef: int RESO_HR = 5 int RESO_DAY = 6 +reso_str_bump_map = { + "D": "H", + "H": "T", + "T": "S", + "S": "L", + "L": "U", + "U": "N", + "N": None, +} + +_abbrev_to_attrnames = {v: k for k, v in attrname_to_abbrevs.items()} + +_reso_str_map = { + RESO_NS: "nanosecond", + RESO_US: "microsecond", + RESO_MS: "millisecond", + RESO_SEC: "second", + RESO_MIN: "minute", + RESO_HR: "hour", + RESO_DAY: "day", +} + +_str_reso_map = {v: k for k, v in _reso_str_map.items()} + +# factor to multiply a value by to convert it to the next finer grained +# resolution +_reso_mult_map = { + RESO_NS: None, + RESO_US: 1000, + RESO_MS: 1000, + RESO_SEC: 1000, + RESO_MIN: 60, + RESO_HR: 60, + RESO_DAY: 24, +} # ---------------------------------------------------------------------- -def resolution(const int64_t[:] stamps, tz=None): + +def get_resolution(const int64_t[:] stamps, tz=None): cdef: Py_ssize_t i, n = len(stamps) npy_datetimestruct dts @@ -82,7 +120,7 @@ def resolution(const int64_t[:] stamps, tz=None): if curr_reso < reso: reso = curr_reso - return reso + return Resolution(reso) cdef inline int _reso_stamp(npy_datetimestruct *dts): @@ -99,7 +137,7 @@ cdef inline int _reso_stamp(npy_datetimestruct *dts): return RESO_DAY -class Resolution: +class Resolution(Enum): # Note: cython won't allow us to reference the cdef versions at the # module level @@ -111,41 +149,14 @@ class Resolution: RESO_HR = 5 RESO_DAY = 6 - _reso_str_map = { - RESO_NS: 'nanosecond', - RESO_US: 'microsecond', - RESO_MS: 'millisecond', - RESO_SEC: 'second', - RESO_MIN: 'minute', - RESO_HR: 'hour', - RESO_DAY: 'day'} - - # factor to multiply a value by to convert it to the next finer grained - # resolution - _reso_mult_map = { - RESO_NS: None, - RESO_US: 1000, - RESO_MS: 1000, - RESO_SEC: 1000, - RESO_MIN: 60, - RESO_HR: 60, - RESO_DAY: 24} - - reso_str_bump_map = { - 'D': 'H', - 'H': 'T', - 'T': 'S', - 'S': 'L', - 'L': 'U', - 'U': 'N', - 'N': None} - - _str_reso_map = {v: k for k, v in _reso_str_map.items()} - - _freq_reso_map = {v: k for k, v in attrname_to_abbrevs.items()} + def __lt__(self, other): + return self.value < other.value + + def __ge__(self, other): + return self.value >= other.value @classmethod - def get_str(cls, reso: int) -> str: + def get_str(cls, reso: "Resolution") -> str: """ Return resolution str against resolution code. @@ -154,10 +165,10 @@ class Resolution: >>> Resolution.get_str(Resolution.RESO_SEC) 'second' """ - return cls._reso_str_map.get(reso, 'day') + return _reso_str_map[reso.value] @classmethod - def get_reso(cls, resostr: str) -> int: + def get_reso(cls, resostr: str) -> "Resolution": """ Return resolution str against resolution code. @@ -169,25 +180,27 @@ class Resolution: >>> Resolution.get_reso('second') == Resolution.RESO_SEC True """ - return cls._str_reso_map.get(resostr, cls.RESO_DAY) + return cls(_str_reso_map[resostr]) @classmethod - def get_str_from_freq(cls, freq: str) -> str: + def get_attrname_from_abbrev(cls, freq: str) -> str: """ Return resolution str against frequency str. Examples -------- - >>> Resolution.get_str_from_freq('H') + >>> Resolution.get_attrname_from_abbrev('H') 'hour' """ - return cls._freq_reso_map.get(freq, 'day') + return _abbrev_to_attrnames[freq] @classmethod - def get_reso_from_freq(cls, freq: str) -> int: + def get_reso_from_freq(cls, freq: str) -> "Resolution": """ Return resolution code against frequency str. + `freq` is given by the `offset.freqstr` for some DateOffset object. + Examples -------- >>> Resolution.get_reso_from_freq('H') @@ -196,16 +209,16 @@ class Resolution: >>> Resolution.get_reso_from_freq('H') == Resolution.RESO_HR True """ - return cls.get_reso(cls.get_str_from_freq(freq)) + return cls.get_reso(cls.get_attrname_from_abbrev(freq)) @classmethod - def get_stride_from_decimal(cls, value, freq): + def get_stride_from_decimal(cls, value: float, freq: str): """ Convert freq with decimal stride into a higher freq with integer stride Parameters ---------- - value : int or float + value : float freq : str Frequency string @@ -229,13 +242,13 @@ class Resolution: return int(value), freq else: start_reso = cls.get_reso_from_freq(freq) - if start_reso == 0: + if start_reso.value == 0: raise ValueError( "Could not convert to integer offset at any resolution" ) - next_value = cls._reso_mult_map[start_reso] * value - next_name = cls.reso_str_bump_map[freq] + next_value = _reso_mult_map[start_reso.value] * value + next_name = 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 cf3cde155a3bb..b9f712e4d64fe 100644 --- a/pandas/core/arrays/datetimelike.py +++ b/pandas/core/arrays/datetimelike.py @@ -1,6 +1,6 @@ from datetime import datetime, timedelta import operator -from typing import Any, Callable, Sequence, Tuple, Type, TypeVar, Union, cast +from typing import Any, Callable, Optional, Sequence, Tuple, Type, TypeVar, Union, cast import warnings import numpy as np @@ -804,7 +804,7 @@ def _validate_scalar(self, value, msg: str, cast_str: bool = False): return value def _validate_listlike( - self, value, opname: str, cast_str: bool = False, allow_object: bool = False, + self, value, opname: str, cast_str: bool = False, allow_object: bool = False ): if isinstance(value, type(self)): return value @@ -1103,14 +1103,22 @@ def inferred_freq(self): return None @property # NB: override with cache_readonly in immutable subclasses - def _resolution(self): - return Resolution.get_reso_from_freq(self.freqstr) + def _resolution(self) -> Optional[Resolution]: + try: + return Resolution.get_reso_from_freq(self.freqstr) + except KeyError: + return None @property # NB: override with cache_readonly in immutable subclasses def resolution(self) -> str: """ Returns day, hour, minute, second, millisecond or microsecond """ + if self._resolution is None: + if is_period_dtype(self.dtype): + # somewhere in the past it was decided we default to day + return "day" + # otherwise we fall through and will raise return Resolution.get_str(self._resolution) @classmethod diff --git a/pandas/core/arrays/datetimes.py b/pandas/core/arrays/datetimes.py index 894a519cb693e..4e31477571a5f 100644 --- a/pandas/core/arrays/datetimes.py +++ b/pandas/core/arrays/datetimes.py @@ -538,8 +538,8 @@ def is_normalized(self): return conversion.is_date_array_normalized(self.asi8, self.tz) @property # NB: override with cache_readonly in immutable subclasses - def _resolution(self): - return libresolution.resolution(self.asi8, self.tz) + def _resolution(self) -> libresolution.Resolution: + return libresolution.get_resolution(self.asi8, self.tz) # ---------------------------------------------------------------- # Array-Like / EA-Interface Methods diff --git a/pandas/tests/tseries/frequencies/test_freq_code.py b/pandas/tests/tseries/frequencies/test_freq_code.py index 1c51ad0c45238..51554854378ea 100644 --- a/pandas/tests/tseries/frequencies/test_freq_code.py +++ b/pandas/tests/tseries/frequencies/test_freq_code.py @@ -104,13 +104,13 @@ def test_get_to_timestamp_base(freqstr, exp_freqstr): ("N", "nanosecond"), ], ) -def test_get_str_from_freq(freqstr, expected): - assert _reso.get_str_from_freq(freqstr) == expected +def test_get_attrname_from_abbrev(freqstr, expected): + assert _reso.get_attrname_from_abbrev(freqstr) == expected @pytest.mark.parametrize("freq", ["A", "Q", "M", "D", "H", "T", "S", "L", "U", "N"]) def test_get_freq_roundtrip(freq): - result = _attrname_to_abbrevs[_reso.get_str_from_freq(freq)] + result = _attrname_to_abbrevs[_reso.get_attrname_from_abbrev(freq)] assert freq == result