Skip to content

Commit ad70ed4

Browse files
jbrockmendeljreback
authored andcommitted
Fix bug where offset.copy() != offset (#17452)
1 parent 06a6e63 commit ad70ed4

File tree

2 files changed

+115
-70
lines changed

2 files changed

+115
-70
lines changed

pandas/tests/tseries/test_offsets.py

+5
Original file line numberDiff line numberDiff line change
@@ -1955,6 +1955,11 @@ def _check_roundtrip(obj):
19551955
_check_roundtrip(self._object(2))
19561956
_check_roundtrip(self._object() * 2)
19571957

1958+
def test_copy(self):
1959+
# GH 17452
1960+
off = self._object(weekmask='Mon Wed Fri')
1961+
assert off == off.copy()
1962+
19581963

19591964
class TestCustomBusinessMonthEnd(CustomBusinessMonthBase, Base):
19601965
_object = CBMonthEnd

pandas/tseries/offsets.py

+110-70
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from dateutil.relativedelta import relativedelta, weekday
1212
from dateutil.easter import easter
1313
from pandas._libs import tslib, Timestamp, OutOfBoundsDatetime, Timedelta
14+
from pandas.util._decorators import cache_readonly
1415

1516
import functools
1617
import operator
@@ -573,9 +574,9 @@ def __setstate__(self, state):
573574
"""Reconstruct an instance from a pickled state"""
574575
self.__dict__ = state
575576
if 'weekmask' in state and 'holidays' in state:
576-
calendar, holidays = self.get_calendar(weekmask=self.weekmask,
577-
holidays=self.holidays,
578-
calendar=None)
577+
calendar, holidays = _get_calendar(weekmask=self.weekmask,
578+
holidays=self.holidays,
579+
calendar=None)
579580
self.kwds['calendar'] = self.calendar = calendar
580581
self.kwds['holidays'] = self.holidays = holidays
581582
self.kwds['weekmask'] = state['weekmask']
@@ -978,9 +979,9 @@ def __init__(self, n=1, normalize=False, weekmask='Mon Tue Wed Thu Fri',
978979
self.normalize = normalize
979980
self.kwds = kwds
980981
self.offset = kwds.get('offset', timedelta(0))
981-
calendar, holidays = self.get_calendar(weekmask=weekmask,
982-
holidays=holidays,
983-
calendar=calendar)
982+
calendar, holidays = _get_calendar(weekmask=weekmask,
983+
holidays=holidays,
984+
calendar=calendar)
984985
# CustomBusinessDay instances are identified by the
985986
# following two attributes. See DateOffset._params()
986987
# holidays, weekmask
@@ -989,36 +990,6 @@ def __init__(self, n=1, normalize=False, weekmask='Mon Tue Wed Thu Fri',
989990
self.kwds['holidays'] = self.holidays = holidays
990991
self.kwds['calendar'] = self.calendar = calendar
991992

992-
def get_calendar(self, weekmask, holidays, calendar):
993-
"""Generate busdaycalendar"""
994-
if isinstance(calendar, np.busdaycalendar):
995-
if not holidays:
996-
holidays = tuple(calendar.holidays)
997-
elif not isinstance(holidays, tuple):
998-
holidays = tuple(holidays)
999-
else:
1000-
# trust that calendar.holidays and holidays are
1001-
# consistent
1002-
pass
1003-
return calendar, holidays
1004-
1005-
if holidays is None:
1006-
holidays = []
1007-
try:
1008-
holidays = holidays + calendar.holidays().tolist()
1009-
except AttributeError:
1010-
pass
1011-
holidays = [self._to_dt64(dt, dtype='datetime64[D]') for dt in
1012-
holidays]
1013-
holidays = tuple(sorted(holidays))
1014-
1015-
kwargs = {'weekmask': weekmask}
1016-
if holidays:
1017-
kwargs['holidays'] = holidays
1018-
1019-
busdaycalendar = np.busdaycalendar(**kwargs)
1020-
return busdaycalendar, holidays
1021-
1022993
@apply_wraps
1023994
def apply(self, other):
1024995
if self.n <= 0:
@@ -1050,25 +1021,10 @@ def apply(self, other):
10501021
def apply_index(self, i):
10511022
raise NotImplementedError
10521023

1053-
@staticmethod
1054-
def _to_dt64(dt, dtype='datetime64'):
1055-
# Currently
1056-
# > np.datetime64(dt.datetime(2013,5,1),dtype='datetime64[D]')
1057-
# numpy.datetime64('2013-05-01T02:00:00.000000+0200')
1058-
# Thus astype is needed to cast datetime to datetime64[D]
1059-
if getattr(dt, 'tzinfo', None) is not None:
1060-
i8 = tslib.pydt_to_i8(dt)
1061-
dt = tslib.tz_convert_single(i8, 'UTC', dt.tzinfo)
1062-
dt = Timestamp(dt)
1063-
dt = np.datetime64(dt)
1064-
if dt.dtype.name != dtype:
1065-
dt = dt.astype(dtype)
1066-
return dt
1067-
10681024
def onOffset(self, dt):
10691025
if self.normalize and not _is_normalized(dt):
10701026
return False
1071-
day64 = self._to_dt64(dt, 'datetime64[D]')
1027+
day64 = _to_dt64(dt, 'datetime64[D]')
10721028
return np.is_busday(day64, busdaycal=self.calendar)
10731029

10741030

@@ -1087,19 +1043,25 @@ def __init__(self, n=1, normalize=False, weekmask='Mon Tue Wed Thu Fri',
10871043
self.n = int(n)
10881044
self.normalize = normalize
10891045
super(CustomBusinessHour, self).__init__(**kwds)
1046+
1047+
calendar, holidays = _get_calendar(weekmask=weekmask,
1048+
holidays=holidays,
1049+
calendar=calendar)
1050+
self.kwds['weekmask'] = self.weekmask = weekmask
1051+
self.kwds['holidays'] = self.holidays = holidays
1052+
self.kwds['calendar'] = self.calendar = calendar
1053+
1054+
@cache_readonly
1055+
def next_bday(self):
10901056
# used for moving to next businessday
10911057
if self.n >= 0:
10921058
nb_offset = 1
10931059
else:
10941060
nb_offset = -1
1095-
self.next_bday = CustomBusinessDay(n=nb_offset,
1096-
weekmask=weekmask,
1097-
holidays=holidays,
1098-
calendar=calendar)
1099-
1100-
self.kwds['weekmask'] = self.next_bday.weekmask
1101-
self.kwds['holidays'] = self.next_bday.holidays
1102-
self.kwds['calendar'] = self.next_bday.calendar
1061+
return CustomBusinessDay(n=nb_offset,
1062+
weekmask=self.weekmask,
1063+
holidays=self.holidays,
1064+
calendar=self.calendar)
11031065

11041066

11051067
class MonthOffset(SingleConstructorOffset):
@@ -1471,11 +1433,25 @@ def __init__(self, n=1, normalize=False, weekmask='Mon Tue Wed Thu Fri',
14711433
self.normalize = normalize
14721434
self.kwds = kwds
14731435
self.offset = kwds.get('offset', timedelta(0))
1474-
self.cbday = CustomBusinessDay(n=self.n, normalize=normalize,
1475-
weekmask=weekmask, holidays=holidays,
1476-
calendar=calendar, **kwds)
1477-
self.m_offset = MonthEnd(n=1, normalize=normalize, **kwds)
1478-
self.kwds['calendar'] = self.cbday.calendar # cache numpy calendar
1436+
1437+
calendar, holidays = _get_calendar(weekmask=weekmask,
1438+
holidays=holidays,
1439+
calendar=calendar)
1440+
self.kwds['weekmask'] = self.weekmask = weekmask
1441+
self.kwds['holidays'] = self.holidays = holidays
1442+
self.kwds['calendar'] = self.calendar = calendar
1443+
1444+
@cache_readonly
1445+
def cbday(self):
1446+
kwds = self.kwds
1447+
return CustomBusinessDay(n=self.n, normalize=self.normalize, **kwds)
1448+
1449+
@cache_readonly
1450+
def m_offset(self):
1451+
kwds = self.kwds
1452+
kwds = {key: kwds[key] for key in kwds
1453+
if key not in ['calendar', 'weekmask', 'holidays']}
1454+
return MonthEnd(n=1, normalize=self.normalize, **kwds)
14791455

14801456
@apply_wraps
14811457
def apply(self, other):
@@ -1531,11 +1507,27 @@ def __init__(self, n=1, normalize=False, weekmask='Mon Tue Wed Thu Fri',
15311507
self.normalize = normalize
15321508
self.kwds = kwds
15331509
self.offset = kwds.get('offset', timedelta(0))
1534-
self.cbday = CustomBusinessDay(n=self.n, normalize=normalize,
1535-
weekmask=weekmask, holidays=holidays,
1536-
calendar=calendar, **kwds)
1537-
self.m_offset = MonthBegin(n=1, normalize=normalize, **kwds)
1538-
self.kwds['calendar'] = self.cbday.calendar # cache numpy calendar
1510+
1511+
# _get_calendar does validation and possible transformation
1512+
# of calendar and holidays.
1513+
calendar, holidays = _get_calendar(weekmask=weekmask,
1514+
holidays=holidays,
1515+
calendar=calendar)
1516+
kwds['calendar'] = self.calendar = calendar
1517+
kwds['weekmask'] = self.weekmask = weekmask
1518+
kwds['holidays'] = self.holidays = holidays
1519+
1520+
@cache_readonly
1521+
def cbday(self):
1522+
kwds = self.kwds
1523+
return CustomBusinessDay(n=self.n, normalize=self.normalize, **kwds)
1524+
1525+
@cache_readonly
1526+
def m_offset(self):
1527+
kwds = self.kwds
1528+
kwds = {key: kwds[key] for key in kwds
1529+
if key not in ['calendar', 'weekmask', 'holidays']}
1530+
return MonthBegin(n=1, normalize=self.normalize, **kwds)
15391531

15401532
@apply_wraps
15411533
def apply(self, other):
@@ -2861,6 +2853,54 @@ class Nano(Tick):
28612853
CBMonthBegin = CustomBusinessMonthBegin
28622854
CDay = CustomBusinessDay
28632855

2856+
# ---------------------------------------------------------------------
2857+
# Business Calendar helpers
2858+
2859+
2860+
def _get_calendar(weekmask, holidays, calendar):
2861+
"""Generate busdaycalendar"""
2862+
if isinstance(calendar, np.busdaycalendar):
2863+
if not holidays:
2864+
holidays = tuple(calendar.holidays)
2865+
elif not isinstance(holidays, tuple):
2866+
holidays = tuple(holidays)
2867+
else:
2868+
# trust that calendar.holidays and holidays are
2869+
# consistent
2870+
pass
2871+
return calendar, holidays
2872+
2873+
if holidays is None:
2874+
holidays = []
2875+
try:
2876+
holidays = holidays + calendar.holidays().tolist()
2877+
except AttributeError:
2878+
pass
2879+
holidays = [_to_dt64(dt, dtype='datetime64[D]') for dt in holidays]
2880+
holidays = tuple(sorted(holidays))
2881+
2882+
kwargs = {'weekmask': weekmask}
2883+
if holidays:
2884+
kwargs['holidays'] = holidays
2885+
2886+
busdaycalendar = np.busdaycalendar(**kwargs)
2887+
return busdaycalendar, holidays
2888+
2889+
2890+
def _to_dt64(dt, dtype='datetime64'):
2891+
# Currently
2892+
# > np.datetime64(dt.datetime(2013,5,1),dtype='datetime64[D]')
2893+
# numpy.datetime64('2013-05-01T02:00:00.000000+0200')
2894+
# Thus astype is needed to cast datetime to datetime64[D]
2895+
if getattr(dt, 'tzinfo', None) is not None:
2896+
i8 = tslib.pydt_to_i8(dt)
2897+
dt = tslib.tz_convert_single(i8, 'UTC', dt.tzinfo)
2898+
dt = Timestamp(dt)
2899+
dt = np.datetime64(dt)
2900+
if dt.dtype.name != dtype:
2901+
dt = dt.astype(dtype)
2902+
return dt
2903+
28642904

28652905
def _get_firstbday(wkday):
28662906
"""

0 commit comments

Comments
 (0)