Skip to content

Commit 8912efc

Browse files
jbrockmendeljreback
authored andcommitted
Fix offset __inits__, apply_index dtypes (pandas-dev#19142)
1 parent 27a5039 commit 8912efc

File tree

5 files changed

+80
-48
lines changed

5 files changed

+80
-48
lines changed

doc/source/whatsnew/v0.23.0.txt

+2-1
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,9 @@ Conversion
381381
- Fixed bug where comparing :class:`DatetimeIndex` failed to raise ``TypeError`` when attempting to compare timezone-aware and timezone-naive datetimelike objects (:issue:`18162`)
382382
- Bug in :class:`DatetimeIndex` where the repr was not showing high-precision time values at the end of a day (e.g., 23:59:59.999999999) (:issue:`19030`)
383383
- Bug where dividing a scalar timedelta-like object with :class:`TimedeltaIndex` performed the reciprocal operation (:issue:`19125`)
384+
- Bug in :class:`WeekOfMonth` and :class:`LastWeekOfMonth` where default keyword arguments for constructor raised ``ValueError`` (:issue:`19142`)
384385
- Bug in localization of a naive, datetime string in a ``Series`` constructor with a ``datetime64[ns, tz]`` dtype (:issue:`174151`)
386+
- :func:`Timestamp.replace` will now handle Daylight Savings transitions gracefully (:issue:`18319`)
385387

386388
Indexing
387389
^^^^^^^^
@@ -473,4 +475,3 @@ Other
473475
^^^^^
474476

475477
- Improved error message when attempting to use a Python keyword as an identifier in a ``numexpr`` backed query (:issue:`18221`)
476-
- :func:`Timestamp.replace` will now handle Daylight Savings transitions gracefully (:issue:`18319`)

pandas/_libs/tslibs/offsets.pyx

+5-32
Original file line numberDiff line numberDiff line change
@@ -290,27 +290,6 @@ class CacheableOffset(object):
290290
_cacheable = True
291291

292292

293-
class EndMixin(object):
294-
# helper for vectorized offsets
295-
296-
def _end_apply_index(self, i, freq):
297-
"""Offsets index to end of Period frequency"""
298-
299-
off = i.to_perioddelta('D')
300-
301-
base, mult = get_freq_code(freq)
302-
base_period = i.to_period(base)
303-
if self.n > 0:
304-
# when adding, dates on end roll to next
305-
roll = np.where(base_period.to_timestamp(how='end') == i - off,
306-
self.n, self.n - 1)
307-
else:
308-
roll = self.n
309-
310-
base = (base_period + roll).to_timestamp(how='end')
311-
return base + off
312-
313-
314293
# ---------------------------------------------------------------------
315294
# Base Classes
316295

@@ -675,11 +654,8 @@ def shift_months(int64_t[:] dtindex, int months, object day=None):
675654
months_to_roll = months
676655
compare_day = get_firstbday(dts.year, dts.month)
677656

678-
if months_to_roll > 0 and dts.day < compare_day:
679-
months_to_roll -= 1
680-
elif months_to_roll <= 0 and dts.day > compare_day:
681-
# as if rolled forward already
682-
months_to_roll += 1
657+
months_to_roll = roll_convention(dts.day, months_to_roll,
658+
compare_day)
683659

684660
dts.year = year_add_months(dts, months_to_roll)
685661
dts.month = month_add_months(dts, months_to_roll)
@@ -698,11 +674,8 @@ def shift_months(int64_t[:] dtindex, int months, object day=None):
698674
months_to_roll = months
699675
compare_day = get_lastbday(dts.year, dts.month)
700676

701-
if months_to_roll > 0 and dts.day < compare_day:
702-
months_to_roll -= 1
703-
elif months_to_roll <= 0 and dts.day > compare_day:
704-
# as if rolled forward already
705-
months_to_roll += 1
677+
months_to_roll = roll_convention(dts.day, months_to_roll,
678+
compare_day)
706679

707680
dts.year = year_add_months(dts, months_to_roll)
708681
dts.month = month_add_months(dts, months_to_roll)
@@ -823,7 +796,7 @@ cpdef int get_day_of_month(datetime other, day_opt) except? -1:
823796
raise ValueError(day_opt)
824797

825798

826-
cpdef int roll_convention(int other, int n, int compare):
799+
cpdef int roll_convention(int other, int n, int compare) nogil:
827800
"""
828801
Possibly increment or decrement the number of periods to shift
829802
based on rollforward/rollbackward conventions.

pandas/tests/indexes/datetimes/test_arithmetic.py

+5-4
Original file line numberDiff line numberDiff line change
@@ -495,7 +495,7 @@ def test_dt64_with_DateOffsets_relativedelta(klass, assert_func):
495495
assert_func(klass([x - op for x in vec]), vec - op)
496496

497497

498-
@pytest.mark.parametrize('cls_name', [
498+
@pytest.mark.parametrize('cls_and_kwargs', [
499499
'YearBegin', ('YearBegin', {'month': 5}),
500500
'YearEnd', ('YearEnd', {'month': 5}),
501501
'MonthBegin', 'MonthEnd',
@@ -518,7 +518,7 @@ def test_dt64_with_DateOffsets_relativedelta(klass, assert_func):
518518
@pytest.mark.parametrize('klass,assert_func', [
519519
(Series, tm.assert_series_equal),
520520
(DatetimeIndex, tm.assert_index_equal)])
521-
def test_dt64_with_DateOffsets(klass, assert_func, normalize, cls_name):
521+
def test_dt64_with_DateOffsets(klass, assert_func, normalize, cls_and_kwargs):
522522
# GH#10699
523523
# assert these are equal on a piecewise basis
524524
vec = klass([Timestamp('2000-01-05 00:15:00'),
@@ -530,11 +530,12 @@ def test_dt64_with_DateOffsets(klass, assert_func, normalize, cls_name):
530530
Timestamp('2000-05-15'),
531531
Timestamp('2001-06-15')])
532532

533-
if isinstance(cls_name, tuple):
533+
if isinstance(cls_and_kwargs, tuple):
534534
# If cls_name param is a tuple, then 2nd entry is kwargs for
535535
# the offset constructor
536-
cls_name, kwargs = cls_name
536+
cls_name, kwargs = cls_and_kwargs
537537
else:
538+
cls_name = cls_and_kwargs
538539
kwargs = {}
539540

540541
offset_cls = getattr(pd.offsets, cls_name)

pandas/tests/tseries/offsets/test_offsets.py

+7
Original file line numberDiff line numberDiff line change
@@ -3087,6 +3087,13 @@ def test_get_offset_day_error():
30873087
DateOffset()._get_offset_day(datetime.now())
30883088

30893089

3090+
def test_valid_default_arguments(offset_types):
3091+
# GH#19142 check that the calling the constructors without passing
3092+
# any keyword arguments produce valid offsets
3093+
cls = offset_types
3094+
cls()
3095+
3096+
30903097
@pytest.mark.parametrize('kwd', sorted(list(liboffsets.relativedelta_kwds)))
30913098
def test_valid_month_attributes(kwd, month_classes):
30923099
# GH#18226

pandas/tseries/offsets.py

+61-11
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from pandas._libs import tslib, Timestamp, OutOfBoundsDatetime, Timedelta
1717
from pandas.util._decorators import cache_readonly
1818

19-
from pandas._libs.tslibs import ccalendar
19+
from pandas._libs.tslibs import ccalendar, frequencies as libfrequencies
2020
from pandas._libs.tslibs.timedeltas import delta_to_nanoseconds
2121
import pandas._libs.tslibs.offsets as liboffsets
2222
from pandas._libs.tslibs.offsets import (
@@ -27,7 +27,6 @@
2727
apply_index_wraps,
2828
roll_yearday,
2929
shift_month,
30-
EndMixin,
3130
BaseOffset)
3231

3332

@@ -1233,7 +1232,19 @@ def _get_roll(self, i, before_day_of_month, after_day_of_month):
12331232
return roll
12341233

12351234
def _apply_index_days(self, i, roll):
1236-
i += (roll % 2) * Timedelta(days=self.day_of_month).value
1235+
"""Add days portion of offset to DatetimeIndex i
1236+
1237+
Parameters
1238+
----------
1239+
i : DatetimeIndex
1240+
roll : ndarray[int64_t]
1241+
1242+
Returns
1243+
-------
1244+
result : DatetimeIndex
1245+
"""
1246+
nanos = (roll % 2) * Timedelta(days=self.day_of_month).value
1247+
i += nanos.astype('timedelta64[ns]')
12371248
return i + Timedelta(days=-1)
12381249

12391250

@@ -1278,13 +1289,25 @@ def _get_roll(self, i, before_day_of_month, after_day_of_month):
12781289
return roll
12791290

12801291
def _apply_index_days(self, i, roll):
1281-
return i + (roll % 2) * Timedelta(days=self.day_of_month - 1).value
1292+
"""Add days portion of offset to DatetimeIndex i
1293+
1294+
Parameters
1295+
----------
1296+
i : DatetimeIndex
1297+
roll : ndarray[int64_t]
1298+
1299+
Returns
1300+
-------
1301+
result : DatetimeIndex
1302+
"""
1303+
nanos = (roll % 2) * Timedelta(days=self.day_of_month - 1).value
1304+
return i + nanos.astype('timedelta64[ns]')
12821305

12831306

12841307
# ---------------------------------------------------------------------
12851308
# Week-Based Offset Classes
12861309

1287-
class Week(EndMixin, DateOffset):
1310+
class Week(DateOffset):
12881311
"""
12891312
Weekly offset
12901313
@@ -1332,7 +1355,34 @@ def apply_index(self, i):
13321355
return ((i.to_period('W') + self.n).to_timestamp() +
13331356
i.to_perioddelta('W'))
13341357
else:
1335-
return self._end_apply_index(i, self.freqstr)
1358+
return self._end_apply_index(i)
1359+
1360+
def _end_apply_index(self, dtindex):
1361+
"""Add self to the given DatetimeIndex, specialized for case where
1362+
self.weekday is non-null.
1363+
1364+
Parameters
1365+
----------
1366+
dtindex : DatetimeIndex
1367+
1368+
Returns
1369+
-------
1370+
result : DatetimeIndex
1371+
"""
1372+
off = dtindex.to_perioddelta('D')
1373+
1374+
base, mult = libfrequencies.get_freq_code(self.freqstr)
1375+
base_period = dtindex.to_period(base)
1376+
if self.n > 0:
1377+
# when adding, dates on end roll to next
1378+
normed = dtindex - off
1379+
roll = np.where(base_period.to_timestamp(how='end') == normed,
1380+
self.n, self.n - 1)
1381+
else:
1382+
roll = self.n
1383+
1384+
base = (base_period + roll).to_timestamp(how='end')
1385+
return base + off
13361386

13371387
def onOffset(self, dt):
13381388
if self.normalize and not _is_normalized(dt):
@@ -1387,9 +1437,9 @@ class WeekOfMonth(_WeekOfMonthMixin, DateOffset):
13871437
Parameters
13881438
----------
13891439
n : int
1390-
week : {0, 1, 2, 3, ...}, default None
1440+
week : {0, 1, 2, 3, ...}, default 0
13911441
0 is 1st week of month, 1 2nd week, etc.
1392-
weekday : {0, 1, ..., 6}, default None
1442+
weekday : {0, 1, ..., 6}, default 0
13931443
0: Mondays
13941444
1: Tuesdays
13951445
2: Wednesdays
@@ -1401,7 +1451,7 @@ class WeekOfMonth(_WeekOfMonthMixin, DateOffset):
14011451
_prefix = 'WOM'
14021452
_adjust_dst = True
14031453

1404-
def __init__(self, n=1, normalize=False, week=None, weekday=None):
1454+
def __init__(self, n=1, normalize=False, week=0, weekday=0):
14051455
self.n = self._validate_n(n)
14061456
self.normalize = normalize
14071457
self.weekday = weekday
@@ -1464,7 +1514,7 @@ class LastWeekOfMonth(_WeekOfMonthMixin, DateOffset):
14641514
Parameters
14651515
----------
14661516
n : int, default 1
1467-
weekday : {0, 1, ..., 6}, default None
1517+
weekday : {0, 1, ..., 6}, default 0
14681518
0: Mondays
14691519
1: Tuesdays
14701520
2: Wednesdays
@@ -1477,7 +1527,7 @@ class LastWeekOfMonth(_WeekOfMonthMixin, DateOffset):
14771527
_prefix = 'LWOM'
14781528
_adjust_dst = True
14791529

1480-
def __init__(self, n=1, normalize=False, weekday=None):
1530+
def __init__(self, n=1, normalize=False, weekday=0):
14811531
self.n = self._validate_n(n)
14821532
self.normalize = normalize
14831533
self.weekday = weekday

0 commit comments

Comments
 (0)