Skip to content

Commit 58a681a

Browse files
jbrockmendelpull[bot]
authored andcommitted
REF: move mixins, repr_attrs to liboffsets (#34139)
1 parent cc263ab commit 58a681a

File tree

5 files changed

+146
-145
lines changed

5 files changed

+146
-145
lines changed

doc/source/reference/offset_frequency.rst

+8
Original file line numberDiff line numberDiff line change
@@ -1056,6 +1056,7 @@ Methods
10561056
Tick.is_anchored
10571057
Tick.is_on_offset
10581058
Tick.__call__
1059+
Tick.apply
10591060

10601061
Day
10611062
---
@@ -1088,6 +1089,7 @@ Methods
10881089
Day.is_anchored
10891090
Day.is_on_offset
10901091
Day.__call__
1092+
Day.apply
10911093

10921094
Hour
10931095
----
@@ -1120,6 +1122,7 @@ Methods
11201122
Hour.is_anchored
11211123
Hour.is_on_offset
11221124
Hour.__call__
1125+
Hour.apply
11231126

11241127
Minute
11251128
------
@@ -1152,6 +1155,7 @@ Methods
11521155
Minute.is_anchored
11531156
Minute.is_on_offset
11541157
Minute.__call__
1158+
Minute.apply
11551159

11561160
Second
11571161
------
@@ -1184,6 +1188,7 @@ Methods
11841188
Second.is_anchored
11851189
Second.is_on_offset
11861190
Second.__call__
1191+
Second.apply
11871192

11881193
Milli
11891194
-----
@@ -1216,6 +1221,7 @@ Methods
12161221
Milli.is_anchored
12171222
Milli.is_on_offset
12181223
Milli.__call__
1224+
Milli.apply
12191225

12201226
Micro
12211227
-----
@@ -1248,6 +1254,7 @@ Methods
12481254
Micro.is_anchored
12491255
Micro.is_on_offset
12501256
Micro.__call__
1257+
Micro.apply
12511258

12521259
Nano
12531260
----
@@ -1280,6 +1287,7 @@ Methods
12801287
Nano.is_anchored
12811288
Nano.is_on_offset
12821289
Nano.__call__
1290+
Nano.apply
12831291

12841292
BDay
12851293
----

pandas/_libs/tslibs/offsets.pxd

-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,2 @@
11
cdef to_offset(object obj)
22
cdef bint is_offset_object(object obj)
3-
cdef bint is_tick_object(object obj)

pandas/_libs/tslibs/offsets.pyx

+133-6
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ cnp.import_array()
2121
from pandas._libs.tslibs cimport util
2222
from pandas._libs.tslibs.util cimport is_integer_object
2323

24-
from pandas._libs.tslibs.base cimport ABCTick, ABCTimestamp
24+
from pandas._libs.tslibs.base cimport ABCTick, ABCTimestamp, is_tick_object
2525

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

9595

96-
cdef bint is_tick_object(object obj):
97-
return isinstance(obj, _Tick)
98-
99-
10096
cdef to_offset(object obj):
10197
"""
10298
Wrap pandas.tseries.frequencies.to_offset to keep centralize runtime
@@ -335,7 +331,7 @@ def to_dt64D(dt):
335331
# Validation
336332

337333

338-
def validate_business_time(t_input):
334+
def _validate_business_time(t_input):
339335
if isinstance(t_input, str):
340336
try:
341337
t = time.strptime(t_input, '%H:%M')
@@ -528,6 +524,21 @@ class _BaseOffset:
528524
out = f'<{n_str}{className}{plural}{self._repr_attrs()}>'
529525
return out
530526

527+
def _repr_attrs(self) -> str:
528+
exclude = {"n", "inc", "normalize"}
529+
attrs = []
530+
for attr in sorted(self.__dict__):
531+
if attr.startswith("_") or attr == "kwds":
532+
continue
533+
elif attr not in exclude:
534+
value = getattr(self, attr)
535+
attrs.append(f"{attr}={value}")
536+
537+
out = ""
538+
if attrs:
539+
out += ": " + ", ".join(attrs)
540+
return out
541+
531542
@property
532543
def name(self) -> str:
533544
return self.rule_code
@@ -790,6 +801,97 @@ class BusinessMixin:
790801
return out
791802

792803

804+
class BusinessHourMixin(BusinessMixin):
805+
_adjust_dst = False
806+
807+
def __init__(self, start="09:00", end="17:00", offset=timedelta(0)):
808+
# must be validated here to equality check
809+
if np.ndim(start) == 0:
810+
# i.e. not is_list_like
811+
start = [start]
812+
if not len(start):
813+
raise ValueError("Must include at least 1 start time")
814+
815+
if np.ndim(end) == 0:
816+
# i.e. not is_list_like
817+
end = [end]
818+
if not len(end):
819+
raise ValueError("Must include at least 1 end time")
820+
821+
start = np.array([_validate_business_time(x) for x in start])
822+
end = np.array([_validate_business_time(x) for x in end])
823+
824+
# Validation of input
825+
if len(start) != len(end):
826+
raise ValueError("number of starting time and ending time must be the same")
827+
num_openings = len(start)
828+
829+
# sort starting and ending time by starting time
830+
index = np.argsort(start)
831+
832+
# convert to tuple so that start and end are hashable
833+
start = tuple(start[index])
834+
end = tuple(end[index])
835+
836+
total_secs = 0
837+
for i in range(num_openings):
838+
total_secs += self._get_business_hours_by_sec(start[i], end[i])
839+
total_secs += self._get_business_hours_by_sec(
840+
end[i], start[(i + 1) % num_openings]
841+
)
842+
if total_secs != 24 * 60 * 60:
843+
raise ValueError(
844+
"invalid starting and ending time(s): "
845+
"opening hours should not touch or overlap with "
846+
"one another"
847+
)
848+
849+
object.__setattr__(self, "start", start)
850+
object.__setattr__(self, "end", end)
851+
object.__setattr__(self, "_offset", offset)
852+
853+
def _repr_attrs(self) -> str:
854+
out = super()._repr_attrs()
855+
hours = ",".join(
856+
f'{st.strftime("%H:%M")}-{en.strftime("%H:%M")}'
857+
for st, en in zip(self.start, self.end)
858+
)
859+
attrs = [f"{self._prefix}={hours}"]
860+
out += ": " + ", ".join(attrs)
861+
return out
862+
863+
def _get_business_hours_by_sec(self, start, end):
864+
"""
865+
Return business hours in a day by seconds.
866+
"""
867+
# create dummy datetime to calculate business hours in a day
868+
dtstart = datetime(2014, 4, 1, start.hour, start.minute)
869+
day = 1 if start < end else 2
870+
until = datetime(2014, 4, day, end.hour, end.minute)
871+
return int((until - dtstart).total_seconds())
872+
873+
def _get_closing_time(self, dt):
874+
"""
875+
Get the closing time of a business hour interval by its opening time.
876+
877+
Parameters
878+
----------
879+
dt : datetime
880+
Opening time of a business hour interval.
881+
882+
Returns
883+
-------
884+
result : datetime
885+
Corresponding closing time.
886+
"""
887+
for i, st in enumerate(self.start):
888+
if st.hour == dt.hour and st.minute == dt.minute:
889+
return dt + timedelta(
890+
seconds=self._get_business_hours_by_sec(st, self.end[i])
891+
)
892+
assert False
893+
894+
793895
class CustomMixin:
794896
"""
795897
Mixin for classes that define and validate calendar, holidays,
@@ -809,6 +911,31 @@ class CustomMixin:
809911
object.__setattr__(self, "calendar", calendar)
810912

811913

914+
class WeekOfMonthMixin:
915+
"""
916+
Mixin for methods common to WeekOfMonth and LastWeekOfMonth.
917+
"""
918+
919+
@apply_wraps
920+
def apply(self, other):
921+
compare_day = self._get_offset_day(other)
922+
923+
months = self.n
924+
if months > 0 and compare_day > other.day:
925+
months -= 1
926+
elif months <= 0 and compare_day < other.day:
927+
months += 1
928+
929+
shifted = shift_month(other, months, "start")
930+
to_day = self._get_offset_day(shifted)
931+
return shift_day(shifted, to_day - shifted.day)
932+
933+
def is_on_offset(self, dt) -> bool:
934+
if self.normalize and not is_normalized(dt):
935+
return False
936+
return dt.day == self._get_offset_day(dt)
937+
938+
812939
# ----------------------------------------------------------------------
813940
# RelativeDelta Arithmetic
814941

pandas/_libs/tslibs/period.pyx

+2-2
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ cdef extern from "src/datetime/np_datetime.h":
3737

3838
cimport pandas._libs.tslibs.util as util
3939

40-
from pandas._libs.tslibs.base cimport ABCPeriod, is_period_object
40+
from pandas._libs.tslibs.base cimport ABCPeriod, is_period_object, is_tick_object
4141

4242
from pandas._libs.tslibs.timestamps import Timestamp
4343
from pandas._libs.tslibs.timezones cimport is_utc, is_tzlocal, get_dst_info
@@ -68,7 +68,7 @@ from pandas._libs.tslibs.nattype cimport (
6868
c_NaT as NaT,
6969
c_nat_strings as nat_strings,
7070
)
71-
from pandas._libs.tslibs.offsets cimport to_offset, is_tick_object
71+
from pandas._libs.tslibs.offsets cimport to_offset
7272
from pandas._libs.tslibs.tzconversion cimport tz_convert_utc_to_tzlocal
7373

7474

0 commit comments

Comments
 (0)