diff --git a/pandas/core/arrays/datetimelike.py b/pandas/core/arrays/datetimelike.py index 93166759d8dbd..f5c098605d18f 100644 --- a/pandas/core/arrays/datetimelike.py +++ b/pandas/core/arrays/datetimelike.py @@ -299,7 +299,28 @@ class TimelikeOps: def _round(self, freq, mode, ambiguous, nonexistent): # round the local times values = _ensure_datetimelike_to_i8(self) - result = round_nsint64(values, mode, freq) + try: + result = round_nsint64(values, mode, freq) + except ValueError as e: + # non-fixed offset, cannot do ns calculation. + # user freq.rollforward/back machinery instead + offset = frequencies.to_offset(freq) + if "non-fixed" in str(e): + if mode == RoundTo.PLUS_INFTY: + result = (self + offset).asi8 + elif mode == RoundTo.MINUS_INFTY: + result = (self - offset).asi8 + elif mode == RoundTo.NEAREST_HALF_EVEN: + msg = ("round only supported fixed offsets " + "(i.e. 'Day' is ok, 'MonthEnd' is not). " + "You may use snap or floor/ceil if applicable.") + raise ValueError(msg) + # upper = (self + offset).asi8 + # lower = (self - offset).asi8 + # mask = (upper-values) <= (values-lower) + # result = np.where(mask, lower, upper).asi8 + else: + raise e result = self._maybe_mask_results(result, fill_value=NaT) dtype = self.dtype diff --git a/pandas/tseries/offsets.py b/pandas/tseries/offsets.py index ac20ad1669638..0a47e72356976 100644 --- a/pandas/tseries/offsets.py +++ b/pandas/tseries/offsets.py @@ -22,6 +22,7 @@ __all__ = ['Day', 'BusinessDay', 'BDay', 'CustomBusinessDay', 'CDay', 'CBMonthEnd', 'CBMonthBegin', + 'DayBegin', 'DayEnd', 'MonthBegin', 'BMonthBegin', 'MonthEnd', 'BMonthEnd', 'SemiMonthEnd', 'SemiMonthBegin', 'BusinessHour', 'CustomBusinessHour', @@ -907,6 +908,68 @@ def __init__(self, n=1, normalize=False, weekmask='Mon Tue Wed Thu Fri', BusinessHourMixin.__init__(self, start=start, end=end, offset=offset) +# --------------------------------------------------------------------- +# Day-Based Offset Classes + + +class DayEnd(SingleConstructorOffset): + _adjust_dst = True + _attributes = frozenset(['n', 'normalize']) + + __init__ = BaseOffset.__init__ + + @property + def name(self): + if self.isAnchored: + return self.rule_code + else: + month = ccalendar.MONTH_ALIASES[self.n] + return "{code}-{month}".format(code=self.rule_code, + month=month) + + def onOffset(self, dt): + if self.normalize and not _is_normalized(dt): + return False + return self.apply(dt) == dt + + @apply_wraps + def apply(self, other): + tz = other.tzinfo + naive = other.replace(tzinfo=None) + shifted = Timestamp(year=naive.year, month=naive.month, day=naive.day) + if self._day_opt == 'end': + n = self.n+1 if self.n < 0 else self.n + shifted += Timedelta(days=n, nanoseconds=-1) + elif self._day_opt == 'start': + n = self.n if self.n < 0 else self.n + shifted += Timedelta(days=n) + elif self._day_opt != 'start': + raise ValurError("Unknown _day_opt value") + return conversion.localize_pydatetime(shifted, tz) + + @apply_index_wraps + def apply_index(self, i): + # TODO: going through __new__ raises on call to _validate_frequency; + # are we passing incorrect freq? + return type(i)._simple_new(np.array([self.apply(_).value for _ in i]), freq=i.freq, dtype=i.dtype) + + +class DayEnd(DayEnd): + """ + DateOffset of one day end. + """ + _prefix = 'DE' + _day_opt = 'end' + + +class DayBegin(DayEnd): + """ + DateOffset of one day at beginning. + """ + _prefix = 'DS' + _day_opt = 'start' + + # --------------------------------------------------------------------- # Month-Based Offset Classes @@ -2512,6 +2575,8 @@ def generate_range(start=None, end=None, periods=None, offset=BDay()): CustomBusinessMonthEnd, # 'CBM' CustomBusinessMonthBegin, # 'CBMS' CustomBusinessHour, # 'CBH' + DayEnd, # 'DE' + DayBegin, # 'DS' MonthEnd, # 'M' MonthBegin, # 'MS' Nano, # 'N'