Skip to content

REF: move mixins, repr_attrs to liboffsets #34139

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 3 commits into from
May 12, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions doc/source/reference/offset_frequency.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1056,6 +1056,7 @@ Methods
Tick.is_anchored
Tick.is_on_offset
Tick.__call__
Tick.apply

Day
---
Expand Down Expand Up @@ -1088,6 +1089,7 @@ Methods
Day.is_anchored
Day.is_on_offset
Day.__call__
Day.apply

Hour
----
Expand Down Expand Up @@ -1120,6 +1122,7 @@ Methods
Hour.is_anchored
Hour.is_on_offset
Hour.__call__
Hour.apply

Minute
------
Expand Down Expand Up @@ -1152,6 +1155,7 @@ Methods
Minute.is_anchored
Minute.is_on_offset
Minute.__call__
Minute.apply

Second
------
Expand Down Expand Up @@ -1184,6 +1188,7 @@ Methods
Second.is_anchored
Second.is_on_offset
Second.__call__
Second.apply

Milli
-----
Expand Down Expand Up @@ -1216,6 +1221,7 @@ Methods
Milli.is_anchored
Milli.is_on_offset
Milli.__call__
Milli.apply

Micro
-----
Expand Down Expand Up @@ -1248,6 +1254,7 @@ Methods
Micro.is_anchored
Micro.is_on_offset
Micro.__call__
Micro.apply

Nano
----
Expand Down Expand Up @@ -1280,6 +1287,7 @@ Methods
Nano.is_anchored
Nano.is_on_offset
Nano.__call__
Nano.apply

BDay
----
Expand Down
1 change: 0 additions & 1 deletion pandas/_libs/tslibs/offsets.pxd
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
cdef to_offset(object obj)
cdef bint is_offset_object(object obj)
cdef bint is_tick_object(object obj)
139 changes: 133 additions & 6 deletions pandas/_libs/tslibs/offsets.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ cnp.import_array()
from pandas._libs.tslibs cimport util
from pandas._libs.tslibs.util cimport is_integer_object

from pandas._libs.tslibs.base cimport ABCTick, ABCTimestamp
from pandas._libs.tslibs.base cimport ABCTick, ABCTimestamp, is_tick_object

from pandas._libs.tslibs.ccalendar import MONTHS, DAYS
from pandas._libs.tslibs.ccalendar cimport get_days_in_month, dayofweek
Expand Down Expand Up @@ -93,10 +93,6 @@ cdef bint is_offset_object(object obj):
return isinstance(obj, _BaseOffset)


cdef bint is_tick_object(object obj):
return isinstance(obj, _Tick)


