Skip to content

cleanup; use timedelta instead of relativedelta where possible #18183

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

Closed
wants to merge 2 commits into from
Closed
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
95 changes: 46 additions & 49 deletions pandas/tseries/offsets.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,19 @@
import numpy as np

from pandas.core.dtypes.generic import ABCSeries, ABCDatetimeIndex, ABCPeriod
from pandas.core.tools.datetimes import to_datetime, normalize_date
from pandas.core.tools.datetimes import to_datetime
from pandas.core.common import AbstractMethodError

# import after tools, dateutil check
from dateutil.relativedelta import relativedelta, weekday
from dateutil.easter import easter
from pandas._libs import tslib, Timestamp, OutOfBoundsDatetime, Timedelta

from pandas.util._decorators import cache_readonly

from pandas._libs import tslib
from pandas._libs.tslib import (Timestamp, Timedelta, NaT,
OutOfBoundsDatetime,
normalize_date)
from pandas._libs.tslibs.timedeltas import delta_to_nanoseconds
from pandas._libs.tslibs.offsets import (
ApplyTypeError,
Expand Down Expand Up @@ -49,16 +53,16 @@ def as_timestamp(obj):
return obj
try:
return Timestamp(obj)
except (OutOfBoundsDatetime):
except OutOfBoundsDatetime:
pass
return obj


def apply_wraps(func):
@functools.wraps(func)
def wrapper(self, other):
if other is tslib.NaT:
return tslib.NaT
if other is NaT:
return NaT
elif isinstance(other, (timedelta, Tick, DateOffset)):
# timedelta path
return func(self, other)
Expand Down Expand Up @@ -86,8 +90,8 @@ def wrapper(self, other):
if not isinstance(self, Nano) and result.nanosecond != nano:
if result.tz is not None:
# convert to UTC
value = tslib.tz_convert_single(
result.value, 'UTC', result.tz)
value = tslib.tz_convert_single(result.value,
'UTC', result.tz)
else:
value = result.value
result = Timestamp(value + nano)
Expand Down Expand Up @@ -173,6 +177,7 @@ def __init__(self, n=1, normalize=False, **kwds):
def apply(self, other):
if self._use_relativedelta:
other = as_datetime(other)
# TODO: Do we risk losing nanoseconds here?

if len(self.kwds) > 0:
tzinfo = getattr(other, 'tzinfo', None)
Expand Down Expand Up @@ -231,8 +236,8 @@ def apply_index(self, i):

weeks = (self.kwds.get('weeks', 0)) * self.n
if weeks:
i = (i.to_period('W') + weeks).to_timestamp() + \
i.to_perioddelta('W')
i = ((i.to_period('W') + weeks).to_timestamp() +
i.to_perioddelta('W'))

timedelta_kwds = dict((k, v) for k, v in self.kwds.items()
if k in ['days', 'hours', 'minutes',
Expand Down Expand Up @@ -1002,12 +1007,12 @@ def apply(self, other):
if not self.onOffset(other):
_, days_in_month = tslib.monthrange(other.year, other.month)
if 1 < other.day < self.day_of_month:
other += relativedelta(day=self.day_of_month)
other = other.replace(day=self.day_of_month)
if n > 0:
# rollforward so subtract 1
n -= 1
elif self.day_of_month < other.day < days_in_month:
other += relativedelta(day=self.day_of_month)
other = other.replace(day=self.day_of_month)
if n < 0:
# rollforward in the negative direction so add 1
n += 1
Expand Down Expand Up @@ -1084,11 +1089,11 @@ def onOffset(self, dt):
def _apply(self, n, other):
# if other.day is not day_of_month move to day_of_month and update n
if other.day < self.day_of_month:
other += relativedelta(day=self.day_of_month)
other = other.replace(day=self.day_of_month)
if n > 0:
n -= 1
elif other.day > self.day_of_month:
other += relativedelta(day=self.day_of_month)
other = other.replace(day=self.day_of_month)
if n == 0:
n = 1
else:
Expand Down Expand Up @@ -1141,13 +1146,13 @@ def onOffset(self, dt):
def _apply(self, n, other):
# if other.day is not day_of_month move to day_of_month and update n
if other.day < self.day_of_month:
other += relativedelta(day=self.day_of_month)
other = other.replace(day=self.day_of_month)
if n == 0:
n = -1
else:
n -= 1
elif other.day > self.day_of_month:
other += relativedelta(day=self.day_of_month)
other = other.replace(day=self.day_of_month)
if n == 0:
n = 1
elif n < 0:
Expand Down Expand Up @@ -1227,12 +1232,7 @@ def onOffset(self, dt):
if self.normalize and not _is_normalized(dt):
return False
first_weekday, _ = tslib.monthrange(dt.year, dt.month)
if first_weekday == 5:
return dt.day == 3
elif first_weekday == 6:
return dt.day == 2
else:
return dt.day == 1
return dt.day == _get_firstbday(first_weekday)


class CustomBusinessMonthEnd(BusinessMixin, MonthOffset):
Expand Down Expand Up @@ -1965,8 +1965,8 @@ def _decrement(date):
date.microsecond)

def _rollf(date):
if date.month != self.month or\
date.day < tslib.monthrange(date.year, date.month)[1]:
if (date.month != self.month or
date.day < tslib.monthrange(date.year, date.month)[1]):
date = _increment(date)
return date

Expand Down Expand Up @@ -2133,9 +2133,9 @@ def _offset_lwom(self):
return LastWeekOfMonth(n=1, weekday=self.weekday)

def isAnchored(self):
return self.n == 1 \
and self.startingMonth is not None \
and self.weekday is not None
return (self.n == 1 and
self.startingMonth is not None and
self.weekday is not None)

def onOffset(self, dt):
if self.normalize and not _is_normalized(dt):
Expand All @@ -2145,8 +2145,8 @@ def onOffset(self, dt):

if self.variation == "nearest":
# We have to check the year end of "this" cal year AND the previous
return year_end == dt or \
self.get_year_end(dt - relativedelta(months=1)) == dt
return (year_end == dt or
self.get_year_end(dt - relativedelta(months=1)) == dt)
else:
return year_end == dt

Expand Down Expand Up @@ -2224,10 +2224,10 @@ def get_year_end(self, dt):
return self._get_year_end_last(dt)

def get_target_month_end(self, dt):
target_month = datetime(
dt.year, self.startingMonth, 1, tzinfo=dt.tzinfo)
target_month = datetime(dt.year, self.startingMonth, 1,
tzinfo=dt.tzinfo)
next_month_first_of = target_month + relativedelta(months=+1)
return next_month_first_of + relativedelta(days=-1)
return next_month_first_of + timedelta(days=-1)

def _get_year_end_nearest(self, dt):
target_date = self.get_target_month_end(dt)
Expand All @@ -2243,8 +2243,8 @@ def _get_year_end_nearest(self, dt):
return backward

def _get_year_end_last(self, dt):
current_year = datetime(
dt.year, self.startingMonth, 1, tzinfo=dt.tzinfo)
current_year = datetime(dt.year, self.startingMonth, 1,
tzinfo=dt.tzinfo)
return current_year + self._offset_lwom

@property
Expand Down Expand Up @@ -2276,17 +2276,15 @@ def _parse_suffix(cls, varion_code, startingMonth_code, weekday_code):
elif varion_code == "L":
variation = "last"
else:
raise ValueError(
"Unable to parse varion_code: {code}".format(code=varion_code))
raise ValueError("Unable to parse varion_code: "
"{code}".format(code=varion_code))

startingMonth = _month_to_int[startingMonth_code]
weekday = _weekday_to_int[weekday_code]

return {
"weekday": weekday,
"startingMonth": startingMonth,
"variation": variation,
}
return {"weekday": weekday,
"startingMonth": startingMonth,
"variation": variation}

@classmethod
def _from_name(cls, *args):
Expand Down Expand Up @@ -2359,10 +2357,9 @@ def __init__(self, n=1, normalize=False, weekday=0, startingMonth=1,

@cache_readonly
def _offset(self):
return FY5253(
startingMonth=self.startingMonth,
weekday=self.weekday,
variation=self.variation)
return FY5253(startingMonth=self.startingMonth,
weekday=self.weekday,
variation=self.variation)

def isAnchored(self):
return self.n == 1 and self._offset.isAnchored()
Expand All @@ -2382,7 +2379,7 @@ def apply(self, other):
qtr_lens = self.get_weeks(other + self._offset)

for weeks in qtr_lens:
start += relativedelta(weeks=weeks)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i would leave the weeks, not real well tested for timedelta; relativedelta may be more intelligent here

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i would leave the weeks, not real well tested for timedelta

Not sure I understand. You mean the stdlib timedelta isn't well-tested?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

relativedelta may be more intelligent here

The relativedelta constructor just adds weeks*7 to the days kwarg.

start += timedelta(weeks=weeks)
if start > other:
other = start
n -= 1
Expand All @@ -2399,7 +2396,7 @@ def apply(self, other):
qtr_lens = self.get_weeks(other)

for weeks in reversed(qtr_lens):
end -= relativedelta(weeks=weeks)
end -= timedelta(weeks=weeks)
if end < other:
other = end
n -= 1
Expand Down Expand Up @@ -2442,7 +2439,7 @@ def onOffset(self, dt):

current = next_year_end
for qtr_len in qtr_lens[0:4]:
current += relativedelta(weeks=qtr_len)
current += timedelta(weeks=qtr_len)
if dt == current:
return True
return False
Expand Down Expand Up @@ -2472,8 +2469,8 @@ class Easter(DateOffset):
@apply_wraps
def apply(self, other):
currentEaster = easter(other.year)
currentEaster = datetime(
currentEaster.year, currentEaster.month, currentEaster.day)
currentEaster = datetime(currentEaster.year,
currentEaster.month, currentEaster.day)
currentEaster = tslib._localize_pydatetime(currentEaster, other.tzinfo)

# NOTE: easter returns a datetime.date so we have to convert to type of
Expand Down