Skip to content

REF: make Resolution an enum #34462

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
May 31, 2020
5 changes: 3 additions & 2 deletions pandas/_libs/tslibs/offsets.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand Down
111 changes: 62 additions & 49 deletions pandas/_libs/tslibs/resolution.pyx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from enum import Enum

import numpy as np
from numpy cimport ndarray, int64_t, int32_t

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand All @@ -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
Expand All @@ -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.

Expand All @@ -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.

Expand All @@ -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')
Expand All @@ -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

Expand All @@ -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)


Expand Down
16 changes: 12 additions & 4 deletions pandas/core/arrays/datetimelike.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions pandas/core/arrays/datetimes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions pandas/tests/tseries/frequencies/test_freq_code.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down