offsets.py 82 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515
  1. # -*- coding: utf-8 -*-
  2. from datetime import date, datetime, timedelta
  3. import functools
  4. import operator
  5. from dateutil.easter import easter
  6. import numpy as np
  7. from pandas._libs.tslibs import (
  8. NaT, OutOfBoundsDatetime, Timedelta, Timestamp, ccalendar, conversion,
  9. delta_to_nanoseconds, frequencies as libfrequencies, normalize_date,
  10. offsets as liboffsets, timezones)
  11. from pandas._libs.tslibs.offsets import (
  12. ApplyTypeError, BaseOffset, _get_calendar, _is_normalized, _to_dt64,
  13. apply_index_wraps, as_datetime, roll_yearday, shift_month)
  14. import pandas.compat as compat
  15. from pandas.compat import range
  16. from pandas.errors import AbstractMethodError
  17. from pandas.util._decorators import cache_readonly
  18. from pandas.core.dtypes.generic import ABCPeriod
  19. from pandas.core.tools.datetimes import to_datetime
  20. __all__ = ['Day', 'BusinessDay', 'BDay', 'CustomBusinessDay', 'CDay',
  21. 'CBMonthEnd', 'CBMonthBegin',
  22. 'MonthBegin', 'BMonthBegin', 'MonthEnd', 'BMonthEnd',
  23. 'SemiMonthEnd', 'SemiMonthBegin',
  24. 'BusinessHour', 'CustomBusinessHour',
  25. 'YearBegin', 'BYearBegin', 'YearEnd', 'BYearEnd',
  26. 'QuarterBegin', 'BQuarterBegin', 'QuarterEnd', 'BQuarterEnd',
  27. 'LastWeekOfMonth', 'FY5253Quarter', 'FY5253',
  28. 'Week', 'WeekOfMonth', 'Easter',
  29. 'Hour', 'Minute', 'Second', 'Milli', 'Micro', 'Nano',
  30. 'DateOffset']
  31. # convert to/from datetime/timestamp to allow invalid Timestamp ranges to
  32. # pass thru
  33. def as_timestamp(obj):
  34. if isinstance(obj, Timestamp):
  35. return obj
  36. try:
  37. return Timestamp(obj)
  38. except (OutOfBoundsDatetime):
  39. pass
  40. return obj
  41. def apply_wraps(func):
  42. @functools.wraps(func)
  43. def wrapper(self, other):
  44. if other is NaT:
  45. return NaT
  46. elif isinstance(other, (timedelta, Tick, DateOffset)):
  47. # timedelta path
  48. return func(self, other)
  49. elif isinstance(other, (np.datetime64, datetime, date)):
  50. other = as_timestamp(other)
  51. tz = getattr(other, 'tzinfo', None)
  52. nano = getattr(other, 'nanosecond', 0)
  53. try:
  54. if self._adjust_dst and isinstance(other, Timestamp):
  55. other = other.tz_localize(None)
  56. result = func(self, other)
  57. if self._adjust_dst:
  58. result = conversion.localize_pydatetime(result, tz)
  59. result = Timestamp(result)
  60. if self.normalize:
  61. result = result.normalize()
  62. # nanosecond may be deleted depending on offset process
  63. if not self.normalize and nano != 0:
  64. if not isinstance(self, Nano) and result.nanosecond != nano:
  65. if result.tz is not None:
  66. # convert to UTC
  67. value = conversion.tz_convert_single(
  68. result.value, timezones.UTC, result.tz)
  69. else:
  70. value = result.value
  71. result = Timestamp(value + nano)
  72. if tz is not None and result.tzinfo is None:
  73. result = conversion.localize_pydatetime(result, tz)
  74. except OutOfBoundsDatetime:
  75. result = func(self, as_datetime(other))
  76. if self.normalize:
  77. # normalize_date returns normal datetime
  78. result = normalize_date(result)
  79. if tz is not None and result.tzinfo is None:
  80. result = conversion.localize_pydatetime(result, tz)
  81. return result
  82. return wrapper
  83. # ---------------------------------------------------------------------
  84. # DateOffset
  85. class DateOffset(BaseOffset):
  86. """
  87. Standard kind of date increment used for a date range.
  88. Works exactly like relativedelta in terms of the keyword args you
  89. pass in, use of the keyword n is discouraged-- you would be better
  90. off specifying n in the keywords you use, but regardless it is
  91. there for you. n is needed for DateOffset subclasses.
  92. DateOffets work as follows. Each offset specify a set of dates
  93. that conform to the DateOffset. For example, Bday defines this
  94. set to be the set of dates that are weekdays (M-F). To test if a
  95. date is in the set of a DateOffset dateOffset we can use the
  96. onOffset method: dateOffset.onOffset(date).
  97. If a date is not on a valid date, the rollback and rollforward
  98. methods can be used to roll the date to the nearest valid date
  99. before/after the date.
  100. DateOffsets can be created to move dates forward a given number of
  101. valid dates. For example, Bday(2) can be added to a date to move
  102. it two business days forward. If the date does not start on a
  103. valid date, first it is moved to a valid date. Thus pseudo code
  104. is:
  105. def __add__(date):
  106. date = rollback(date) # does nothing if date is valid
  107. return date + <n number of periods>
  108. When a date offset is created for a negative number of periods,
  109. the date is first rolled forward. The pseudo code is:
  110. def __add__(date):
  111. date = rollforward(date) # does nothing is date is valid
  112. return date + <n number of periods>
  113. Zero presents a problem. Should it roll forward or back? We
  114. arbitrarily have it rollforward:
  115. date + BDay(0) == BDay.rollforward(date)
  116. Since 0 is a bit weird, we suggest avoiding its use.
  117. Parameters
  118. ----------
  119. n : int, default 1
  120. The number of time periods the offset represents.
  121. normalize : bool, default False
  122. Whether to round the result of a DateOffset addition down to the
  123. previous midnight.
  124. **kwds
  125. Temporal parameter that add to or replace the offset value.
  126. Parameters that **add** to the offset (like Timedelta):
  127. - years
  128. - months
  129. - weeks
  130. - days
  131. - hours
  132. - minutes
  133. - seconds
  134. - microseconds
  135. - nanoseconds
  136. Parameters that **replace** the offset value:
  137. - year
  138. - month
  139. - day
  140. - weekday
  141. - hour
  142. - minute
  143. - second
  144. - microsecond
  145. - nanosecond
  146. See Also
  147. --------
  148. dateutil.relativedelta.relativedelta
  149. Examples
  150. --------
  151. >>> ts = pd.Timestamp('2017-01-01 09:10:11')
  152. >>> ts + DateOffset(months=3)
  153. Timestamp('2017-04-01 09:10:11')
  154. >>> ts = pd.Timestamp('2017-01-01 09:10:11')
  155. >>> ts + DateOffset(month=3)
  156. Timestamp('2017-03-01 09:10:11')
  157. """
  158. _params = cache_readonly(BaseOffset._params.fget)
  159. _use_relativedelta = False
  160. _adjust_dst = False
  161. _attributes = frozenset(['n', 'normalize'] +
  162. list(liboffsets.relativedelta_kwds))
  163. # default for prior pickles
  164. normalize = False
  165. def __init__(self, n=1, normalize=False, **kwds):
  166. BaseOffset.__init__(self, n, normalize)
  167. off, use_rd = liboffsets._determine_offset(kwds)
  168. object.__setattr__(self, "_offset", off)
  169. object.__setattr__(self, "_use_relativedelta", use_rd)
  170. for key in kwds:
  171. val = kwds[key]
  172. object.__setattr__(self, key, val)
  173. @apply_wraps
  174. def apply(self, other):
  175. if self._use_relativedelta:
  176. other = as_datetime(other)
  177. if len(self.kwds) > 0:
  178. tzinfo = getattr(other, 'tzinfo', None)
  179. if tzinfo is not None and self._use_relativedelta:
  180. # perform calculation in UTC
  181. other = other.replace(tzinfo=None)
  182. if self.n > 0:
  183. for i in range(self.n):
  184. other = other + self._offset
  185. else:
  186. for i in range(-self.n):
  187. other = other - self._offset
  188. if tzinfo is not None and self._use_relativedelta:
  189. # bring tz back from UTC calculation
  190. other = conversion.localize_pydatetime(other, tzinfo)
  191. return as_timestamp(other)
  192. else:
  193. return other + timedelta(self.n)
  194. @apply_index_wraps
  195. def apply_index(self, i):
  196. """
  197. Vectorized apply of DateOffset to DatetimeIndex,
  198. raises NotImplentedError for offsets without a
  199. vectorized implementation.
  200. Parameters
  201. ----------
  202. i : DatetimeIndex
  203. Returns
  204. -------
  205. y : DatetimeIndex
  206. """
  207. if type(self) is not DateOffset:
  208. raise NotImplementedError("DateOffset subclass {name} "
  209. "does not have a vectorized "
  210. "implementation".format(
  211. name=self.__class__.__name__))
  212. kwds = self.kwds
  213. relativedelta_fast = {'years', 'months', 'weeks', 'days', 'hours',
  214. 'minutes', 'seconds', 'microseconds'}
  215. # relativedelta/_offset path only valid for base DateOffset
  216. if (self._use_relativedelta and
  217. set(kwds).issubset(relativedelta_fast)):
  218. months = ((kwds.get('years', 0) * 12 +
  219. kwds.get('months', 0)) * self.n)
  220. if months:
  221. shifted = liboffsets.shift_months(i.asi8, months)
  222. i = type(i)(shifted, freq=i.freq, dtype=i.dtype)
  223. weeks = (kwds.get('weeks', 0)) * self.n
  224. if weeks:
  225. # integer addition on PeriodIndex is deprecated,
  226. # so we directly use _time_shift instead
  227. asper = i.to_period('W')
  228. if not isinstance(asper._data, np.ndarray):
  229. # unwrap PeriodIndex --> PeriodArray
  230. asper = asper._data
  231. shifted = asper._time_shift(weeks)
  232. i = shifted.to_timestamp() + i.to_perioddelta('W')
  233. timedelta_kwds = {k: v for k, v in kwds.items()
  234. if k in ['days', 'hours', 'minutes',
  235. 'seconds', 'microseconds']}
  236. if timedelta_kwds:
  237. delta = Timedelta(**timedelta_kwds)
  238. i = i + (self.n * delta)
  239. return i
  240. elif not self._use_relativedelta and hasattr(self, '_offset'):
  241. # timedelta
  242. return i + (self._offset * self.n)
  243. else:
  244. # relativedelta with other keywords
  245. kwd = set(kwds) - relativedelta_fast
  246. raise NotImplementedError("DateOffset with relativedelta "
  247. "keyword(s) {kwd} not able to be "
  248. "applied vectorized".format(kwd=kwd))
  249. def isAnchored(self):
  250. # TODO: Does this make sense for the general case? It would help
  251. # if there were a canonical docstring for what isAnchored means.
  252. return (self.n == 1)
  253. # TODO: Combine this with BusinessMixin version by defining a whitelisted
  254. # set of attributes on each object rather than the existing behavior of
  255. # iterating over internal ``__dict__``
  256. def _repr_attrs(self):
  257. exclude = {'n', 'inc', 'normalize'}
  258. attrs = []
  259. for attr in sorted(self.__dict__):
  260. if attr.startswith('_') or attr == 'kwds':
  261. continue
  262. elif attr not in exclude:
  263. value = getattr(self, attr)
  264. attrs.append('{attr}={value}'.format(attr=attr, value=value))
  265. out = ''
  266. if attrs:
  267. out += ': ' + ', '.join(attrs)
  268. return out
  269. @property
  270. def name(self):
  271. return self.rule_code
  272. def rollback(self, dt):
  273. """
  274. Roll provided date backward to next offset only if not on offset.
  275. """
  276. dt = as_timestamp(dt)
  277. if not self.onOffset(dt):
  278. dt = dt - self.__class__(1, normalize=self.normalize, **self.kwds)
  279. return dt
  280. def rollforward(self, dt):
  281. """
  282. Roll provided date forward to next offset only if not on offset.
  283. """
  284. dt = as_timestamp(dt)
  285. if not self.onOffset(dt):
  286. dt = dt + self.__class__(1, normalize=self.normalize, **self.kwds)
  287. return dt
  288. def onOffset(self, dt):
  289. if self.normalize and not _is_normalized(dt):
  290. return False
  291. # XXX, see #1395
  292. if type(self) == DateOffset or isinstance(self, Tick):
  293. return True
  294. # Default (slow) method for determining if some date is a member of the
  295. # date range generated by this offset. Subclasses may have this
  296. # re-implemented in a nicer way.
  297. a = dt
  298. b = ((dt + self) - self)
  299. return a == b
  300. # way to get around weirdness with rule_code
  301. @property
  302. def _prefix(self):
  303. raise NotImplementedError('Prefix not defined')
  304. @property
  305. def rule_code(self):
  306. return self._prefix
  307. @cache_readonly
  308. def freqstr(self):
  309. try:
  310. code = self.rule_code
  311. except NotImplementedError:
  312. return repr(self)
  313. if self.n != 1:
  314. fstr = '{n}{code}'.format(n=self.n, code=code)
  315. else:
  316. fstr = code
  317. try:
  318. if self._offset:
  319. fstr += self._offset_str()
  320. except AttributeError:
  321. # TODO: standardize `_offset` vs `offset` naming convention
  322. pass
  323. return fstr
  324. def _offset_str(self):
  325. return ''
  326. @property
  327. def nanos(self):
  328. raise ValueError("{name} is a non-fixed frequency".format(name=self))
  329. class SingleConstructorOffset(DateOffset):
  330. @classmethod
  331. def _from_name(cls, suffix=None):
  332. # default _from_name calls cls with no args
  333. if suffix:
  334. raise ValueError("Bad freq suffix {suffix}".format(suffix=suffix))
  335. return cls()
  336. class _CustomMixin(object):
  337. """
  338. Mixin for classes that define and validate calendar, holidays,
  339. and weekdays attributes.
  340. """
  341. def __init__(self, weekmask, holidays, calendar):
  342. calendar, holidays = _get_calendar(weekmask=weekmask,
  343. holidays=holidays,
  344. calendar=calendar)
  345. # Custom offset instances are identified by the
  346. # following two attributes. See DateOffset._params()
  347. # holidays, weekmask
  348. object.__setattr__(self, "weekmask", weekmask)
  349. object.__setattr__(self, "holidays", holidays)
  350. object.__setattr__(self, "calendar", calendar)
  351. class BusinessMixin(object):
  352. """
  353. Mixin to business types to provide related functions.
  354. """
  355. @property
  356. def offset(self):
  357. """
  358. Alias for self._offset.
  359. """
  360. # Alias for backward compat
  361. return self._offset
  362. def _repr_attrs(self):
  363. if self.offset:
  364. attrs = ['offset={offset!r}'.format(offset=self.offset)]
  365. else:
  366. attrs = None
  367. out = ''
  368. if attrs:
  369. out += ': ' + ', '.join(attrs)
  370. return out
  371. class BusinessDay(BusinessMixin, SingleConstructorOffset):
  372. """
  373. DateOffset subclass representing possibly n business days.
  374. """
  375. _prefix = 'B'
  376. _adjust_dst = True
  377. _attributes = frozenset(['n', 'normalize', 'offset'])
  378. def __init__(self, n=1, normalize=False, offset=timedelta(0)):
  379. BaseOffset.__init__(self, n, normalize)
  380. object.__setattr__(self, "_offset", offset)
  381. def _offset_str(self):
  382. def get_str(td):
  383. off_str = ''
  384. if td.days > 0:
  385. off_str += str(td.days) + 'D'
  386. if td.seconds > 0:
  387. s = td.seconds
  388. hrs = int(s / 3600)
  389. if hrs != 0:
  390. off_str += str(hrs) + 'H'
  391. s -= hrs * 3600
  392. mts = int(s / 60)
  393. if mts != 0:
  394. off_str += str(mts) + 'Min'
  395. s -= mts * 60
  396. if s != 0:
  397. off_str += str(s) + 's'
  398. if td.microseconds > 0:
  399. off_str += str(td.microseconds) + 'us'
  400. return off_str
  401. if isinstance(self.offset, timedelta):
  402. zero = timedelta(0, 0, 0)
  403. if self.offset >= zero:
  404. off_str = '+' + get_str(self.offset)
  405. else:
  406. off_str = '-' + get_str(-self.offset)
  407. return off_str
  408. else:
  409. return '+' + repr(self.offset)
  410. @apply_wraps
  411. def apply(self, other):
  412. if isinstance(other, datetime):
  413. n = self.n
  414. wday = other.weekday()
  415. # avoid slowness below by operating on weeks first
  416. weeks = n // 5
  417. if n <= 0 and wday > 4:
  418. # roll forward
  419. n += 1
  420. n -= 5 * weeks
  421. # n is always >= 0 at this point
  422. if n == 0 and wday > 4:
  423. # roll back
  424. days = 4 - wday
  425. elif wday > 4:
  426. # roll forward
  427. days = (7 - wday) + (n - 1)
  428. elif wday + n <= 4:
  429. # shift by n days without leaving the current week
  430. days = n
  431. else:
  432. # shift by n days plus 2 to get past the weekend
  433. days = n + 2
  434. result = other + timedelta(days=7 * weeks + days)
  435. if self.offset:
  436. result = result + self.offset
  437. return result
  438. elif isinstance(other, (timedelta, Tick)):
  439. return BDay(self.n, offset=self.offset + other,
  440. normalize=self.normalize)
  441. else:
  442. raise ApplyTypeError('Only know how to combine business day with '
  443. 'datetime or timedelta.')
  444. @apply_index_wraps
  445. def apply_index(self, i):
  446. time = i.to_perioddelta('D')
  447. # to_period rolls forward to next BDay; track and
  448. # reduce n where it does when rolling forward
  449. asper = i.to_period('B')
  450. if not isinstance(asper._data, np.ndarray):
  451. # unwrap PeriodIndex --> PeriodArray
  452. asper = asper._data
  453. if self.n > 0:
  454. shifted = (i.to_perioddelta('B') - time).asi8 != 0
  455. # Integer-array addition is deprecated, so we use
  456. # _time_shift directly
  457. roll = np.where(shifted, self.n - 1, self.n)
  458. shifted = asper._addsub_int_array(roll, operator.add)
  459. else:
  460. # Integer addition is deprecated, so we use _time_shift directly
  461. roll = self.n
  462. shifted = asper._time_shift(roll)
  463. result = shifted.to_timestamp() + time
  464. return result
  465. def onOffset(self, dt):
  466. if self.normalize and not _is_normalized(dt):
  467. return False
  468. return dt.weekday() < 5
  469. class BusinessHourMixin(BusinessMixin):
  470. def __init__(self, start='09:00', end='17:00', offset=timedelta(0)):
  471. # must be validated here to equality check
  472. start = liboffsets._validate_business_time(start)
  473. object.__setattr__(self, "start", start)
  474. end = liboffsets._validate_business_time(end)
  475. object.__setattr__(self, "end", end)
  476. object.__setattr__(self, "_offset", offset)
  477. @cache_readonly
  478. def next_bday(self):
  479. """
  480. Used for moving to next business day.
  481. """
  482. if self.n >= 0:
  483. nb_offset = 1
  484. else:
  485. nb_offset = -1
  486. if self._prefix.startswith('C'):
  487. # CustomBusinessHour
  488. return CustomBusinessDay(n=nb_offset,
  489. weekmask=self.weekmask,
  490. holidays=self.holidays,
  491. calendar=self.calendar)
  492. else:
  493. return BusinessDay(n=nb_offset)
  494. @cache_readonly
  495. def _get_daytime_flag(self):
  496. if self.start == self.end:
  497. raise ValueError('start and end must not be the same')
  498. elif self.start < self.end:
  499. return True
  500. else:
  501. return False
  502. def _next_opening_time(self, other):
  503. """
  504. If n is positive, return tomorrow's business day opening time.
  505. Otherwise yesterday's business day's opening time.
  506. Opening time always locates on BusinessDay.
  507. Otherwise, closing time may not if business hour extends over midnight.
  508. """
  509. if not self.next_bday.onOffset(other):
  510. other = other + self.next_bday
  511. else:
  512. if self.n >= 0 and self.start < other.time():
  513. other = other + self.next_bday
  514. elif self.n < 0 and other.time() < self.start:
  515. other = other + self.next_bday
  516. return datetime(other.year, other.month, other.day,
  517. self.start.hour, self.start.minute)
  518. def _prev_opening_time(self, other):
  519. """
  520. If n is positive, return yesterday's business day opening time.
  521. Otherwise yesterday business day's opening time.
  522. """
  523. if not self.next_bday.onOffset(other):
  524. other = other - self.next_bday
  525. else:
  526. if self.n >= 0 and other.time() < self.start:
  527. other = other - self.next_bday
  528. elif self.n < 0 and other.time() > self.start:
  529. other = other - self.next_bday
  530. return datetime(other.year, other.month, other.day,
  531. self.start.hour, self.start.minute)
  532. @cache_readonly
  533. def _get_business_hours_by_sec(self):
  534. """
  535. Return business hours in a day by seconds.
  536. """
  537. if self._get_daytime_flag:
  538. # create dummy datetime to calculate businesshours in a day
  539. dtstart = datetime(2014, 4, 1, self.start.hour, self.start.minute)
  540. until = datetime(2014, 4, 1, self.end.hour, self.end.minute)
  541. return (until - dtstart).total_seconds()
  542. else:
  543. dtstart = datetime(2014, 4, 1, self.start.hour, self.start.minute)
  544. until = datetime(2014, 4, 2, self.end.hour, self.end.minute)
  545. return (until - dtstart).total_seconds()
  546. @apply_wraps
  547. def rollback(self, dt):
  548. """
  549. Roll provided date backward to next offset only if not on offset.
  550. """
  551. if not self.onOffset(dt):
  552. businesshours = self._get_business_hours_by_sec
  553. if self.n >= 0:
  554. dt = self._prev_opening_time(
  555. dt) + timedelta(seconds=businesshours)
  556. else:
  557. dt = self._next_opening_time(
  558. dt) + timedelta(seconds=businesshours)
  559. return dt
  560. @apply_wraps
  561. def rollforward(self, dt):
  562. """
  563. Roll provided date forward to next offset only if not on offset.
  564. """
  565. if not self.onOffset(dt):
  566. if self.n >= 0:
  567. return self._next_opening_time(dt)
  568. else:
  569. return self._prev_opening_time(dt)
  570. return dt
  571. @apply_wraps
  572. def apply(self, other):
  573. daytime = self._get_daytime_flag
  574. businesshours = self._get_business_hours_by_sec
  575. bhdelta = timedelta(seconds=businesshours)
  576. if isinstance(other, datetime):
  577. # used for detecting edge condition
  578. nanosecond = getattr(other, 'nanosecond', 0)
  579. # reset timezone and nanosecond
  580. # other may be a Timestamp, thus not use replace
  581. other = datetime(other.year, other.month, other.day,
  582. other.hour, other.minute,
  583. other.second, other.microsecond)
  584. n = self.n
  585. if n >= 0:
  586. if (other.time() == self.end or
  587. not self._onOffset(other, businesshours)):
  588. other = self._next_opening_time(other)
  589. else:
  590. if other.time() == self.start:
  591. # adjustment to move to previous business day
  592. other = other - timedelta(seconds=1)
  593. if not self._onOffset(other, businesshours):
  594. other = self._next_opening_time(other)
  595. other = other + bhdelta
  596. bd, r = divmod(abs(n * 60), businesshours // 60)
  597. if n < 0:
  598. bd, r = -bd, -r
  599. if bd != 0:
  600. skip_bd = BusinessDay(n=bd)
  601. # midnight business hour may not on BusinessDay
  602. if not self.next_bday.onOffset(other):
  603. remain = other - self._prev_opening_time(other)
  604. other = self._next_opening_time(other + skip_bd) + remain
  605. else:
  606. other = other + skip_bd
  607. hours, minutes = divmod(r, 60)
  608. result = other + timedelta(hours=hours, minutes=minutes)
  609. # because of previous adjustment, time will be larger than start
  610. if ((daytime and (result.time() < self.start or
  611. self.end < result.time())) or
  612. not daytime and (self.end < result.time() < self.start)):
  613. if n >= 0:
  614. bday_edge = self._prev_opening_time(other)
  615. bday_edge = bday_edge + bhdelta
  616. # calculate remainder
  617. bday_remain = result - bday_edge
  618. result = self._next_opening_time(other)
  619. result += bday_remain
  620. else:
  621. bday_edge = self._next_opening_time(other)
  622. bday_remain = result - bday_edge
  623. result = self._next_opening_time(result) + bhdelta
  624. result += bday_remain
  625. # edge handling
  626. if n >= 0:
  627. if result.time() == self.end:
  628. result = self._next_opening_time(result)
  629. else:
  630. if result.time() == self.start and nanosecond == 0:
  631. # adjustment to move to previous business day
  632. result = self._next_opening_time(
  633. result - timedelta(seconds=1)) + bhdelta
  634. return result
  635. else:
  636. # TODO: Figure out the end of this sente
  637. raise ApplyTypeError(
  638. 'Only know how to combine business hour with ')
  639. def onOffset(self, dt):
  640. if self.normalize and not _is_normalized(dt):
  641. return False
  642. if dt.tzinfo is not None:
  643. dt = datetime(dt.year, dt.month, dt.day, dt.hour,
  644. dt.minute, dt.second, dt.microsecond)
  645. # Valid BH can be on the different BusinessDay during midnight
  646. # Distinguish by the time spent from previous opening time
  647. businesshours = self._get_business_hours_by_sec
  648. return self._onOffset(dt, businesshours)
  649. def _onOffset(self, dt, businesshours):
  650. """
  651. Slight speedups using calculated values.
  652. """
  653. # if self.normalize and not _is_normalized(dt):
  654. # return False
  655. # Valid BH can be on the different BusinessDay during midnight
  656. # Distinguish by the time spent from previous opening time
  657. if self.n >= 0:
  658. op = self._prev_opening_time(dt)
  659. else:
  660. op = self._next_opening_time(dt)
  661. span = (dt - op).total_seconds()
  662. if span <= businesshours:
  663. return True
  664. else:
  665. return False
  666. def _repr_attrs(self):
  667. out = super(BusinessHourMixin, self)._repr_attrs()
  668. start = self.start.strftime('%H:%M')
  669. end = self.end.strftime('%H:%M')
  670. attrs = ['{prefix}={start}-{end}'.format(prefix=self._prefix,
  671. start=start, end=end)]
  672. out += ': ' + ', '.join(attrs)
  673. return out
  674. class BusinessHour(BusinessHourMixin, SingleConstructorOffset):
  675. """
  676. DateOffset subclass representing possibly n business days.
  677. .. versionadded:: 0.16.1
  678. """
  679. _prefix = 'BH'
  680. _anchor = 0
  681. _attributes = frozenset(['n', 'normalize', 'start', 'end', 'offset'])
  682. def __init__(self, n=1, normalize=False, start='09:00',
  683. end='17:00', offset=timedelta(0)):
  684. BaseOffset.__init__(self, n, normalize)
  685. super(BusinessHour, self).__init__(start=start, end=end, offset=offset)
  686. class CustomBusinessDay(_CustomMixin, BusinessDay):
  687. """
  688. DateOffset subclass representing possibly n custom business days,
  689. excluding holidays.
  690. Parameters
  691. ----------
  692. n : int, default 1
  693. normalize : bool, default False
  694. Normalize start/end dates to midnight before generating date range
  695. weekmask : str, Default 'Mon Tue Wed Thu Fri'
  696. weekmask of valid business days, passed to ``numpy.busdaycalendar``
  697. holidays : list
  698. list/array of dates to exclude from the set of valid business days,
  699. passed to ``numpy.busdaycalendar``
  700. calendar : pd.HolidayCalendar or np.busdaycalendar
  701. offset : timedelta, default timedelta(0)
  702. """
  703. _prefix = 'C'
  704. _attributes = frozenset(['n', 'normalize',
  705. 'weekmask', 'holidays', 'calendar', 'offset'])
  706. def __init__(self, n=1, normalize=False, weekmask='Mon Tue Wed Thu Fri',
  707. holidays=None, calendar=None, offset=timedelta(0)):
  708. BaseOffset.__init__(self, n, normalize)
  709. object.__setattr__(self, "_offset", offset)
  710. _CustomMixin.__init__(self, weekmask, holidays, calendar)
  711. @apply_wraps
  712. def apply(self, other):
  713. if self.n <= 0:
  714. roll = 'forward'
  715. else:
  716. roll = 'backward'
  717. if isinstance(other, datetime):
  718. date_in = other
  719. np_dt = np.datetime64(date_in.date())
  720. np_incr_dt = np.busday_offset(np_dt, self.n, roll=roll,
  721. busdaycal=self.calendar)
  722. dt_date = np_incr_dt.astype(datetime)
  723. result = datetime.combine(dt_date, date_in.time())
  724. if self.offset:
  725. result = result + self.offset
  726. return result
  727. elif isinstance(other, (timedelta, Tick)):
  728. return BDay(self.n, offset=self.offset + other,
  729. normalize=self.normalize)
  730. else:
  731. raise ApplyTypeError('Only know how to combine trading day with '
  732. 'datetime, datetime64 or timedelta.')
  733. def apply_index(self, i):
  734. raise NotImplementedError
  735. def onOffset(self, dt):
  736. if self.normalize and not _is_normalized(dt):
  737. return False
  738. day64 = _to_dt64(dt, 'datetime64[D]')
  739. return np.is_busday(day64, busdaycal=self.calendar)
  740. class CustomBusinessHour(_CustomMixin, BusinessHourMixin,
  741. SingleConstructorOffset):
  742. """
  743. DateOffset subclass representing possibly n custom business days.
  744. .. versionadded:: 0.18.1
  745. """
  746. _prefix = 'CBH'
  747. _anchor = 0
  748. _attributes = frozenset(['n', 'normalize',
  749. 'weekmask', 'holidays', 'calendar',
  750. 'start', 'end', 'offset'])
  751. def __init__(self, n=1, normalize=False, weekmask='Mon Tue Wed Thu Fri',
  752. holidays=None, calendar=None,
  753. start='09:00', end='17:00', offset=timedelta(0)):
  754. BaseOffset.__init__(self, n, normalize)
  755. object.__setattr__(self, "_offset", offset)
  756. _CustomMixin.__init__(self, weekmask, holidays, calendar)
  757. BusinessHourMixin.__init__(self, start=start, end=end, offset=offset)
  758. # ---------------------------------------------------------------------
  759. # Month-Based Offset Classes
  760. class MonthOffset(SingleConstructorOffset):
  761. _adjust_dst = True
  762. _attributes = frozenset(['n', 'normalize'])
  763. __init__ = BaseOffset.__init__
  764. @property
  765. def name(self):
  766. if self.isAnchored:
  767. return self.rule_code
  768. else:
  769. month = ccalendar.MONTH_ALIASES[self.n]
  770. return "{code}-{month}".format(code=self.rule_code,
  771. month=month)
  772. def onOffset(self, dt):
  773. if self.normalize and not _is_normalized(dt):
  774. return False
  775. return dt.day == self._get_offset_day(dt)
  776. @apply_wraps
  777. def apply(self, other):
  778. compare_day = self._get_offset_day(other)
  779. n = liboffsets.roll_convention(other.day, self.n, compare_day)
  780. return shift_month(other, n, self._day_opt)
  781. @apply_index_wraps
  782. def apply_index(self, i):
  783. shifted = liboffsets.shift_months(i.asi8, self.n, self._day_opt)
  784. # TODO: going through __new__ raises on call to _validate_frequency;
  785. # are we passing incorrect freq?
  786. return type(i)._simple_new(shifted, freq=i.freq, dtype=i.dtype)
  787. class MonthEnd(MonthOffset):
  788. """
  789. DateOffset of one month end.
  790. """
  791. _prefix = 'M'
  792. _day_opt = 'end'
  793. class MonthBegin(MonthOffset):
  794. """
  795. DateOffset of one month at beginning.
  796. """
  797. _prefix = 'MS'
  798. _day_opt = 'start'
  799. class BusinessMonthEnd(MonthOffset):
  800. """
  801. DateOffset increments between business EOM dates.
  802. """
  803. _prefix = 'BM'
  804. _day_opt = 'business_end'
  805. class BusinessMonthBegin(MonthOffset):
  806. """
  807. DateOffset of one business month at beginning.
  808. """
  809. _prefix = 'BMS'
  810. _day_opt = 'business_start'
  811. class _CustomBusinessMonth(_CustomMixin, BusinessMixin, MonthOffset):
  812. """
  813. DateOffset subclass representing one custom business month, incrementing
  814. between [BEGIN/END] of month dates.
  815. Parameters
  816. ----------
  817. n : int, default 1
  818. normalize : bool, default False
  819. Normalize start/end dates to midnight before generating date range
  820. weekmask : str, Default 'Mon Tue Wed Thu Fri'
  821. weekmask of valid business days, passed to ``numpy.busdaycalendar``
  822. holidays : list
  823. list/array of dates to exclude from the set of valid business days,
  824. passed to ``numpy.busdaycalendar``
  825. calendar : pd.HolidayCalendar or np.busdaycalendar
  826. offset : timedelta, default timedelta(0)
  827. """
  828. _attributes = frozenset(['n', 'normalize',
  829. 'weekmask', 'holidays', 'calendar', 'offset'])
  830. onOffset = DateOffset.onOffset # override MonthOffset method
  831. apply_index = DateOffset.apply_index # override MonthOffset method
  832. def __init__(self, n=1, normalize=False, weekmask='Mon Tue Wed Thu Fri',
  833. holidays=None, calendar=None, offset=timedelta(0)):
  834. BaseOffset.__init__(self, n, normalize)
  835. object.__setattr__(self, "_offset", offset)
  836. _CustomMixin.__init__(self, weekmask, holidays, calendar)
  837. @cache_readonly
  838. def cbday_roll(self):
  839. """
  840. Define default roll function to be called in apply method.
  841. """
  842. cbday = CustomBusinessDay(n=self.n, normalize=False, **self.kwds)
  843. if self._prefix.endswith('S'):
  844. # MonthBegin
  845. roll_func = cbday.rollforward
  846. else:
  847. # MonthEnd
  848. roll_func = cbday.rollback
  849. return roll_func
  850. @cache_readonly
  851. def m_offset(self):
  852. if self._prefix.endswith('S'):
  853. # MonthBegin
  854. moff = MonthBegin(n=1, normalize=False)
  855. else:
  856. # MonthEnd
  857. moff = MonthEnd(n=1, normalize=False)
  858. return moff
  859. @cache_readonly
  860. def month_roll(self):
  861. """
  862. Define default roll function to be called in apply method.
  863. """
  864. if self._prefix.endswith('S'):
  865. # MonthBegin
  866. roll_func = self.m_offset.rollback
  867. else:
  868. # MonthEnd
  869. roll_func = self.m_offset.rollforward
  870. return roll_func
  871. @apply_wraps
  872. def apply(self, other):
  873. # First move to month offset
  874. cur_month_offset_date = self.month_roll(other)
  875. # Find this custom month offset
  876. compare_date = self.cbday_roll(cur_month_offset_date)
  877. n = liboffsets.roll_convention(other.day, self.n, compare_date.day)
  878. new = cur_month_offset_date + n * self.m_offset
  879. result = self.cbday_roll(new)
  880. return result
  881. class CustomBusinessMonthEnd(_CustomBusinessMonth):
  882. # TODO(py27): Replace condition with Subsitution after dropping Py27
  883. if _CustomBusinessMonth.__doc__:
  884. __doc__ = _CustomBusinessMonth.__doc__.replace('[BEGIN/END]', 'end')
  885. _prefix = 'CBM'
  886. class CustomBusinessMonthBegin(_CustomBusinessMonth):
  887. # TODO(py27): Replace condition with Subsitution after dropping Py27
  888. if _CustomBusinessMonth.__doc__:
  889. __doc__ = _CustomBusinessMonth.__doc__.replace('[BEGIN/END]',
  890. 'beginning')
  891. _prefix = 'CBMS'
  892. # ---------------------------------------------------------------------
  893. # Semi-Month Based Offset Classes
  894. class SemiMonthOffset(DateOffset):
  895. _adjust_dst = True
  896. _default_day_of_month = 15
  897. _min_day_of_month = 2
  898. _attributes = frozenset(['n', 'normalize', 'day_of_month'])
  899. def __init__(self, n=1, normalize=False, day_of_month=None):
  900. BaseOffset.__init__(self, n, normalize)
  901. if day_of_month is None:
  902. object.__setattr__(self, "day_of_month",
  903. self._default_day_of_month)
  904. else:
  905. object.__setattr__(self, "day_of_month", int(day_of_month))
  906. if not self._min_day_of_month <= self.day_of_month <= 27:
  907. msg = 'day_of_month must be {min}<=day_of_month<=27, got {day}'
  908. raise ValueError(msg.format(min=self._min_day_of_month,
  909. day=self.day_of_month))
  910. @classmethod
  911. def _from_name(cls, suffix=None):
  912. return cls(day_of_month=suffix)
  913. @property
  914. def rule_code(self):
  915. suffix = '-{day_of_month}'.format(day_of_month=self.day_of_month)
  916. return self._prefix + suffix
  917. @apply_wraps
  918. def apply(self, other):
  919. # shift `other` to self.day_of_month, incrementing `n` if necessary
  920. n = liboffsets.roll_convention(other.day, self.n, self.day_of_month)
  921. days_in_month = ccalendar.get_days_in_month(other.year, other.month)
  922. # For SemiMonthBegin on other.day == 1 and
  923. # SemiMonthEnd on other.day == days_in_month,
  924. # shifting `other` to `self.day_of_month` _always_ requires
  925. # incrementing/decrementing `n`, regardless of whether it is
  926. # initially positive.
  927. if type(self) is SemiMonthBegin and (self.n <= 0 and other.day == 1):
  928. n -= 1
  929. elif type(self) is SemiMonthEnd and (self.n > 0 and
  930. other.day == days_in_month):
  931. n += 1
  932. return self._apply(n, other)
  933. def _apply(self, n, other):
  934. """
  935. Handle specific apply logic for child classes.
  936. """
  937. raise AbstractMethodError(self)
  938. @apply_index_wraps
  939. def apply_index(self, i):
  940. # determine how many days away from the 1st of the month we are
  941. dti = i
  942. days_from_start = i.to_perioddelta('M').asi8
  943. delta = Timedelta(days=self.day_of_month - 1).value
  944. # get boolean array for each element before the day_of_month
  945. before_day_of_month = days_from_start < delta
  946. # get boolean array for each element after the day_of_month
  947. after_day_of_month = days_from_start > delta
  948. # determine the correct n for each date in i
  949. roll = self._get_roll(i, before_day_of_month, after_day_of_month)
  950. # isolate the time since it will be striped away one the next line
  951. time = i.to_perioddelta('D')
  952. # apply the correct number of months
  953. # integer-array addition on PeriodIndex is deprecated,
  954. # so we use _addsub_int_array directly
  955. asper = i.to_period('M')
  956. if not isinstance(asper._data, np.ndarray):
  957. # unwrap PeriodIndex --> PeriodArray
  958. asper = asper._data
  959. shifted = asper._addsub_int_array(roll // 2, operator.add)
  960. i = type(dti)(shifted.to_timestamp())
  961. # apply the correct day
  962. i = self._apply_index_days(i, roll)
  963. return i + time
  964. def _get_roll(self, i, before_day_of_month, after_day_of_month):
  965. """
  966. Return an array with the correct n for each date in i.
  967. The roll array is based on the fact that i gets rolled back to
  968. the first day of the month.
  969. """
  970. raise AbstractMethodError(self)
  971. def _apply_index_days(self, i, roll):
  972. """
  973. Apply the correct day for each date in i.
  974. """
  975. raise AbstractMethodError(self)
  976. class SemiMonthEnd(SemiMonthOffset):
  977. """
  978. Two DateOffset's per month repeating on the last
  979. day of the month and day_of_month.
  980. .. versionadded:: 0.19.0
  981. Parameters
  982. ----------
  983. n : int
  984. normalize : bool, default False
  985. day_of_month : int, {1, 3,...,27}, default 15
  986. """
  987. _prefix = 'SM'
  988. _min_day_of_month = 1
  989. def onOffset(self, dt):
  990. if self.normalize and not _is_normalized(dt):
  991. return False
  992. days_in_month = ccalendar.get_days_in_month(dt.year, dt.month)
  993. return dt.day in (self.day_of_month, days_in_month)
  994. def _apply(self, n, other):
  995. months = n // 2
  996. day = 31 if n % 2 else self.day_of_month
  997. return shift_month(other, months, day)
  998. def _get_roll(self, i, before_day_of_month, after_day_of_month):
  999. n = self.n
  1000. is_month_end = i.is_month_end
  1001. if n > 0:
  1002. roll_end = np.where(is_month_end, 1, 0)
  1003. roll_before = np.where(before_day_of_month, n, n + 1)
  1004. roll = roll_end + roll_before
  1005. elif n == 0:
  1006. roll_after = np.where(after_day_of_month, 2, 0)
  1007. roll_before = np.where(~after_day_of_month, 1, 0)
  1008. roll = roll_before + roll_after
  1009. else:
  1010. roll = np.where(after_day_of_month, n + 2, n + 1)
  1011. return roll
  1012. def _apply_index_days(self, i, roll):
  1013. """
  1014. Add days portion of offset to DatetimeIndex i.
  1015. Parameters
  1016. ----------
  1017. i : DatetimeIndex
  1018. roll : ndarray[int64_t]
  1019. Returns
  1020. -------
  1021. result : DatetimeIndex
  1022. """
  1023. nanos = (roll % 2) * Timedelta(days=self.day_of_month).value
  1024. i += nanos.astype('timedelta64[ns]')
  1025. return i + Timedelta(days=-1)
  1026. class SemiMonthBegin(SemiMonthOffset):
  1027. """
  1028. Two DateOffset's per month repeating on the first
  1029. day of the month and day_of_month.
  1030. .. versionadded:: 0.19.0
  1031. Parameters
  1032. ----------
  1033. n : int
  1034. normalize : bool, default False
  1035. day_of_month : int, {2, 3,...,27}, default 15
  1036. """
  1037. _prefix = 'SMS'
  1038. def onOffset(self, dt):
  1039. if self.normalize and not _is_normalized(dt):
  1040. return False
  1041. return dt.day in (1, self.day_of_month)
  1042. def _apply(self, n, other):
  1043. months = n // 2 + n % 2
  1044. day = 1 if n % 2 else self.day_of_month
  1045. return shift_month(other, months, day)
  1046. def _get_roll(self, i, before_day_of_month, after_day_of_month):
  1047. n = self.n
  1048. is_month_start = i.is_month_start
  1049. if n > 0:
  1050. roll = np.where(before_day_of_month, n, n + 1)
  1051. elif n == 0:
  1052. roll_start = np.where(is_month_start, 0, 1)
  1053. roll_after = np.where(after_day_of_month, 1, 0)
  1054. roll = roll_start + roll_after
  1055. else:
  1056. roll_after = np.where(after_day_of_month, n + 2, n + 1)
  1057. roll_start = np.where(is_month_start, -1, 0)
  1058. roll = roll_after + roll_start
  1059. return roll
  1060. def _apply_index_days(self, i, roll):
  1061. """
  1062. Add days portion of offset to DatetimeIndex i.
  1063. Parameters
  1064. ----------
  1065. i : DatetimeIndex
  1066. roll : ndarray[int64_t]
  1067. Returns
  1068. -------
  1069. result : DatetimeIndex
  1070. """
  1071. nanos = (roll % 2) * Timedelta(days=self.day_of_month - 1).value
  1072. return i + nanos.astype('timedelta64[ns]')
  1073. # ---------------------------------------------------------------------
  1074. # Week-Based Offset Classes
  1075. class Week(DateOffset):
  1076. """
  1077. Weekly offset.
  1078. Parameters
  1079. ----------
  1080. weekday : int, default None
  1081. Always generate specific day of week. 0 for Monday
  1082. """
  1083. _adjust_dst = True
  1084. _inc = timedelta(weeks=1)
  1085. _prefix = 'W'
  1086. _attributes = frozenset(['n', 'normalize', 'weekday'])
  1087. def __init__(self, n=1, normalize=False, weekday=None):
  1088. BaseOffset.__init__(self, n, normalize)
  1089. object.__setattr__(self, "weekday", weekday)
  1090. if self.weekday is not None:
  1091. if self.weekday < 0 or self.weekday > 6:
  1092. raise ValueError('Day must be 0<=day<=6, got {day}'
  1093. .format(day=self.weekday))
  1094. def isAnchored(self):
  1095. return (self.n == 1 and self.weekday is not None)
  1096. @apply_wraps
  1097. def apply(self, other):
  1098. if self.weekday is None:
  1099. return other + self.n * self._inc
  1100. k = self.n
  1101. otherDay = other.weekday()
  1102. if otherDay != self.weekday:
  1103. other = other + timedelta((self.weekday - otherDay) % 7)
  1104. if k > 0:
  1105. k -= 1
  1106. return other + timedelta(weeks=k)
  1107. @apply_index_wraps
  1108. def apply_index(self, i):
  1109. if self.weekday is None:
  1110. # integer addition on PeriodIndex is deprecated,
  1111. # so we use _time_shift directly
  1112. asper = i.to_period('W')
  1113. if not isinstance(asper._data, np.ndarray):
  1114. # unwrap PeriodIndex --> PeriodArray
  1115. asper = asper._data
  1116. shifted = asper._time_shift(self.n)
  1117. return shifted.to_timestamp() + i.to_perioddelta('W')
  1118. else:
  1119. return self._end_apply_index(i)
  1120. def _end_apply_index(self, dtindex):
  1121. """
  1122. Add self to the given DatetimeIndex, specialized for case where
  1123. self.weekday is non-null.
  1124. Parameters
  1125. ----------
  1126. dtindex : DatetimeIndex
  1127. Returns
  1128. -------
  1129. result : DatetimeIndex
  1130. """
  1131. off = dtindex.to_perioddelta('D')
  1132. base, mult = libfrequencies.get_freq_code(self.freqstr)
  1133. base_period = dtindex.to_period(base)
  1134. if not isinstance(base_period._data, np.ndarray):
  1135. # unwrap PeriodIndex --> PeriodArray
  1136. base_period = base_period._data
  1137. if self.n > 0:
  1138. # when adding, dates on end roll to next
  1139. normed = dtindex - off + Timedelta(1, 'D') - Timedelta(1, 'ns')
  1140. roll = np.where(base_period.to_timestamp(how='end') == normed,
  1141. self.n, self.n - 1)
  1142. # integer-array addition on PeriodIndex is deprecated,
  1143. # so we use _addsub_int_array directly
  1144. shifted = base_period._addsub_int_array(roll, operator.add)
  1145. base = shifted.to_timestamp(how='end')
  1146. else:
  1147. # integer addition on PeriodIndex is deprecated,
  1148. # so we use _time_shift directly
  1149. roll = self.n
  1150. base = base_period._time_shift(roll).to_timestamp(how='end')
  1151. return base + off + Timedelta(1, 'ns') - Timedelta(1, 'D')
  1152. def onOffset(self, dt):
  1153. if self.normalize and not _is_normalized(dt):
  1154. return False
  1155. elif self.weekday is None:
  1156. return True
  1157. return dt.weekday() == self.weekday
  1158. @property
  1159. def rule_code(self):
  1160. suffix = ''
  1161. if self.weekday is not None:
  1162. weekday = ccalendar.int_to_weekday[self.weekday]
  1163. suffix = '-{weekday}'.format(weekday=weekday)
  1164. return self._prefix + suffix
  1165. @classmethod
  1166. def _from_name(cls, suffix=None):
  1167. if not suffix:
  1168. weekday = None
  1169. else:
  1170. weekday = ccalendar.weekday_to_int[suffix]
  1171. return cls(weekday=weekday)
  1172. class _WeekOfMonthMixin(object):
  1173. """
  1174. Mixin for methods common to WeekOfMonth and LastWeekOfMonth.
  1175. """
  1176. @apply_wraps
  1177. def apply(self, other):
  1178. compare_day = self._get_offset_day(other)
  1179. months = self.n
  1180. if months > 0 and compare_day > other.day:
  1181. months -= 1
  1182. elif months <= 0 and compare_day < other.day:
  1183. months += 1
  1184. shifted = shift_month(other, months, 'start')
  1185. to_day = self._get_offset_day(shifted)
  1186. return liboffsets.shift_day(shifted, to_day - shifted.day)
  1187. def onOffset(self, dt):
  1188. if self.normalize and not _is_normalized(dt):
  1189. return False
  1190. return dt.day == self._get_offset_day(dt)
  1191. class WeekOfMonth(_WeekOfMonthMixin, DateOffset):
  1192. """
  1193. Describes monthly dates like "the Tuesday of the 2nd week of each month".
  1194. Parameters
  1195. ----------
  1196. n : int
  1197. week : {0, 1, 2, 3, ...}, default 0
  1198. 0 is 1st week of month, 1 2nd week, etc.
  1199. weekday : {0, 1, ..., 6}, default 0
  1200. 0: Mondays
  1201. 1: Tuesdays
  1202. 2: Wednesdays
  1203. 3: Thursdays
  1204. 4: Fridays
  1205. 5: Saturdays
  1206. 6: Sundays
  1207. """
  1208. _prefix = 'WOM'
  1209. _adjust_dst = True
  1210. _attributes = frozenset(['n', 'normalize', 'week', 'weekday'])
  1211. def __init__(self, n=1, normalize=False, week=0, weekday=0):
  1212. BaseOffset.__init__(self, n, normalize)
  1213. object.__setattr__(self, "weekday", weekday)
  1214. object.__setattr__(self, "week", week)
  1215. if self.weekday < 0 or self.weekday > 6:
  1216. raise ValueError('Day must be 0<=day<=6, got {day}'
  1217. .format(day=self.weekday))
  1218. if self.week < 0 or self.week > 3:
  1219. raise ValueError('Week must be 0<=week<=3, got {week}'
  1220. .format(week=self.week))
  1221. def _get_offset_day(self, other):
  1222. """
  1223. Find the day in the same month as other that has the same
  1224. weekday as self.weekday and is the self.week'th such day in the month.
  1225. Parameters
  1226. ----------
  1227. other : datetime
  1228. Returns
  1229. -------
  1230. day : int
  1231. """
  1232. mstart = datetime(other.year, other.month, 1)
  1233. wday = mstart.weekday()
  1234. shift_days = (self.weekday - wday) % 7
  1235. return 1 + shift_days + self.week * 7
  1236. @property
  1237. def rule_code(self):
  1238. weekday = ccalendar.int_to_weekday.get(self.weekday, '')
  1239. return '{prefix}-{week}{weekday}'.format(prefix=self._prefix,
  1240. week=self.week + 1,
  1241. weekday=weekday)
  1242. @classmethod
  1243. def _from_name(cls, suffix=None):
  1244. if not suffix:
  1245. raise ValueError("Prefix {prefix!r} requires a suffix."
  1246. .format(prefix=cls._prefix))
  1247. # TODO: handle n here...
  1248. # only one digit weeks (1 --> week 0, 2 --> week 1, etc.)
  1249. week = int(suffix[0]) - 1
  1250. weekday = ccalendar.weekday_to_int[suffix[1:]]
  1251. return cls(week=week, weekday=weekday)
  1252. class LastWeekOfMonth(_WeekOfMonthMixin, DateOffset):
  1253. """
  1254. Describes monthly dates in last week of month like "the last Tuesday of
  1255. each month".
  1256. Parameters
  1257. ----------
  1258. n : int, default 1
  1259. weekday : {0, 1, ..., 6}, default 0
  1260. 0: Mondays
  1261. 1: Tuesdays
  1262. 2: Wednesdays
  1263. 3: Thursdays
  1264. 4: Fridays
  1265. 5: Saturdays
  1266. 6: Sundays
  1267. """
  1268. _prefix = 'LWOM'
  1269. _adjust_dst = True
  1270. _attributes = frozenset(['n', 'normalize', 'weekday'])
  1271. def __init__(self, n=1, normalize=False, weekday=0):
  1272. BaseOffset.__init__(self, n, normalize)
  1273. object.__setattr__(self, "weekday", weekday)
  1274. if self.n == 0:
  1275. raise ValueError('N cannot be 0')
  1276. if self.weekday < 0 or self.weekday > 6:
  1277. raise ValueError('Day must be 0<=day<=6, got {day}'
  1278. .format(day=self.weekday))
  1279. def _get_offset_day(self, other):
  1280. """
  1281. Find the day in the same month as other that has the same
  1282. weekday as self.weekday and is the last such day in the month.
  1283. Parameters
  1284. ----------
  1285. other: datetime
  1286. Returns
  1287. -------
  1288. day: int
  1289. """
  1290. dim = ccalendar.get_days_in_month(other.year, other.month)
  1291. mend = datetime(other.year, other.month, dim)
  1292. wday = mend.weekday()
  1293. shift_days = (wday - self.weekday) % 7
  1294. return dim - shift_days
  1295. @property
  1296. def rule_code(self):
  1297. weekday = ccalendar.int_to_weekday.get(self.weekday, '')
  1298. return '{prefix}-{weekday}'.format(prefix=self._prefix,
  1299. weekday=weekday)
  1300. @classmethod
  1301. def _from_name(cls, suffix=None):
  1302. if not suffix:
  1303. raise ValueError("Prefix {prefix!r} requires a suffix."
  1304. .format(prefix=cls._prefix))
  1305. # TODO: handle n here...
  1306. weekday = ccalendar.weekday_to_int[suffix]
  1307. return cls(weekday=weekday)
  1308. # ---------------------------------------------------------------------
  1309. # Quarter-Based Offset Classes
  1310. class QuarterOffset(DateOffset):
  1311. """
  1312. Quarter representation - doesn't call super.
  1313. """
  1314. _default_startingMonth = None
  1315. _from_name_startingMonth = None
  1316. _adjust_dst = True
  1317. _attributes = frozenset(['n', 'normalize', 'startingMonth'])
  1318. # TODO: Consider combining QuarterOffset and YearOffset __init__ at some
  1319. # point. Also apply_index, onOffset, rule_code if
  1320. # startingMonth vs month attr names are resolved
  1321. def __init__(self, n=1, normalize=False, startingMonth=None):
  1322. BaseOffset.__init__(self, n, normalize)
  1323. if startingMonth is None:
  1324. startingMonth = self._default_startingMonth
  1325. object.__setattr__(self, "startingMonth", startingMonth)
  1326. def isAnchored(self):
  1327. return (self.n == 1 and self.startingMonth is not None)
  1328. @classmethod
  1329. def _from_name(cls, suffix=None):
  1330. kwargs = {}
  1331. if suffix:
  1332. kwargs['startingMonth'] = ccalendar.MONTH_TO_CAL_NUM[suffix]
  1333. else:
  1334. if cls._from_name_startingMonth is not None:
  1335. kwargs['startingMonth'] = cls._from_name_startingMonth
  1336. return cls(**kwargs)
  1337. @property
  1338. def rule_code(self):
  1339. month = ccalendar.MONTH_ALIASES[self.startingMonth]
  1340. return '{prefix}-{month}'.format(prefix=self._prefix, month=month)
  1341. @apply_wraps
  1342. def apply(self, other):
  1343. # months_since: find the calendar quarter containing other.month,
  1344. # e.g. if other.month == 8, the calendar quarter is [Jul, Aug, Sep].
  1345. # Then find the month in that quarter containing an onOffset date for
  1346. # self. `months_since` is the number of months to shift other.month
  1347. # to get to this on-offset month.
  1348. months_since = other.month % 3 - self.startingMonth % 3
  1349. qtrs = liboffsets.roll_qtrday(other, self.n, self.startingMonth,
  1350. day_opt=self._day_opt, modby=3)
  1351. months = qtrs * 3 - months_since
  1352. return shift_month(other, months, self._day_opt)
  1353. def onOffset(self, dt):
  1354. if self.normalize and not _is_normalized(dt):
  1355. return False
  1356. mod_month = (dt.month - self.startingMonth) % 3
  1357. return mod_month == 0 and dt.day == self._get_offset_day(dt)
  1358. @apply_index_wraps
  1359. def apply_index(self, dtindex):
  1360. shifted = liboffsets.shift_quarters(dtindex.asi8, self.n,
  1361. self.startingMonth, self._day_opt)
  1362. # TODO: going through __new__ raises on call to _validate_frequency;
  1363. # are we passing incorrect freq?
  1364. return type(dtindex)._simple_new(shifted, freq=dtindex.freq,
  1365. dtype=dtindex.dtype)
  1366. class BQuarterEnd(QuarterOffset):
  1367. """
  1368. DateOffset increments between business Quarter dates.
  1369. startingMonth = 1 corresponds to dates like 1/31/2007, 4/30/2007, ...
  1370. startingMonth = 2 corresponds to dates like 2/28/2007, 5/31/2007, ...
  1371. startingMonth = 3 corresponds to dates like 3/30/2007, 6/29/2007, ...
  1372. """
  1373. _outputName = 'BusinessQuarterEnd'
  1374. _default_startingMonth = 3
  1375. _from_name_startingMonth = 12
  1376. _prefix = 'BQ'
  1377. _day_opt = 'business_end'
  1378. # TODO: This is basically the same as BQuarterEnd
  1379. class BQuarterBegin(QuarterOffset):
  1380. _outputName = "BusinessQuarterBegin"
  1381. # I suspect this is wrong for *all* of them.
  1382. _default_startingMonth = 3
  1383. _from_name_startingMonth = 1
  1384. _prefix = 'BQS'
  1385. _day_opt = 'business_start'
  1386. class QuarterEnd(QuarterOffset):
  1387. """
  1388. DateOffset increments between business Quarter dates.
  1389. startingMonth = 1 corresponds to dates like 1/31/2007, 4/30/2007, ...
  1390. startingMonth = 2 corresponds to dates like 2/28/2007, 5/31/2007, ...
  1391. startingMonth = 3 corresponds to dates like 3/31/2007, 6/30/2007, ...
  1392. """
  1393. _outputName = 'QuarterEnd'
  1394. _default_startingMonth = 3
  1395. _prefix = 'Q'
  1396. _day_opt = 'end'
  1397. class QuarterBegin(QuarterOffset):
  1398. _outputName = 'QuarterBegin'
  1399. _default_startingMonth = 3
  1400. _from_name_startingMonth = 1
  1401. _prefix = 'QS'
  1402. _day_opt = 'start'
  1403. # ---------------------------------------------------------------------
  1404. # Year-Based Offset Classes
  1405. class YearOffset(DateOffset):
  1406. """
  1407. DateOffset that just needs a month.
  1408. """
  1409. _adjust_dst = True
  1410. _attributes = frozenset(['n', 'normalize', 'month'])
  1411. def _get_offset_day(self, other):
  1412. # override BaseOffset method to use self.month instead of other.month
  1413. # TODO: there may be a more performant way to do this
  1414. return liboffsets.get_day_of_month(other.replace(month=self.month),
  1415. self._day_opt)
  1416. @apply_wraps
  1417. def apply(self, other):
  1418. years = roll_yearday(other, self.n, self.month, self._day_opt)
  1419. months = years * 12 + (self.month - other.month)
  1420. return shift_month(other, months, self._day_opt)
  1421. @apply_index_wraps
  1422. def apply_index(self, dtindex):
  1423. shifted = liboffsets.shift_quarters(dtindex.asi8, self.n,
  1424. self.month, self._day_opt,
  1425. modby=12)
  1426. # TODO: going through __new__ raises on call to _validate_frequency;
  1427. # are we passing incorrect freq?
  1428. return type(dtindex)._simple_new(shifted, freq=dtindex.freq,
  1429. dtype=dtindex.dtype)
  1430. def onOffset(self, dt):
  1431. if self.normalize and not _is_normalized(dt):
  1432. return False
  1433. return dt.month == self.month and dt.day == self._get_offset_day(dt)
  1434. def __init__(self, n=1, normalize=False, month=None):
  1435. BaseOffset.__init__(self, n, normalize)
  1436. month = month if month is not None else self._default_month
  1437. object.__setattr__(self, "month", month)
  1438. if self.month < 1 or self.month > 12:
  1439. raise ValueError('Month must go from 1 to 12')
  1440. @classmethod
  1441. def _from_name(cls, suffix=None):
  1442. kwargs = {}
  1443. if suffix:
  1444. kwargs['month'] = ccalendar.MONTH_TO_CAL_NUM[suffix]
  1445. return cls(**kwargs)
  1446. @property
  1447. def rule_code(self):
  1448. month = ccalendar.MONTH_ALIASES[self.month]
  1449. return '{prefix}-{month}'.format(prefix=self._prefix, month=month)
  1450. class BYearEnd(YearOffset):
  1451. """
  1452. DateOffset increments between business EOM dates.
  1453. """
  1454. _outputName = 'BusinessYearEnd'
  1455. _default_month = 12
  1456. _prefix = 'BA'
  1457. _day_opt = 'business_end'
  1458. class BYearBegin(YearOffset):
  1459. """
  1460. DateOffset increments between business year begin dates.
  1461. """
  1462. _outputName = 'BusinessYearBegin'
  1463. _default_month = 1
  1464. _prefix = 'BAS'
  1465. _day_opt = 'business_start'
  1466. class YearEnd(YearOffset):
  1467. """
  1468. DateOffset increments between calendar year ends.
  1469. """
  1470. _default_month = 12
  1471. _prefix = 'A'
  1472. _day_opt = 'end'
  1473. class YearBegin(YearOffset):
  1474. """
  1475. DateOffset increments between calendar year begin dates.
  1476. """
  1477. _default_month = 1
  1478. _prefix = 'AS'
  1479. _day_opt = 'start'
  1480. # ---------------------------------------------------------------------
  1481. # Special Offset Classes
  1482. class FY5253(DateOffset):
  1483. """
  1484. Describes 52-53 week fiscal year. This is also known as a 4-4-5 calendar.
  1485. It is used by companies that desire that their
  1486. fiscal year always end on the same day of the week.
  1487. It is a method of managing accounting periods.
  1488. It is a common calendar structure for some industries,
  1489. such as retail, manufacturing and parking industry.
  1490. For more information see:
  1491. http://en.wikipedia.org/wiki/4-4-5_calendar
  1492. The year may either:
  1493. - end on the last X day of the Y month.
  1494. - end on the last X day closest to the last day of the Y month.
  1495. X is a specific day of the week.
  1496. Y is a certain month of the year
  1497. Parameters
  1498. ----------
  1499. n : int
  1500. weekday : {0, 1, ..., 6}
  1501. 0: Mondays
  1502. 1: Tuesdays
  1503. 2: Wednesdays
  1504. 3: Thursdays
  1505. 4: Fridays
  1506. 5: Saturdays
  1507. 6: Sundays
  1508. startingMonth : The month in which fiscal years end. {1, 2, ... 12}
  1509. variation : str
  1510. {"nearest", "last"} for "LastOfMonth" or "NearestEndMonth"
  1511. """
  1512. _prefix = 'RE'
  1513. _adjust_dst = True
  1514. _attributes = frozenset(['weekday', 'startingMonth', 'variation'])
  1515. def __init__(self, n=1, normalize=False, weekday=0, startingMonth=1,
  1516. variation="nearest"):
  1517. BaseOffset.__init__(self, n, normalize)
  1518. object.__setattr__(self, "startingMonth", startingMonth)
  1519. object.__setattr__(self, "weekday", weekday)
  1520. object.__setattr__(self, "variation", variation)
  1521. if self.n == 0:
  1522. raise ValueError('N cannot be 0')
  1523. if self.variation not in ["nearest", "last"]:
  1524. raise ValueError('{variation} is not a valid variation'
  1525. .format(variation=self.variation))
  1526. def isAnchored(self):
  1527. return (self.n == 1 and
  1528. self.startingMonth is not None and
  1529. self.weekday is not None)
  1530. def onOffset(self, dt):
  1531. if self.normalize and not _is_normalized(dt):
  1532. return False
  1533. dt = datetime(dt.year, dt.month, dt.day)
  1534. year_end = self.get_year_end(dt)
  1535. if self.variation == "nearest":
  1536. # We have to check the year end of "this" cal year AND the previous
  1537. return (year_end == dt or
  1538. self.get_year_end(shift_month(dt, -1, None)) == dt)
  1539. else:
  1540. return year_end == dt
  1541. @apply_wraps
  1542. def apply(self, other):
  1543. norm = Timestamp(other).normalize()
  1544. n = self.n
  1545. prev_year = self.get_year_end(
  1546. datetime(other.year - 1, self.startingMonth, 1))
  1547. cur_year = self.get_year_end(
  1548. datetime(other.year, self.startingMonth, 1))
  1549. next_year = self.get_year_end(
  1550. datetime(other.year + 1, self.startingMonth, 1))
  1551. prev_year = conversion.localize_pydatetime(prev_year, other.tzinfo)
  1552. cur_year = conversion.localize_pydatetime(cur_year, other.tzinfo)
  1553. next_year = conversion.localize_pydatetime(next_year, other.tzinfo)
  1554. # Note: next_year.year == other.year + 1, so we will always
  1555. # have other < next_year
  1556. if norm == prev_year:
  1557. n -= 1
  1558. elif norm == cur_year:
  1559. pass
  1560. elif n > 0:
  1561. if norm < prev_year:
  1562. n -= 2
  1563. elif prev_year < norm < cur_year:
  1564. n -= 1
  1565. elif cur_year < norm < next_year:
  1566. pass
  1567. else:
  1568. if cur_year < norm < next_year:
  1569. n += 1
  1570. elif prev_year < norm < cur_year:
  1571. pass
  1572. elif (norm.year == prev_year.year and norm < prev_year and
  1573. prev_year - norm <= timedelta(6)):
  1574. # GH#14774, error when next_year.year == cur_year.year
  1575. # e.g. prev_year == datetime(2004, 1, 3),
  1576. # other == datetime(2004, 1, 1)
  1577. n -= 1
  1578. else:
  1579. assert False
  1580. shifted = datetime(other.year + n, self.startingMonth, 1)
  1581. result = self.get_year_end(shifted)
  1582. result = datetime(result.year, result.month, result.day,
  1583. other.hour, other.minute, other.second,
  1584. other.microsecond)
  1585. return result
  1586. def get_year_end(self, dt):
  1587. assert dt.tzinfo is None
  1588. dim = ccalendar.get_days_in_month(dt.year, self.startingMonth)
  1589. target_date = datetime(dt.year, self.startingMonth, dim)
  1590. wkday_diff = self.weekday - target_date.weekday()
  1591. if wkday_diff == 0:
  1592. # year_end is the same for "last" and "nearest" cases
  1593. return target_date
  1594. if self.variation == "last":
  1595. days_forward = (wkday_diff % 7) - 7
  1596. # days_forward is always negative, so we always end up
  1597. # in the same year as dt
  1598. return target_date + timedelta(days=days_forward)
  1599. else:
  1600. # variation == "nearest":
  1601. days_forward = wkday_diff % 7
  1602. if days_forward <= 3:
  1603. # The upcoming self.weekday is closer than the previous one
  1604. return target_date + timedelta(days_forward)
  1605. else:
  1606. # The previous self.weekday is closer than the upcoming one
  1607. return target_date + timedelta(days_forward - 7)
  1608. @property
  1609. def rule_code(self):
  1610. prefix = self._prefix
  1611. suffix = self.get_rule_code_suffix()
  1612. return "{prefix}-{suffix}".format(prefix=prefix, suffix=suffix)
  1613. def _get_suffix_prefix(self):
  1614. if self.variation == "nearest":
  1615. return 'N'
  1616. else:
  1617. return 'L'
  1618. def get_rule_code_suffix(self):
  1619. prefix = self._get_suffix_prefix()
  1620. month = ccalendar.MONTH_ALIASES[self.startingMonth]
  1621. weekday = ccalendar.int_to_weekday[self.weekday]
  1622. return '{prefix}-{month}-{weekday}'.format(prefix=prefix, month=month,
  1623. weekday=weekday)
  1624. @classmethod
  1625. def _parse_suffix(cls, varion_code, startingMonth_code, weekday_code):
  1626. if varion_code == "N":
  1627. variation = "nearest"
  1628. elif varion_code == "L":
  1629. variation = "last"
  1630. else:
  1631. raise ValueError("Unable to parse varion_code: "
  1632. "{code}".format(code=varion_code))
  1633. startingMonth = ccalendar.MONTH_TO_CAL_NUM[startingMonth_code]
  1634. weekday = ccalendar.weekday_to_int[weekday_code]
  1635. return {"weekday": weekday,
  1636. "startingMonth": startingMonth,
  1637. "variation": variation}
  1638. @classmethod
  1639. def _from_name(cls, *args):
  1640. return cls(**cls._parse_suffix(*args))
  1641. class FY5253Quarter(DateOffset):
  1642. """
  1643. DateOffset increments between business quarter dates
  1644. for 52-53 week fiscal year (also known as a 4-4-5 calendar).
  1645. It is used by companies that desire that their
  1646. fiscal year always end on the same day of the week.
  1647. It is a method of managing accounting periods.
  1648. It is a common calendar structure for some industries,
  1649. such as retail, manufacturing and parking industry.
  1650. For more information see:
  1651. http://en.wikipedia.org/wiki/4-4-5_calendar
  1652. The year may either:
  1653. - end on the last X day of the Y month.
  1654. - end on the last X day closest to the last day of the Y month.
  1655. X is a specific day of the week.
  1656. Y is a certain month of the year
  1657. startingMonth = 1 corresponds to dates like 1/31/2007, 4/30/2007, ...
  1658. startingMonth = 2 corresponds to dates like 2/28/2007, 5/31/2007, ...
  1659. startingMonth = 3 corresponds to dates like 3/30/2007, 6/29/2007, ...
  1660. Parameters
  1661. ----------
  1662. n : int
  1663. weekday : {0, 1, ..., 6}
  1664. 0: Mondays
  1665. 1: Tuesdays
  1666. 2: Wednesdays
  1667. 3: Thursdays
  1668. 4: Fridays
  1669. 5: Saturdays
  1670. 6: Sundays
  1671. startingMonth : The month in which fiscal years end. {1, 2, ... 12}
  1672. qtr_with_extra_week : The quarter number that has the leap
  1673. or 14 week when needed. {1,2,3,4}
  1674. variation : str
  1675. {"nearest", "last"} for "LastOfMonth" or "NearestEndMonth"
  1676. """
  1677. _prefix = 'REQ'
  1678. _adjust_dst = True
  1679. _attributes = frozenset(['weekday', 'startingMonth', 'qtr_with_extra_week',
  1680. 'variation'])
  1681. def __init__(self, n=1, normalize=False, weekday=0, startingMonth=1,
  1682. qtr_with_extra_week=1, variation="nearest"):
  1683. BaseOffset.__init__(self, n, normalize)
  1684. object.__setattr__(self, "startingMonth", startingMonth)
  1685. object.__setattr__(self, "weekday", weekday)
  1686. object.__setattr__(self, "qtr_with_extra_week", qtr_with_extra_week)
  1687. object.__setattr__(self, "variation", variation)
  1688. if self.n == 0:
  1689. raise ValueError('N cannot be 0')
  1690. @cache_readonly
  1691. def _offset(self):
  1692. return FY5253(startingMonth=self.startingMonth,
  1693. weekday=self.weekday,
  1694. variation=self.variation)
  1695. def isAnchored(self):
  1696. return self.n == 1 and self._offset.isAnchored()
  1697. def _rollback_to_year(self, other):
  1698. """
  1699. Roll `other` back to the most recent date that was on a fiscal year
  1700. end.
  1701. Return the date of that year-end, the number of full quarters
  1702. elapsed between that year-end and other, and the remaining Timedelta
  1703. since the most recent quarter-end.
  1704. Parameters
  1705. ----------
  1706. other : datetime or Timestamp
  1707. Returns
  1708. -------
  1709. tuple of
  1710. prev_year_end : Timestamp giving most recent fiscal year end
  1711. num_qtrs : int
  1712. tdelta : Timedelta
  1713. """
  1714. num_qtrs = 0
  1715. norm = Timestamp(other).tz_localize(None)
  1716. start = self._offset.rollback(norm)
  1717. # Note: start <= norm and self._offset.onOffset(start)
  1718. if start < norm:
  1719. # roll adjustment
  1720. qtr_lens = self.get_weeks(norm)
  1721. # check thet qtr_lens is consistent with self._offset addition
  1722. end = liboffsets.shift_day(start, days=7 * sum(qtr_lens))
  1723. assert self._offset.onOffset(end), (start, end, qtr_lens)
  1724. tdelta = norm - start
  1725. for qlen in qtr_lens:
  1726. if qlen * 7 <= tdelta.days:
  1727. num_qtrs += 1
  1728. tdelta -= Timedelta(days=qlen * 7)
  1729. else:
  1730. break
  1731. else:
  1732. tdelta = Timedelta(0)
  1733. # Note: we always have tdelta.value >= 0
  1734. return start, num_qtrs, tdelta
  1735. @apply_wraps
  1736. def apply(self, other):
  1737. # Note: self.n == 0 is not allowed.
  1738. n = self.n
  1739. prev_year_end, num_qtrs, tdelta = self._rollback_to_year(other)
  1740. res = prev_year_end
  1741. n += num_qtrs
  1742. if self.n <= 0 and tdelta.value > 0:
  1743. n += 1
  1744. # Possible speedup by handling years first.
  1745. years = n // 4
  1746. if years:
  1747. res += self._offset * years
  1748. n -= years * 4
  1749. # Add an extra day to make *sure* we are getting the quarter lengths
  1750. # for the upcoming year, not the previous year
  1751. qtr_lens = self.get_weeks(res + Timedelta(days=1))
  1752. # Note: we always have 0 <= n < 4
  1753. weeks = sum(qtr_lens[:n])
  1754. if weeks:
  1755. res = liboffsets.shift_day(res, days=weeks * 7)
  1756. return res
  1757. def get_weeks(self, dt):
  1758. ret = [13] * 4
  1759. year_has_extra_week = self.year_has_extra_week(dt)
  1760. if year_has_extra_week:
  1761. ret[self.qtr_with_extra_week - 1] = 14
  1762. return ret
  1763. def year_has_extra_week(self, dt):
  1764. # Avoid round-down errors --> normalize to get
  1765. # e.g. '370D' instead of '360D23H'
  1766. norm = Timestamp(dt).normalize().tz_localize(None)
  1767. next_year_end = self._offset.rollforward(norm)
  1768. prev_year_end = norm - self._offset
  1769. weeks_in_year = (next_year_end - prev_year_end).days / 7
  1770. assert weeks_in_year in [52, 53], weeks_in_year
  1771. return weeks_in_year == 53
  1772. def onOffset(self, dt):
  1773. if self.normalize and not _is_normalized(dt):
  1774. return False
  1775. if self._offset.onOffset(dt):
  1776. return True
  1777. next_year_end = dt - self._offset
  1778. qtr_lens = self.get_weeks(dt)
  1779. current = next_year_end
  1780. for qtr_len in qtr_lens:
  1781. current = liboffsets.shift_day(current, days=qtr_len * 7)
  1782. if dt == current:
  1783. return True
  1784. return False
  1785. @property
  1786. def rule_code(self):
  1787. suffix = self._offset.get_rule_code_suffix()
  1788. qtr = self.qtr_with_extra_week
  1789. return "{prefix}-{suffix}-{qtr}".format(prefix=self._prefix,
  1790. suffix=suffix, qtr=qtr)
  1791. @classmethod
  1792. def _from_name(cls, *args):
  1793. return cls(**dict(FY5253._parse_suffix(*args[:-1]),
  1794. qtr_with_extra_week=int(args[-1])))
  1795. class Easter(DateOffset):
  1796. """
  1797. DateOffset for the Easter holiday using logic defined in dateutil.
  1798. Right now uses the revised method which is valid in years 1583-4099.
  1799. """
  1800. _adjust_dst = True
  1801. _attributes = frozenset(['n', 'normalize'])
  1802. __init__ = BaseOffset.__init__
  1803. @apply_wraps
  1804. def apply(self, other):
  1805. current_easter = easter(other.year)
  1806. current_easter = datetime(current_easter.year,
  1807. current_easter.month, current_easter.day)
  1808. current_easter = conversion.localize_pydatetime(current_easter,
  1809. other.tzinfo)
  1810. n = self.n
  1811. if n >= 0 and other < current_easter:
  1812. n -= 1
  1813. elif n < 0 and other > current_easter:
  1814. n += 1
  1815. # TODO: Why does this handle the 0 case the opposite of others?
  1816. # NOTE: easter returns a datetime.date so we have to convert to type of
  1817. # other
  1818. new = easter(other.year + n)
  1819. new = datetime(new.year, new.month, new.day, other.hour,
  1820. other.minute, other.second, other.microsecond)
  1821. return new
  1822. def onOffset(self, dt):
  1823. if self.normalize and not _is_normalized(dt):
  1824. return False
  1825. return date(dt.year, dt.month, dt.day) == easter(dt.year)
  1826. # ---------------------------------------------------------------------
  1827. # Ticks
  1828. def _tick_comp(op):
  1829. assert op not in [operator.eq, operator.ne]
  1830. def f(self, other):
  1831. try:
  1832. return op(self.delta, other.delta)
  1833. except AttributeError:
  1834. # comparing with a non-Tick object
  1835. raise TypeError("Invalid comparison between {cls} and {typ}"
  1836. .format(cls=type(self).__name__,
  1837. typ=type(other).__name__))
  1838. f.__name__ = '__{opname}__'.format(opname=op.__name__)
  1839. return f
  1840. class Tick(liboffsets._Tick, SingleConstructorOffset):
  1841. _inc = Timedelta(microseconds=1000)
  1842. _prefix = 'undefined'
  1843. _attributes = frozenset(['n', 'normalize'])
  1844. def __init__(self, n=1, normalize=False):
  1845. BaseOffset.__init__(self, n, normalize)
  1846. if normalize:
  1847. raise ValueError("Tick offset with `normalize=True` are not "
  1848. "allowed.") # GH#21427
  1849. __gt__ = _tick_comp(operator.gt)
  1850. __ge__ = _tick_comp(operator.ge)
  1851. __lt__ = _tick_comp(operator.lt)
  1852. __le__ = _tick_comp(operator.le)
  1853. def __add__(self, other):
  1854. if isinstance(other, Tick):
  1855. if type(self) == type(other):
  1856. return type(self)(self.n + other.n)
  1857. else:
  1858. return _delta_to_tick(self.delta + other.delta)
  1859. elif isinstance(other, ABCPeriod):
  1860. return other + self
  1861. try:
  1862. return self.apply(other)
  1863. except ApplyTypeError:
  1864. return NotImplemented
  1865. except OverflowError:
  1866. raise OverflowError("the add operation between {self} and {other} "
  1867. "will overflow".format(self=self, other=other))
  1868. def __eq__(self, other):
  1869. if isinstance(other, compat.string_types):
  1870. from pandas.tseries.frequencies import to_offset
  1871. try:
  1872. # GH#23524 if to_offset fails, we are dealing with an
  1873. # incomparable type so == is False and != is True
  1874. other = to_offset(other)
  1875. except ValueError:
  1876. # e.g. "infer"
  1877. return False
  1878. if isinstance(other, Tick):
  1879. return self.delta == other.delta
  1880. else:
  1881. return False
  1882. # This is identical to DateOffset.__hash__, but has to be redefined here
  1883. # for Python 3, because we've redefined __eq__.
  1884. def __hash__(self):
  1885. return hash(self._params)
  1886. def __ne__(self, other):
  1887. if isinstance(other, compat.string_types):
  1888. from pandas.tseries.frequencies import to_offset
  1889. try:
  1890. # GH#23524 if to_offset fails, we are dealing with an
  1891. # incomparable type so == is False and != is True
  1892. other = to_offset(other)
  1893. except ValueError:
  1894. # e.g. "infer"
  1895. return True
  1896. if isinstance(other, Tick):
  1897. return self.delta != other.delta
  1898. else:
  1899. return True
  1900. @property
  1901. def delta(self):
  1902. return self.n * self._inc
  1903. @property
  1904. def nanos(self):
  1905. return delta_to_nanoseconds(self.delta)
  1906. # TODO: Should Tick have its own apply_index?
  1907. def apply(self, other):
  1908. # Timestamp can handle tz and nano sec, thus no need to use apply_wraps
  1909. if isinstance(other, Timestamp):
  1910. # GH 15126
  1911. # in order to avoid a recursive
  1912. # call of __add__ and __radd__ if there is
  1913. # an exception, when we call using the + operator,
  1914. # we directly call the known method
  1915. result = other.__add__(self)
  1916. if result == NotImplemented:
  1917. raise OverflowError
  1918. return result
  1919. elif isinstance(other, (datetime, np.datetime64, date)):
  1920. return as_timestamp(other) + self
  1921. if isinstance(other, timedelta):
  1922. return other + self.delta
  1923. elif isinstance(other, type(self)):
  1924. return type(self)(self.n + other.n)
  1925. raise ApplyTypeError('Unhandled type: {type_str}'
  1926. .format(type_str=type(other).__name__))
  1927. def isAnchored(self):
  1928. return False
  1929. def _delta_to_tick(delta):
  1930. if delta.microseconds == 0 and getattr(delta, "nanoseconds", 0) == 0:
  1931. # nanoseconds only for pd.Timedelta
  1932. if delta.seconds == 0:
  1933. return Day(delta.days)
  1934. else:
  1935. seconds = delta.days * 86400 + delta.seconds
  1936. if seconds % 3600 == 0:
  1937. return Hour(seconds / 3600)
  1938. elif seconds % 60 == 0:
  1939. return Minute(seconds / 60)
  1940. else:
  1941. return Second(seconds)
  1942. else:
  1943. nanos = delta_to_nanoseconds(delta)
  1944. if nanos % 1000000 == 0:
  1945. return Milli(nanos // 1000000)
  1946. elif nanos % 1000 == 0:
  1947. return Micro(nanos // 1000)
  1948. else: # pragma: no cover
  1949. return Nano(nanos)
  1950. class Day(Tick):
  1951. _inc = Timedelta(days=1)
  1952. _prefix = 'D'
  1953. class Hour(Tick):
  1954. _inc = Timedelta(hours=1)
  1955. _prefix = 'H'
  1956. class Minute(Tick):
  1957. _inc = Timedelta(minutes=1)
  1958. _prefix = 'T'
  1959. class Second(Tick):
  1960. _inc = Timedelta(seconds=1)
  1961. _prefix = 'S'
  1962. class Milli(Tick):
  1963. _inc = Timedelta(milliseconds=1)
  1964. _prefix = 'L'
  1965. class Micro(Tick):
  1966. _inc = Timedelta(microseconds=1)
  1967. _prefix = 'U'
  1968. class Nano(Tick):
  1969. _inc = Timedelta(nanoseconds=1)
  1970. _prefix = 'N'
  1971. BDay = BusinessDay
  1972. BMonthEnd = BusinessMonthEnd
  1973. BMonthBegin = BusinessMonthBegin
  1974. CBMonthEnd = CustomBusinessMonthEnd
  1975. CBMonthBegin = CustomBusinessMonthBegin
  1976. CDay = CustomBusinessDay
  1977. # ---------------------------------------------------------------------
  1978. def generate_range(start=None, end=None, periods=None, offset=BDay()):
  1979. """
  1980. Generates a sequence of dates corresponding to the specified time
  1981. offset. Similar to dateutil.rrule except uses pandas DateOffset
  1982. objects to represent time increments.
  1983. Parameters
  1984. ----------
  1985. start : datetime (default None)
  1986. end : datetime (default None)
  1987. periods : int, (default None)
  1988. offset : DateOffset, (default BDay())
  1989. Notes
  1990. -----
  1991. * This method is faster for generating weekdays than dateutil.rrule
  1992. * At least two of (start, end, periods) must be specified.
  1993. * If both start and end are specified, the returned dates will
  1994. satisfy start <= date <= end.
  1995. Returns
  1996. -------
  1997. dates : generator object
  1998. """
  1999. from pandas.tseries.frequencies import to_offset
  2000. offset = to_offset(offset)
  2001. start = to_datetime(start)
  2002. end = to_datetime(end)
  2003. if start and not offset.onOffset(start):
  2004. start = offset.rollforward(start)
  2005. elif end and not offset.onOffset(end):
  2006. end = offset.rollback(end)
  2007. if periods is None and end < start and offset.n >= 0:
  2008. end = None
  2009. periods = 0
  2010. if end is None:
  2011. end = start + (periods - 1) * offset
  2012. if start is None:
  2013. start = end - (periods - 1) * offset
  2014. cur = start
  2015. if offset.n >= 0:
  2016. while cur <= end:
  2017. yield cur
  2018. # faster than cur + offset
  2019. next_date = offset.apply(cur)
  2020. if next_date <= cur:
  2021. raise ValueError('Offset {offset} did not increment date'
  2022. .format(offset=offset))
  2023. cur = next_date
  2024. else:
  2025. while cur >= end:
  2026. yield cur
  2027. # faster than cur + offset
  2028. next_date = offset.apply(cur)
  2029. if next_date >= cur:
  2030. raise ValueError('Offset {offset} did not decrement date'
  2031. .format(offset=offset))
  2032. cur = next_date
  2033. prefix_mapping = {offset._prefix: offset for offset in [
  2034. YearBegin, # 'AS'
  2035. YearEnd, # 'A'
  2036. BYearBegin, # 'BAS'
  2037. BYearEnd, # 'BA'
  2038. BusinessDay, # 'B'
  2039. BusinessMonthBegin, # 'BMS'
  2040. BusinessMonthEnd, # 'BM'
  2041. BQuarterEnd, # 'BQ'
  2042. BQuarterBegin, # 'BQS'
  2043. BusinessHour, # 'BH'
  2044. CustomBusinessDay, # 'C'
  2045. CustomBusinessMonthEnd, # 'CBM'
  2046. CustomBusinessMonthBegin, # 'CBMS'
  2047. CustomBusinessHour, # 'CBH'
  2048. MonthEnd, # 'M'
  2049. MonthBegin, # 'MS'
  2050. Nano, # 'N'
  2051. SemiMonthEnd, # 'SM'
  2052. SemiMonthBegin, # 'SMS'
  2053. Week, # 'W'
  2054. Second, # 'S'
  2055. Minute, # 'T'
  2056. Micro, # 'U'
  2057. QuarterEnd, # 'Q'
  2058. QuarterBegin, # 'QS'
  2059. Milli, # 'L'
  2060. Hour, # 'H'
  2061. Day, # 'D'
  2062. WeekOfMonth, # 'WOM'
  2063. FY5253,
  2064. FY5253Quarter
  2065. ]}