offsets.py 85 KB

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