Skip to content

Commit f7681d3

Browse files
authored
REF: make Resolution an enum (#34462)
1 parent 4679714 commit f7681d3

File tree

5 files changed

+82
-60
lines changed

5 files changed

+82
-60
lines changed

pandas/_libs/tslibs/offsets.pyx

+3-2
Original file line numberDiff line numberDiff line change
@@ -3598,9 +3598,10 @@ cpdef to_offset(freq):
35983598
if not stride:
35993599
stride = 1
36003600

3601-
from .resolution import Resolution # TODO: avoid runtime import
3601+
# TODO: avoid runtime import
3602+
from .resolution import Resolution, reso_str_bump_map
36023603

3603-
if prefix in Resolution.reso_str_bump_map:
3604+
if prefix in reso_str_bump_map:
36043605
stride, name = Resolution.get_stride_from_decimal(
36053606
float(stride), prefix
36063607
)

pandas/_libs/tslibs/resolution.pyx

+62-49
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from enum import Enum
2+
13
import numpy as np
24
from numpy cimport ndarray, int64_t, int32_t
35

@@ -25,10 +27,46 @@ cdef:
2527
int RESO_HR = 5
2628
int RESO_DAY = 6
2729

30+
reso_str_bump_map = {
31+
"D": "H",
32+
"H": "T",
33+
"T": "S",
34+
"S": "L",
35+
"L": "U",
36+
"U": "N",
37+
"N": None,
38+
}
39+
40+
_abbrev_to_attrnames = {v: k for k, v in attrname_to_abbrevs.items()}
41+
42+
_reso_str_map = {
43+
RESO_NS: "nanosecond",
44+
RESO_US: "microsecond",
45+
RESO_MS: "millisecond",
46+
RESO_SEC: "second",
47+
RESO_MIN: "minute",
48+
RESO_HR: "hour",
49+
RESO_DAY: "day",
50+
}
51+
52+
_str_reso_map = {v: k for k, v in _reso_str_map.items()}
53+
54+
# factor to multiply a value by to convert it to the next finer grained
55+
# resolution
56+
_reso_mult_map = {
57+
RESO_NS: None,
58+
RESO_US: 1000,
59+
RESO_MS: 1000,
60+
RESO_SEC: 1000,
61+
RESO_MIN: 60,
62+
RESO_HR: 60,
63+
RESO_DAY: 24,
64+
}
2865

2966
# ----------------------------------------------------------------------
3067

31-
def resolution(const int64_t[:] stamps, tz=None):
68+
69+
def get_resolution(const int64_t[:] stamps, tz=None):
3270
cdef:
3371
Py_ssize_t i, n = len(stamps)
3472
npy_datetimestruct dts
@@ -82,7 +120,7 @@ def resolution(const int64_t[:] stamps, tz=None):
82120
if curr_reso < reso:
83121
reso = curr_reso
84122

85-
return reso
123+
return Resolution(reso)
86124

87125

88126
cdef inline int _reso_stamp(npy_datetimestruct *dts):
@@ -99,7 +137,7 @@ cdef inline int _reso_stamp(npy_datetimestruct *dts):
99137
return RESO_DAY
100138

101139

102-
class Resolution:
140+
class Resolution(Enum):
103141

104142
# Note: cython won't allow us to reference the cdef versions at the
105143
# module level
@@ -111,41 +149,14 @@ class Resolution:
111149
RESO_HR = 5
112150
RESO_DAY = 6
113151

114-
_reso_str_map = {
115-
RESO_NS: 'nanosecond',
116-
RESO_US: 'microsecond',
117-
RESO_MS: 'millisecond',
118-
RESO_SEC: 'second',
119-
RESO_MIN: 'minute',
120-
RESO_HR: 'hour',
121-
RESO_DAY: 'day'}
122-
123-
# factor to multiply a value by to convert it to the next finer grained
124-
# resolution
125-
_reso_mult_map = {
126-
RESO_NS: None,
127-
RESO_US: 1000,
128-
RESO_MS: 1000,
129-
RESO_SEC: 1000,
130-
RESO_MIN: 60,
131-
RESO_HR: 60,
132-
RESO_DAY: 24}
133-
134-
reso_str_bump_map = {
135-
'D': 'H',
136-
'H': 'T',
137-
'T': 'S',
138-
'S': 'L',
139-
'L': 'U',
140-
'U': 'N',
141-
'N': None}
142-
143-
_str_reso_map = {v: k for k, v in _reso_str_map.items()}
144-
145-
_freq_reso_map = {v: k for k, v in attrname_to_abbrevs.items()}
152+
def __lt__(self, other):
153+
return self.value < other.value
154+
155+
def __ge__(self, other):
156+
return self.value >= other.value
146157

147158
@classmethod
148-
def get_str(cls, reso: int) -> str:
159+
def get_str(cls, reso: "Resolution") -> str:
149160
"""
150161
Return resolution str against resolution code.
151162

@@ -154,10 +165,10 @@ class Resolution:
154165
>>> Resolution.get_str(Resolution.RESO_SEC)
155166
'second'
156167
"""
157-
return cls._reso_str_map.get(reso, 'day')
168+
return _reso_str_map[reso.value]
158169

159170
@classmethod
160-
def get_reso(cls, resostr: str) -> int:
171+
def get_reso(cls, resostr: str) -> "Resolution":
161172
"""
162173
Return resolution str against resolution code.
163174

@@ -169,25 +180,27 @@ class Resolution:
169180
>>> Resolution.get_reso('second') == Resolution.RESO_SEC
170181
True
171182
"""
172-
return cls._str_reso_map.get(resostr, cls.RESO_DAY)
183+
return cls(_str_reso_map[resostr])
173184

174185
@classmethod
175-
def get_str_from_freq(cls, freq: str) -> str:
186+
def get_attrname_from_abbrev(cls, freq: str) -> str:
176187
"""
177188
Return resolution str against frequency str.
178189

179190
Examples
180191
--------
181-
>>> Resolution.get_str_from_freq('H')
192+
>>> Resolution.get_attrname_from_abbrev('H')
182193
'hour'
183194
"""
184-
return cls._freq_reso_map.get(freq, 'day')
195+
return _abbrev_to_attrnames[freq]
185196

186197
@classmethod
187-
def get_reso_from_freq(cls, freq: str) -> int:
198+
def get_reso_from_freq(cls, freq: str) -> "Resolution":
188199
"""
189200
Return resolution code against frequency str.
190201

202+
`freq` is given by the `offset.freqstr` for some DateOffset object.
203+
191204
Examples
192205
--------
193206
>>> Resolution.get_reso_from_freq('H')
@@ -196,16 +209,16 @@ class Resolution:
196209
>>> Resolution.get_reso_from_freq('H') == Resolution.RESO_HR
197210
True
198211
"""
199-
return cls.get_reso(cls.get_str_from_freq(freq))
212+
return cls.get_reso(cls.get_attrname_from_abbrev(freq))
200213

201214
@classmethod
202-
def get_stride_from_decimal(cls, value, freq):
215+
def get_stride_from_decimal(cls, value: float, freq: str):
203216
"""
204217
Convert freq with decimal stride into a higher freq with integer stride
205218
206219
Parameters
207220
----------
208-
value : int or float
221+
value : float
209222
freq : str
210223
Frequency string
211224
@@ -229,13 +242,13 @@ class Resolution:
229242
return int(value), freq
230243
else:
231244
start_reso = cls.get_reso_from_freq(freq)
232-
if start_reso == 0:
245+
if start_reso.value == 0:
233246
raise ValueError(
234247
"Could not convert to integer offset at any resolution"
235248
)
236249

237-
next_value = cls._reso_mult_map[start_reso] * value
238-
next_name = cls.reso_str_bump_map[freq]
250+
next_value = _reso_mult_map[start_reso.value] * value
251+
next_name = reso_str_bump_map[freq]
239252
return cls.get_stride_from_decimal(next_value, next_name)
240253

241254

pandas/core/arrays/datetimelike.py

+12-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from datetime import datetime, timedelta
22
import operator
3-
from typing import Any, Callable, Sequence, Tuple, Type, TypeVar, Union, cast
3+
from typing import Any, Callable, Optional, Sequence, Tuple, Type, TypeVar, Union, cast
44
import warnings
55

66
import numpy as np
@@ -804,7 +804,7 @@ def _validate_scalar(self, value, msg: str, cast_str: bool = False):
804804
return value
805805

806806
def _validate_listlike(
807-
self, value, opname: str, cast_str: bool = False, allow_object: bool = False,
807+
self, value, opname: str, cast_str: bool = False, allow_object: bool = False
808808
):
809809
if isinstance(value, type(self)):
810810
return value
@@ -1103,14 +1103,22 @@ def inferred_freq(self):
11031103
return None
11041104

11051105
@property # NB: override with cache_readonly in immutable subclasses
1106-
def _resolution(self):
1107-
return Resolution.get_reso_from_freq(self.freqstr)
1106+
def _resolution(self) -> Optional[Resolution]:
1107+
try:
1108+
return Resolution.get_reso_from_freq(self.freqstr)
1109+
except KeyError:
1110+
return None
11081111

11091112
@property # NB: override with cache_readonly in immutable subclasses
11101113
def resolution(self) -> str:
11111114
"""
11121115
Returns day, hour, minute, second, millisecond or microsecond
11131116
"""
1117+
if self._resolution is None:
1118+
if is_period_dtype(self.dtype):
1119+
# somewhere in the past it was decided we default to day
1120+
return "day"
1121+
# otherwise we fall through and will raise
11141122
return Resolution.get_str(self._resolution)
11151123

11161124
@classmethod

pandas/core/arrays/datetimes.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -538,8 +538,8 @@ def is_normalized(self):
538538
return conversion.is_date_array_normalized(self.asi8, self.tz)
539539

540540
@property # NB: override with cache_readonly in immutable subclasses
541-
def _resolution(self):
542-
return libresolution.resolution(self.asi8, self.tz)
541+
def _resolution(self) -> libresolution.Resolution:
542+
return libresolution.get_resolution(self.asi8, self.tz)
543543

544544
# ----------------------------------------------------------------
545545
# Array-Like / EA-Interface Methods

pandas/tests/tseries/frequencies/test_freq_code.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -104,13 +104,13 @@ def test_get_to_timestamp_base(freqstr, exp_freqstr):
104104
("N", "nanosecond"),
105105
],
106106
)
107-
def test_get_str_from_freq(freqstr, expected):
108-
assert _reso.get_str_from_freq(freqstr) == expected
107+
def test_get_attrname_from_abbrev(freqstr, expected):
108+
assert _reso.get_attrname_from_abbrev(freqstr) == expected
109109

110110

111111
@pytest.mark.parametrize("freq", ["A", "Q", "M", "D", "H", "T", "S", "L", "U", "N"])
112112
def test_get_freq_roundtrip(freq):
113-
result = _attrname_to_abbrevs[_reso.get_str_from_freq(freq)]
113+
result = _attrname_to_abbrevs[_reso.get_attrname_from_abbrev(freq)]
114114
assert freq == result
115115

116116

0 commit comments

Comments
 (0)