diff --git a/doc/source/whatsnew/v0.22.0.txt b/doc/source/whatsnew/v0.22.0.txt index e85ba505887b4..cbd094ec4ef49 100644 --- a/doc/source/whatsnew/v0.22.0.txt +++ b/doc/source/whatsnew/v0.22.0.txt @@ -42,7 +42,7 @@ Other API Changes - ``NaT`` division with :class:`datetime.timedelta` will now return ``NaN`` instead of raising (:issue:`17876`) - :class:`Timestamp` will no longer silently ignore unused or invalid `tz` or `tzinfo` arguments (:issue:`17690`) -- +- :class:`CacheableOffset` and :class:`WeekDay` are no longer available in the `tseries.offsets` module (:issue:`17830`) - .. _whatsnew_0220.deprecations: diff --git a/pandas/_libs/tslibs/offsets.pyx b/pandas/_libs/tslibs/offsets.pyx new file mode 100644 index 0000000000000..9959b053707c7 --- /dev/null +++ b/pandas/_libs/tslibs/offsets.pyx @@ -0,0 +1,208 @@ +# -*- coding: utf-8 -*- +# cython: profile=False + +cimport cython + +import time +from cpython.datetime cimport time as dt_time + +import numpy as np +cimport numpy as np +np.import_array() + + +from util cimport is_string_object + + +from pandas._libs.tslib import pydt_to_i8, tz_convert_single + +# --------------------------------------------------------------------- +# Constants + +# Duplicated in tslib +_MONTHS = ['JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', + 'AUG', 'SEP', 'OCT', 'NOV', 'DEC'] +_int_to_month = {(k + 1): v for k, v in enumerate(_MONTHS)} +_month_to_int = dict((v, k) for k, v in _int_to_month.items()) + + +class WeekDay(object): + MON = 0 + TUE = 1 + WED = 2 + THU = 3 + FRI = 4 + SAT = 5 + SUN = 6 + + +_int_to_weekday = { + WeekDay.MON: 'MON', + WeekDay.TUE: 'TUE', + WeekDay.WED: 'WED', + WeekDay.THU: 'THU', + WeekDay.FRI: 'FRI', + WeekDay.SAT: 'SAT', + WeekDay.SUN: 'SUN'} + +_weekday_to_int = {_int_to_weekday[key]: key for key in _int_to_weekday} + + +_offset_to_period_map = { + 'WEEKDAY': 'D', + 'EOM': 'M', + 'BM': 'M', + 'BQS': 'Q', + 'QS': 'Q', + 'BQ': 'Q', + 'BA': 'A', + 'AS': 'A', + 'BAS': 'A', + 'MS': 'M', + 'D': 'D', + 'C': 'C', + 'B': 'B', + 'T': 'T', + 'S': 'S', + 'L': 'L', + 'U': 'U', + 'N': 'N', + 'H': 'H', + 'Q': 'Q', + 'A': 'A', + 'W': 'W', + 'M': 'M', + 'Y': 'A', + 'BY': 'A', + 'YS': 'A', + 'BYS': 'A'} + +need_suffix = ['QS', 'BQ', 'BQS', 'YS', 'AS', 'BY', 'BA', 'BYS', 'BAS'] + + +for __prefix in need_suffix: + for _m in _MONTHS: + key = '%s-%s' % (__prefix, _m) + _offset_to_period_map[key] = _offset_to_period_map[__prefix] + +for __prefix in ['A', 'Q']: + for _m in _MONTHS: + _alias = '%s-%s' % (__prefix, _m) + _offset_to_period_map[_alias] = _alias + +_days = ['MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT', 'SUN'] +for _d in _days: + _offset_to_period_map['W-%s' % _d] = 'W-%s' % _d + + +# --------------------------------------------------------------------- +# Misc Helpers + +def as_datetime(obj): + f = getattr(obj, 'to_pydatetime', None) + if f is not None: + obj = f() + return obj + + +def _is_normalized(dt): + if (dt.hour != 0 or dt.minute != 0 or dt.second != 0 or + dt.microsecond != 0 or getattr(dt, 'nanosecond', 0) != 0): + return False + return True + + +# --------------------------------------------------------------------- +# Business Helpers + +def _get_firstbday(wkday): + """ + wkday is the result of monthrange(year, month) + + If it's a saturday or sunday, increment first business day to reflect this + """ + first = 1 + if wkday == 5: # on Saturday + first = 3 + elif wkday == 6: # on Sunday + first = 2 + return first + + +def _get_calendar(weekmask, holidays, calendar): + """Generate busdaycalendar""" + if isinstance(calendar, np.busdaycalendar): + if not holidays: + holidays = tuple(calendar.holidays) + elif not isinstance(holidays, tuple): + holidays = tuple(holidays) + else: + # trust that calendar.holidays and holidays are + # consistent + pass + return calendar, holidays + + if holidays is None: + holidays = [] + try: + holidays = holidays + calendar.holidays().tolist() + except AttributeError: + pass + holidays = [_to_dt64(dt, dtype='datetime64[D]') for dt in holidays] + holidays = tuple(sorted(holidays)) + + kwargs = {'weekmask': weekmask} + if holidays: + kwargs['holidays'] = holidays + + busdaycalendar = np.busdaycalendar(**kwargs) + return busdaycalendar, holidays + + +def _to_dt64(dt, dtype='datetime64'): + # Currently + # > np.datetime64(dt.datetime(2013,5,1),dtype='datetime64[D]') + # numpy.datetime64('2013-05-01T02:00:00.000000+0200') + # Thus astype is needed to cast datetime to datetime64[D] + if getattr(dt, 'tzinfo', None) is not None: + i8 = pydt_to_i8(dt) + dt = tz_convert_single(i8, 'UTC', dt.tzinfo) + dt = np.int64(dt).astype('datetime64[ns]') + else: + dt = np.datetime64(dt) + if dt.dtype.name != dtype: + dt = dt.astype(dtype) + return dt + + +# --------------------------------------------------------------------- +# Validation + + +def _validate_business_time(t_input): + if is_string_object(t_input): + try: + t = time.strptime(t_input, '%H:%M') + return dt_time(hour=t.tm_hour, minute=t.tm_min) + except ValueError: + raise ValueError("time data must match '%H:%M' format") + elif isinstance(t_input, dt_time): + if t_input.second != 0 or t_input.microsecond != 0: + raise ValueError( + "time data must be specified only with hour and minute") + return t_input + else: + raise ValueError("time data must be string or datetime.time") + +# --------------------------------------------------------------------- +# Mixins & Singletons + + +class ApplyTypeError(TypeError): + # sentinel class for catching the apply error to return NotImplemented + pass + + +# TODO: unused. remove? +class CacheableOffset(object): + _cacheable = True diff --git a/pandas/tests/tseries/test_offsets.py b/pandas/tests/tseries/test_offsets.py index c0e682c978610..4fd3bba01602f 100644 --- a/pandas/tests/tseries/test_offsets.py +++ b/pandas/tests/tseries/test_offsets.py @@ -17,9 +17,10 @@ get_offset, get_standard_freq) from pandas.core.indexes.datetimes import ( _to_m8, DatetimeIndex, _daterange_cache) +from pandas._libs.tslibs.offsets import WeekDay, CacheableOffset from pandas.tseries.offsets import (BDay, CDay, BQuarterEnd, BMonthEnd, BusinessHour, WeekOfMonth, CBMonthEnd, - CustomBusinessHour, WeekDay, + CustomBusinessHour, CBMonthBegin, BYearEnd, MonthEnd, MonthBegin, SemiMonthBegin, SemiMonthEnd, BYearBegin, QuarterBegin, BQuarterBegin, @@ -27,7 +28,7 @@ YearEnd, Hour, Minute, Second, Day, Micro, QuarterEnd, BusinessMonthEnd, FY5253, Milli, Nano, Easter, FY5253Quarter, - LastWeekOfMonth, CacheableOffset) + LastWeekOfMonth) from pandas.core.tools.datetimes import ( format, ole2datetime, parse_time_string, to_datetime, DateParseError) diff --git a/pandas/tseries/frequencies.py b/pandas/tseries/frequencies.py index b055c4b4cb27f..763e6547ea2cb 100644 --- a/pandas/tseries/frequencies.py +++ b/pandas/tseries/frequencies.py @@ -312,7 +312,7 @@ def _get_freq_str(base, mult=1): # --------------------------------------------------------------------- # Offset names ("time rules") and related functions - +from pandas._libs.tslibs.offsets import _offset_to_period_map from pandas.tseries.offsets import (Nano, Micro, Milli, Second, # noqa Minute, Hour, Day, BDay, CDay, Week, MonthBegin, @@ -328,51 +328,6 @@ def _get_freq_str(base, mult=1): #: cache of previously seen offsets _offset_map = {} -_offset_to_period_map = { - 'WEEKDAY': 'D', - 'EOM': 'M', - 'BM': 'M', - 'BQS': 'Q', - 'QS': 'Q', - 'BQ': 'Q', - 'BA': 'A', - 'AS': 'A', - 'BAS': 'A', - 'MS': 'M', - 'D': 'D', - 'C': 'C', - 'B': 'B', - 'T': 'T', - 'S': 'S', - 'L': 'L', - 'U': 'U', - 'N': 'N', - 'H': 'H', - 'Q': 'Q', - 'A': 'A', - 'W': 'W', - 'M': 'M', - 'Y': 'A', - 'BY': 'A', - 'YS': 'A', - 'BYS': 'A', -} - -need_suffix = ['QS', 'BQ', 'BQS', 'YS', 'AS', 'BY', 'BA', 'BYS', 'BAS'] -for __prefix in need_suffix: - for _m in tslib._MONTHS: - _alias = '{prefix}-{month}'.format(prefix=__prefix, month=_m) - _offset_to_period_map[_alias] = _offset_to_period_map[__prefix] -for __prefix in ['A', 'Q']: - for _m in tslib._MONTHS: - _alias = '{prefix}-{month}'.format(prefix=__prefix, month=_m) - _offset_to_period_map[_alias] = _alias - -_days = ['MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT', 'SUN'] -for _d in _days: - _alias = 'W-{day}'.format(day=_d) - _offset_to_period_map[_alias] = _alias - def get_period_alias(offset_str): """ alias to closest period strings BQ->Q etc""" diff --git a/pandas/tseries/offsets.py b/pandas/tseries/offsets.py index c65691618e654..88c878c9cb9a6 100644 --- a/pandas/tseries/offsets.py +++ b/pandas/tseries/offsets.py @@ -14,6 +14,13 @@ from pandas._libs import tslib, Timestamp, OutOfBoundsDatetime, Timedelta from pandas.util._decorators import cache_readonly +from pandas._libs.tslib import _delta_to_nanoseconds +from pandas._libs.tslibs.offsets import ( + ApplyTypeError, + as_datetime, _is_normalized, + _get_firstbday, _get_calendar, _to_dt64, _validate_business_time, + _int_to_weekday, _weekday_to_int) + import functools import operator @@ -43,13 +50,6 @@ def as_timestamp(obj): return obj -def as_datetime(obj): - f = getattr(obj, 'to_pydatetime', None) - if f is not None: - obj = f() - return obj - - def apply_wraps(func): @functools.wraps(func) def wrapper(self, other): @@ -115,25 +115,10 @@ def wrapper(self, other): return wrapper -def _is_normalized(dt): - if (dt.hour != 0 or dt.minute != 0 or dt.second != 0 or - dt.microsecond != 0 or getattr(dt, 'nanosecond', 0) != 0): - return False - return True - # --------------------------------------------------------------------- # DateOffset -class ApplyTypeError(TypeError): - # sentinel class for catching the apply error to return NotImplemented - pass - - -class CacheableOffset(object): - _cacheable = True - - class DateOffset(object): """ Standard kind of date increment used for a date range. @@ -697,28 +682,11 @@ class BusinessHourMixin(BusinessMixin): def __init__(self, start='09:00', end='17:00', offset=timedelta(0)): # must be validated here to equality check kwds = {'offset': offset} - self.start = kwds['start'] = self._validate_time(start) - self.end = kwds['end'] = self._validate_time(end) + self.start = kwds['start'] = _validate_business_time(start) + self.end = kwds['end'] = _validate_business_time(end) self.kwds = kwds self._offset = offset - def _validate_time(self, t_input): - from datetime import time as dt_time - import time - if isinstance(t_input, compat.string_types): - try: - t = time.strptime(t_input, '%H:%M') - return dt_time(hour=t.tm_hour, minute=t.tm_min) - except ValueError: - raise ValueError("time data must match '%H:%M' format") - elif isinstance(t_input, dt_time): - if t_input.second != 0 or t_input.microsecond != 0: - raise ValueError( - "time data must be specified only with hour and minute") - return t_input - else: - raise ValueError("time data must be string or datetime.time") - def _get_daytime_flag(self): if self.start == self.end: raise ValueError('start and end must not be the same') @@ -1617,29 +1585,6 @@ def _from_name(cls, suffix=None): return cls(weekday=weekday) -class WeekDay(object): - MON = 0 - TUE = 1 - WED = 2 - THU = 3 - FRI = 4 - SAT = 5 - SUN = 6 - - -_int_to_weekday = { - WeekDay.MON: 'MON', - WeekDay.TUE: 'TUE', - WeekDay.WED: 'WED', - WeekDay.THU: 'THU', - WeekDay.FRI: 'FRI', - WeekDay.SAT: 'SAT', - WeekDay.SUN: 'SUN' -} - -_weekday_to_int = dict((v, k) for k, v in _int_to_weekday.items()) - - class WeekOfMonth(DateOffset): """ Describes monthly dates like "the Tuesday of the 2nd week of each month" @@ -2802,9 +2747,6 @@ def _delta_to_tick(delta): return Nano(nanos) -_delta_to_nanoseconds = tslib._delta_to_nanoseconds - - class Day(Tick): _inc = Timedelta(days=1) _prefix = 'D' @@ -2848,66 +2790,6 @@ class Nano(Tick): CDay = CustomBusinessDay # --------------------------------------------------------------------- -# Business Calendar helpers - - -def _get_calendar(weekmask, holidays, calendar): - """Generate busdaycalendar""" - if isinstance(calendar, np.busdaycalendar): - if not holidays: - holidays = tuple(calendar.holidays) - elif not isinstance(holidays, tuple): - holidays = tuple(holidays) - else: - # trust that calendar.holidays and holidays are - # consistent - pass - return calendar, holidays - - if holidays is None: - holidays = [] - try: - holidays = holidays + calendar.holidays().tolist() - except AttributeError: - pass - holidays = [_to_dt64(dt, dtype='datetime64[D]') for dt in holidays] - holidays = tuple(sorted(holidays)) - - kwargs = {'weekmask': weekmask} - if holidays: - kwargs['holidays'] = holidays - - busdaycalendar = np.busdaycalendar(**kwargs) - return busdaycalendar, holidays - - -def _to_dt64(dt, dtype='datetime64'): - # Currently - # > np.datetime64(dt.datetime(2013,5,1),dtype='datetime64[D]') - # numpy.datetime64('2013-05-01T02:00:00.000000+0200') - # Thus astype is needed to cast datetime to datetime64[D] - if getattr(dt, 'tzinfo', None) is not None: - i8 = tslib.pydt_to_i8(dt) - dt = tslib.tz_convert_single(i8, 'UTC', dt.tzinfo) - dt = Timestamp(dt) - dt = np.datetime64(dt) - if dt.dtype.name != dtype: - dt = dt.astype(dtype) - return dt - - -def _get_firstbday(wkday): - """ - wkday is the result of monthrange(year, month) - - If it's a saturday or sunday, increment first business day to reflect this - """ - first = 1 - if wkday == 5: # on Saturday - first = 3 - elif wkday == 6: # on Sunday - first = 2 - return first def generate_range(start=None, end=None, periods=None, diff --git a/setup.py b/setup.py index 5b9b13ee97acf..8b3ae40f01a10 100755 --- a/setup.py +++ b/setup.py @@ -347,6 +347,7 @@ class CheckSDist(sdist_class): 'pandas/_libs/tslibs/timedeltas.pyx', 'pandas/_libs/tslibs/timezones.pyx', 'pandas/_libs/tslibs/fields.pyx', + 'pandas/_libs/tslibs/offsets.pyx', 'pandas/_libs/tslibs/frequencies.pyx', 'pandas/_libs/tslibs/parsing.pyx', 'pandas/io/sas/sas.pyx'] @@ -489,6 +490,7 @@ def pxd(name): '_libs.tslibs.strptime': {'pyxfile': '_libs/tslibs/strptime', 'depends': tseries_depends, 'sources': npdt_srces}, + '_libs.tslibs.offsets': {'pyxfile': '_libs/tslibs/offsets'}, '_libs.tslib': {'pyxfile': '_libs/tslib', 'pxdfiles': ['_libs/src/util', '_libs/lib'], 'depends': tseries_depends,