cdef to_offset(object obj):
"""
Wrap pandas.tseries.frequencies.to_offset to keep centralize runtime
Expand Down Expand Up @@ -335,7 +331,7 @@ def to_dt64D(dt):
# Validation


def validate_business_time(t_input):
def _validate_business_time(t_input):
if isinstance(t_input, str):
try:
t = time.strptime(t_input, '%H:%M')
Expand Down Expand Up @@ -528,6 +524,21 @@ class _BaseOffset:
out = f'<{n_str}{className}{plural}{self._repr_attrs()}>'
return out

def _repr_attrs(self) -> str:
exclude = {"n", "inc", "normalize"}
attrs = []
for attr in sorted(self.__dict__):
if attr.startswith("_") or attr == "kwds":
continue
elif attr not in exclude:
value = getattr(self, attr)
attrs.append(f"{attr}={value}")

out = ""
if attrs:
out += ": " + ", ".join(attrs)
return out

@property
def name(self) -> str:
return self.rule_code
Expand Down Expand Up @@ -790,6 +801,97 @@ class BusinessMixin:
return out


class BusinessHourMixin(BusinessMixin):
_adjust_dst = False

def __init__(self, start="09:00", end="17:00", offset=timedelta(0)):
# must be validated here to equality check
if np.ndim(start) == 0:
# i.e. not is_list_like
start = [start]
if not len(start):
raise ValueError("Must include at least 1 start time")

if np.ndim(end) == 0:
# i.e. not is_list_like
end = [end]
if not len(end):
raise ValueError("Must include at least 1 end time")

start = np.array([_validate_business_time(x) for x in start])
end = np.array([_validate_business_time(x) for x in end])

# Validation of input
if len(start) != len(end):
raise ValueError("number of starting time and ending time must be the same")
num_openings = len(start)

# sort starting and ending time by starting time
index = np.argsort(start)

# convert to tuple so that start and end are hashable
start = tuple(start[index])
end = tuple(end[index])

total_secs = 0
for i in range(num_openings):
total_secs += self._get_business_hours_by_sec(start[i], end[i])
total_secs += self._get_business_hours_by_sec(
end[i], start[(i + 1) % num_openings]
)
if total_secs != 24 * 60 * 60:
raise ValueError(
"invalid starting and ending time(s): "
"opening hours should not touch or overlap with "
"one another"
)

object.__setattr__(self, "start", start)
object.__setattr__(self, "end", end)
object.__setattr__(self, "_offset", offset)

def _repr_attrs(self) -> str:
out = super()._repr_attrs()
hours = ",".join(
f'{st.strftime("%H:%M")}-{en.strftime("%H:%M")}'
for st, en in zip(self.start, self.end)
)
attrs = [f"{self._prefix}={hours}"]
out += ": " + ", ".join(attrs)
return out

def _get_business_hours_by_sec(self, start, end):
"""
Return business hours in a day by seconds.
"""
# create dummy datetime to calculate business hours in a day
dtstart = datetime(2014, 4, 1, start.hour, start.minute)
day = 1 if start < end else 2
until = datetime(2014, 4, day, end.hour, end.minute)
return int((until - dtstart).total_seconds())

def _get_closing_time(self, dt):
"""
Get the closing time of a business hour interval by its opening time.

Parameters
----------
dt : datetime
Opening time of a business hour interval.

Returns
-------
result : datetime
Corresponding closing time.
"""
for i, st in enumerate(self.start):
if st.hour == dt.hour and st.minute == dt.minute:
return dt + timedelta(
seconds=self._get_business_hours_by_sec(st, self.end[i])
)
assert False


class CustomMixin:
"""
Mixin for classes that define and validate calendar, holidays,
Expand All @@ -809,6 +911,31 @@ class CustomMixin:
object.__setattr__(self, "calendar", calendar)


class WeekOfMonthMixin:
"""
Mixin for methods common to WeekOfMonth and LastWeekOfMonth.
"""

@apply_wraps
def apply(self, other):
compare_day = self._get_offset_day(other)

months = self.n
if months > 0 and compare_day > other.day:
months -= 1
elif months <= 0 and compare_day < other.day:
months += 1

shifted = shift_month(other, months, "start")
to_day = self._get_offset_day(shifted)
return shift_day(shifted, to_day - shifted.day)

def is_on_offset(self, dt) -> bool:
if self.normalize and not is_normalized(dt):
return False
return dt.day == self._get_offset_day(dt)


# ----------------------------------------------------------------------
# RelativeDelta Arithmetic

Expand Down
4 changes: 2 additions & 2 deletions pandas/_libs/tslibs/period.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ cdef extern from "src/datetime/np_datetime.h":

cimport pandas._libs.tslibs.util as util

from pandas._libs.tslibs.base cimport ABCPeriod, is_period_object
from pandas._libs.tslibs.base cimport ABCPeriod, is_period_object, is_tick_object

from pandas._libs.tslibs.timestamps import Timestamp
from pandas._libs.tslibs.timezones cimport is_utc, is_tzlocal, get_dst_info
Expand Down Expand Up @@ -68,7 +68,7 @@ from pandas._libs.tslibs.nattype cimport (
c_NaT as NaT,
c_nat_strings as nat_strings,
)
from pandas._libs.tslibs.offsets cimport to_offset, is_tick_object
from pandas._libs.tslibs.offsets cimport to_offset
from pandas._libs.tslibs.tzconversion cimport tz_convert_utc_to_tzlocal


Expand Down
Loading