offsets.pyx 140 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833383438353836383738383839384038413842384338443845384638473848384938503851385238533854385538563857385838593860386138623863386438653866386738683869387038713872387338743875387638773878387938803881388238833884388538863887388838893890389138923893389438953896389738983899390039013902390339043905390639073908390939103911391239133914391539163917391839193920392139223923392439253926392739283929393039313932393339343935393639373938393939403941394239433944394539463947394839493950395139523953395439553956395739583959396039613962396339643965396639673968396939703971397239733974397539763977397839793980398139823983398439853986398739883989399039913992399339943995399639973998399940004001400240034004400540064007400840094010401140124013401440154016401740184019402040214022402340244025402640274028402940304031403240334034403540364037403840394040404140424043404440454046404740484049405040514052405340544055405640574058405940604061406240634064406540664067406840694070407140724073407440754076407740784079408040814082408340844085408640874088408940904091409240934094409540964097409840994100410141024103410441054106410741084109411041114112411341144115411641174118411941204121412241234124412541264127412841294130413141324133413441354136413741384139414041414142414341444145414641474148414941504151415241534154415541564157415841594160416141624163416441654166416741684169417041714172417341744175417641774178417941804181418241834184418541864187418841894190419141924193419441954196419741984199420042014202420342044205420642074208420942104211421242134214421542164217421842194220422142224223422442254226422742284229423042314232423342344235423642374238423942404241424242434244424542464247424842494250425142524253425442554256425742584259426042614262426342644265426642674268426942704271427242734274427542764277427842794280428142824283428442854286428742884289429042914292429342944295429642974298429943004301430243034304430543064307430843094310431143124313431443154316431743184319432043214322432343244325432643274328432943304331433243334334433543364337433843394340434143424343434443454346434743484349435043514352435343544355435643574358435943604361436243634364436543664367436843694370437143724373437443754376437743784379438043814382438343844385438643874388438943904391439243934394439543964397439843994400440144024403440444054406440744084409441044114412441344144415441644174418441944204421442244234424442544264427442844294430443144324433443444354436443744384439444044414442444344444445444644474448444944504451445244534454445544564457445844594460446144624463446444654466446744684469447044714472447344744475447644774478447944804481448244834484448544864487448844894490449144924493449444954496449744984499450045014502450345044505450645074508450945104511451245134514451545164517451845194520452145224523452445254526452745284529453045314532453345344535453645374538453945404541454245434544454545464547454845494550455145524553455445554556455745584559456045614562456345644565456645674568456945704571457245734574457545764577457845794580458145824583458445854586458745884589459045914592459345944595
  1. import re
  2. import time
  3. cimport cython
  4. from cpython.datetime cimport (
  5. PyDate_Check,
  6. PyDateTime_Check,
  7. PyDelta_Check,
  8. date,
  9. datetime,
  10. import_datetime,
  11. time as dt_time,
  12. timedelta,
  13. )
  14. import_datetime()
  15. from dateutil.easter import easter
  16. from dateutil.relativedelta import relativedelta
  17. import numpy as np
  18. cimport numpy as cnp
  19. from numpy cimport (
  20. int64_t,
  21. ndarray,
  22. )
  23. cnp.import_array()
  24. # TODO: formalize having _libs.properties "above" tslibs in the dependency structure
  25. from pandas._libs.properties import cache_readonly
  26. from pandas._libs.tslibs cimport util
  27. from pandas._libs.tslibs.util cimport (
  28. is_datetime64_object,
  29. is_float_object,
  30. is_integer_object,
  31. )
  32. from pandas._libs.tslibs.ccalendar import (
  33. MONTH_ALIASES,
  34. MONTH_TO_CAL_NUM,
  35. int_to_weekday,
  36. weekday_to_int,
  37. )
  38. from pandas._libs.tslibs.ccalendar cimport (
  39. dayofweek,
  40. get_days_in_month,
  41. get_firstbday,
  42. get_lastbday,
  43. )
  44. from pandas._libs.tslibs.conversion cimport localize_pydatetime
  45. from pandas._libs.tslibs.dtypes cimport periods_per_day
  46. from pandas._libs.tslibs.nattype cimport (
  47. NPY_NAT,
  48. c_NaT as NaT,
  49. )
  50. from pandas._libs.tslibs.np_datetime cimport (
  51. NPY_DATETIMEUNIT,
  52. get_unit_from_dtype,
  53. npy_datetimestruct,
  54. npy_datetimestruct_to_datetime,
  55. pandas_datetime_to_datetimestruct,
  56. pydate_to_dtstruct,
  57. )
  58. from .dtypes cimport PeriodDtypeCode
  59. from .timedeltas cimport (
  60. _Timedelta,
  61. delta_to_nanoseconds,
  62. is_any_td_scalar,
  63. )
  64. from .timedeltas import Timedelta
  65. from .timestamps cimport _Timestamp
  66. from .timestamps import Timestamp
  67. # ---------------------------------------------------------------------
  68. # Misc Helpers
  69. cdef bint is_offset_object(object obj):
  70. return isinstance(obj, BaseOffset)
  71. cdef bint is_tick_object(object obj):
  72. return isinstance(obj, Tick)
  73. cdef datetime _as_datetime(datetime obj):
  74. if isinstance(obj, _Timestamp):
  75. return obj.to_pydatetime()
  76. return obj
  77. cdef bint _is_normalized(datetime dt):
  78. if dt.hour != 0 or dt.minute != 0 or dt.second != 0 or dt.microsecond != 0:
  79. # Regardless of whether dt is datetime vs Timestamp
  80. return False
  81. if isinstance(dt, _Timestamp):
  82. return dt.nanosecond == 0
  83. return True
  84. def apply_wrapper_core(func, self, other) -> ndarray:
  85. result = func(self, other)
  86. result = np.asarray(result)
  87. if self.normalize:
  88. # TODO: Avoid circular/runtime import
  89. from .vectorized import normalize_i8_timestamps
  90. reso = get_unit_from_dtype(other.dtype)
  91. result = normalize_i8_timestamps(result.view("i8"), None, reso=reso)
  92. return result
  93. def apply_array_wraps(func):
  94. # Note: normally we would use `@functools.wraps(func)`, but this does
  95. # not play nicely with cython class methods
  96. def wrapper(self, other) -> np.ndarray:
  97. # other is a DatetimeArray
  98. result = apply_wrapper_core(func, self, other)
  99. return result
  100. # do @functools.wraps(func) manually since it doesn't work on cdef funcs
  101. wrapper.__name__ = func.__name__
  102. wrapper.__doc__ = func.__doc__
  103. return wrapper
  104. def apply_wraps(func):
  105. # Note: normally we would use `@functools.wraps(func)`, but this does
  106. # not play nicely with cython class methods
  107. def wrapper(self, other):
  108. if other is NaT:
  109. return NaT
  110. elif (
  111. isinstance(other, BaseOffset)
  112. or PyDelta_Check(other)
  113. or util.is_timedelta64_object(other)
  114. ):
  115. # timedelta path
  116. return func(self, other)
  117. elif is_datetime64_object(other) or PyDate_Check(other):
  118. # PyDate_Check includes date, datetime
  119. other = Timestamp(other)
  120. else:
  121. # This will end up returning NotImplemented back in __add__
  122. raise ApplyTypeError
  123. tz = other.tzinfo
  124. nano = other.nanosecond
  125. if self._adjust_dst:
  126. other = other.tz_localize(None)
  127. result = func(self, other)
  128. result2 = Timestamp(result).as_unit(other.unit)
  129. if result == result2:
  130. # i.e. the conversion is non-lossy, not the case for e.g.
  131. # test_milliseconds_combination
  132. result = result2
  133. if self._adjust_dst:
  134. result = result.tz_localize(tz)
  135. if self.normalize:
  136. result = result.normalize()
  137. # If the offset object does not have a nanoseconds component,
  138. # the result's nanosecond component may be lost.
  139. if not self.normalize and nano != 0 and not hasattr(self, "nanoseconds"):
  140. if result.nanosecond != nano:
  141. if result.tz is not None:
  142. # convert to UTC
  143. res = result.tz_localize(None)
  144. else:
  145. res = result
  146. value = res.as_unit("ns")._value
  147. result = Timestamp(value + nano)
  148. if tz is not None and result.tzinfo is None:
  149. result = result.tz_localize(tz)
  150. return result
  151. # do @functools.wraps(func) manually since it doesn't work on cdef funcs
  152. wrapper.__name__ = func.__name__
  153. wrapper.__doc__ = func.__doc__
  154. return wrapper
  155. cdef _wrap_timedelta_result(result):
  156. """
  157. Tick operations dispatch to their Timedelta counterparts. Wrap the result
  158. of these operations in a Tick if possible.
  159. Parameters
  160. ----------
  161. result : object
  162. Returns
  163. -------
  164. object
  165. """
  166. if PyDelta_Check(result):
  167. # convert Timedelta back to a Tick
  168. return delta_to_tick(result)
  169. return result
  170. # ---------------------------------------------------------------------
  171. # Business Helpers
  172. cdef _get_calendar(weekmask, holidays, calendar):
  173. """
  174. Generate busdaycalendar
  175. """
  176. if isinstance(calendar, np.busdaycalendar):
  177. if not holidays:
  178. holidays = tuple(calendar.holidays)
  179. elif not isinstance(holidays, tuple):
  180. holidays = tuple(holidays)
  181. else:
  182. # trust that calendar.holidays and holidays are
  183. # consistent
  184. pass
  185. return calendar, holidays
  186. if holidays is None:
  187. holidays = []
  188. try:
  189. holidays = holidays + calendar.holidays().tolist()
  190. except AttributeError:
  191. pass
  192. holidays = [_to_dt64D(dt) for dt in holidays]
  193. holidays = tuple(sorted(holidays))
  194. kwargs = {"weekmask": weekmask}
  195. if holidays:
  196. kwargs["holidays"] = holidays
  197. busdaycalendar = np.busdaycalendar(**kwargs)
  198. return busdaycalendar, holidays
  199. cdef _to_dt64D(dt):
  200. # Currently
  201. # > np.datetime64(dt.datetime(2013,5,1),dtype='datetime64[D]')
  202. # numpy.datetime64('2013-05-01T02:00:00.000000+0200')
  203. # Thus astype is needed to cast datetime to datetime64[D]
  204. if getattr(dt, "tzinfo", None) is not None:
  205. # Get the nanosecond timestamp,
  206. # equiv `Timestamp(dt).value` or `dt.timestamp() * 10**9`
  207. # The `naive` must be the `dt` naive wall time
  208. # instead of the naive absolute time (GH#49441)
  209. naive = dt.replace(tzinfo=None)
  210. dt = np.datetime64(naive, "D")
  211. else:
  212. dt = np.datetime64(dt)
  213. if dt.dtype.name != "datetime64[D]":
  214. dt = dt.astype("datetime64[D]")
  215. return dt
  216. # ---------------------------------------------------------------------
  217. # Validation
  218. cdef _validate_business_time(t_input):
  219. if isinstance(t_input, str):
  220. try:
  221. t = time.strptime(t_input, "%H:%M")
  222. return dt_time(hour=t.tm_hour, minute=t.tm_min)
  223. except ValueError:
  224. raise ValueError("time data must match '%H:%M' format")
  225. elif isinstance(t_input, dt_time):
  226. if t_input.second != 0 or t_input.microsecond != 0:
  227. raise ValueError(
  228. "time data must be specified only with hour and minute")
  229. return t_input
  230. else:
  231. raise ValueError("time data must be string or datetime.time")
  232. # ---------------------------------------------------------------------
  233. # Constructor Helpers
  234. _relativedelta_kwds = {"years", "months", "weeks", "days", "year", "month",
  235. "day", "weekday", "hour", "minute", "second",
  236. "microsecond", "millisecond", "nanosecond",
  237. "nanoseconds", "hours", "minutes", "seconds",
  238. "milliseconds", "microseconds"}
  239. cdef _determine_offset(kwds):
  240. if not kwds:
  241. # GH 45643/45890: (historically) defaults to 1 day
  242. return timedelta(days=1), False
  243. if "millisecond" in kwds:
  244. raise NotImplementedError(
  245. "Using DateOffset to replace `millisecond` component in "
  246. "datetime object is not supported. Use "
  247. "`microsecond=timestamp.microsecond % 1000 + ms * 1000` "
  248. "instead."
  249. )
  250. nanos = {"nanosecond", "nanoseconds"}
  251. # nanos are handled by apply_wraps
  252. if all(k in nanos for k in kwds):
  253. return timedelta(days=0), False
  254. kwds_no_nanos = {k: v for k, v in kwds.items() if k not in nanos}
  255. kwds_use_relativedelta = {
  256. "year", "month", "day", "hour", "minute",
  257. "second", "microsecond", "weekday", "years", "months", "weeks", "days",
  258. "hours", "minutes", "seconds", "microseconds"
  259. }
  260. # "weeks" and "days" are left out despite being valid args for timedelta,
  261. # because (historically) timedelta is used only for sub-daily.
  262. kwds_use_timedelta = {
  263. "seconds", "microseconds", "milliseconds", "minutes", "hours",
  264. }
  265. if all(k in kwds_use_timedelta for k in kwds_no_nanos):
  266. # Sub-daily offset - use timedelta (tz-aware)
  267. # This also handles "milliseconds" (plur): see GH 49897
  268. return timedelta(**kwds_no_nanos), False
  269. # convert milliseconds to microseconds, so relativedelta can parse it
  270. if "milliseconds" in kwds_no_nanos:
  271. micro = kwds_no_nanos.pop("milliseconds") * 1000
  272. kwds_no_nanos["microseconds"] = kwds_no_nanos.get("microseconds", 0) + micro
  273. if all(k in kwds_use_relativedelta for k in kwds_no_nanos):
  274. return relativedelta(**kwds_no_nanos), True
  275. raise ValueError(
  276. f"Invalid argument/s or bad combination of arguments: {list(kwds.keys())}"
  277. )
  278. # ---------------------------------------------------------------------
  279. # Mixins & Singletons
  280. class ApplyTypeError(TypeError):
  281. # sentinel class for catching the apply error to return NotImplemented
  282. pass
  283. # ---------------------------------------------------------------------
  284. # Base Classes
  285. cdef class BaseOffset:
  286. """
  287. Base class for DateOffset methods that are not overridden by subclasses.
  288. Parameters
  289. ----------
  290. n : int
  291. Number of multiples of the frequency.
  292. normalize : bool
  293. Whether the frequency can align with midnight.
  294. Examples
  295. --------
  296. >>> pd.offsets.Hour(5).n
  297. 5
  298. >>> pd.offsets.Hour(5).normalize
  299. False
  300. """
  301. # ensure that reversed-ops with numpy scalars return NotImplemented
  302. __array_priority__ = 1000
  303. _day_opt = None
  304. _attributes = tuple(["n", "normalize"])
  305. _use_relativedelta = False
  306. _adjust_dst = True
  307. # cdef readonly:
  308. # int64_t n
  309. # bint normalize
  310. # dict _cache
  311. def __init__(self, n=1, normalize=False):
  312. n = self._validate_n(n)
  313. self.n = n
  314. self.normalize = normalize
  315. self._cache = {}
  316. def __eq__(self, other) -> bool:
  317. if isinstance(other, str):
  318. try:
  319. # GH#23524 if to_offset fails, we are dealing with an
  320. # incomparable type so == is False and != is True
  321. other = to_offset(other)
  322. except ValueError:
  323. # e.g. "infer"
  324. return False
  325. try:
  326. return self._params == other._params
  327. except AttributeError:
  328. # other is not a DateOffset object
  329. return False
  330. def __ne__(self, other):
  331. return not self == other
  332. def __hash__(self) -> int:
  333. return hash(self._params)
  334. @cache_readonly
  335. def _params(self):
  336. """
  337. Returns a tuple containing all of the attributes needed to evaluate
  338. equality between two DateOffset objects.
  339. """
  340. d = getattr(self, "__dict__", {})
  341. all_paras = d.copy()
  342. all_paras["n"] = self.n
  343. all_paras["normalize"] = self.normalize
  344. for attr in self._attributes:
  345. if hasattr(self, attr) and attr not in d:
  346. # cython attributes are not in __dict__
  347. all_paras[attr] = getattr(self, attr)
  348. if "holidays" in all_paras and not all_paras["holidays"]:
  349. all_paras.pop("holidays")
  350. exclude = ["kwds", "name", "calendar"]
  351. attrs = [(k, v) for k, v in all_paras.items()
  352. if (k not in exclude) and (k[0] != "_")]
  353. attrs = sorted(set(attrs))
  354. params = tuple([str(type(self))] + attrs)
  355. return params
  356. @property
  357. def kwds(self) -> dict:
  358. """
  359. Return a dict of extra parameters for the offset.
  360. Examples
  361. --------
  362. >>> pd.DateOffset(5).kwds
  363. {}
  364. >>> pd.offsets.FY5253Quarter().kwds
  365. {'weekday': 0,
  366. 'startingMonth': 1,
  367. 'qtr_with_extra_week': 1,
  368. 'variation': 'nearest'}
  369. """
  370. # for backwards-compatibility
  371. kwds = {name: getattr(self, name, None) for name in self._attributes
  372. if name not in ["n", "normalize"]}
  373. return {name: kwds[name] for name in kwds if kwds[name] is not None}
  374. @property
  375. def base(self):
  376. """
  377. Returns a copy of the calling offset object with n=1 and all other
  378. attributes equal.
  379. """
  380. return type(self)(n=1, normalize=self.normalize, **self.kwds)
  381. def __add__(self, other):
  382. if not isinstance(self, BaseOffset):
  383. # cython semantics; this is __radd__
  384. # TODO(cython3): remove this, this moved to __radd__
  385. return other.__add__(self)
  386. elif util.is_array(other) and other.dtype == object:
  387. return np.array([self + x for x in other])
  388. try:
  389. return self._apply(other)
  390. except ApplyTypeError:
  391. return NotImplemented
  392. def __radd__(self, other):
  393. return self.__add__(other)
  394. def __sub__(self, other):
  395. if PyDateTime_Check(other):
  396. raise TypeError("Cannot subtract datetime from offset.")
  397. elif type(other) == type(self):
  398. return type(self)(self.n - other.n, normalize=self.normalize,
  399. **self.kwds)
  400. elif not isinstance(self, BaseOffset):
  401. # TODO(cython3): remove, this moved to __rsub__
  402. # cython semantics, this is __rsub__
  403. return (-other).__add__(self)
  404. else:
  405. # e.g. PeriodIndex
  406. return NotImplemented
  407. def __rsub__(self, other):
  408. return (-self).__add__(other)
  409. def __mul__(self, other):
  410. if util.is_array(other):
  411. return np.array([self * x for x in other])
  412. elif is_integer_object(other):
  413. return type(self)(n=other * self.n, normalize=self.normalize,
  414. **self.kwds)
  415. elif not isinstance(self, BaseOffset):
  416. # TODO(cython3): remove this, this moved to __rmul__
  417. # cython semantics, this is __rmul__
  418. return other.__mul__(self)
  419. return NotImplemented
  420. def __rmul__(self, other):
  421. return self.__mul__(other)
  422. def __neg__(self):
  423. # Note: we are deferring directly to __mul__ instead of __rmul__, as
  424. # that allows us to use methods that can go in a `cdef class`
  425. return self * -1
  426. def copy(self):
  427. # Note: we are deferring directly to __mul__ instead of __rmul__, as
  428. # that allows us to use methods that can go in a `cdef class`
  429. """
  430. Return a copy of the frequency.
  431. Examples
  432. --------
  433. >>> freq = pd.DateOffset(1)
  434. >>> freq_copy = freq.copy()
  435. >>> freq is freq_copy
  436. False
  437. """
  438. return self * 1
  439. # ------------------------------------------------------------------
  440. # Name and Rendering Methods
  441. def __repr__(self) -> str:
  442. # _output_name used by B(Year|Quarter)(End|Begin) to
  443. # expand "B" -> "Business"
  444. class_name = getattr(self, "_output_name", type(self).__name__)
  445. if abs(self.n) != 1:
  446. plural = "s"
  447. else:
  448. plural = ""
  449. n_str = ""
  450. if self.n != 1:
  451. n_str = f"{self.n} * "
  452. out = f"<{n_str}{class_name}{plural}{self._repr_attrs()}>"
  453. return out
  454. def _repr_attrs(self) -> str:
  455. exclude = {"n", "inc", "normalize"}
  456. attrs = []
  457. for attr in sorted(self._attributes):
  458. # _attributes instead of __dict__ because cython attrs are not in __dict__
  459. if attr.startswith("_") or attr == "kwds" or not hasattr(self, attr):
  460. # DateOffset may not have some of these attributes
  461. continue
  462. elif attr not in exclude:
  463. value = getattr(self, attr)
  464. attrs.append(f"{attr}={value}")
  465. out = ""
  466. if attrs:
  467. out += ": " + ", ".join(attrs)
  468. return out
  469. @property
  470. def name(self) -> str:
  471. """
  472. Return a string representing the base frequency.
  473. Examples
  474. --------
  475. >>> pd.offsets.Hour().name
  476. 'H'
  477. >>> pd.offsets.Hour(5).name
  478. 'H'
  479. """
  480. return self.rule_code
  481. @property
  482. def _prefix(self) -> str:
  483. raise NotImplementedError("Prefix not defined")
  484. @property
  485. def rule_code(self) -> str:
  486. return self._prefix
  487. @cache_readonly
  488. def freqstr(self) -> str:
  489. """
  490. Return a string representing the frequency.
  491. Examples
  492. --------
  493. >>> pd.DateOffset(5).freqstr
  494. '<5 * DateOffsets>'
  495. >>> pd.offsets.BusinessHour(2).freqstr
  496. '2BH'
  497. >>> pd.offsets.Nano().freqstr
  498. 'N'
  499. >>> pd.offsets.Nano(-3).freqstr
  500. '-3N'
  501. """
  502. try:
  503. code = self.rule_code
  504. except NotImplementedError:
  505. return str(repr(self))
  506. if self.n != 1:
  507. fstr = f"{self.n}{code}"
  508. else:
  509. fstr = code
  510. try:
  511. if self._offset:
  512. fstr += self._offset_str()
  513. except AttributeError:
  514. # TODO: standardize `_offset` vs `offset` naming convention
  515. pass
  516. return fstr
  517. def _offset_str(self) -> str:
  518. return ""
  519. # ------------------------------------------------------------------
  520. def _apply(self, other):
  521. raise NotImplementedError("implemented by subclasses")
  522. @apply_array_wraps
  523. def _apply_array(self, dtarr):
  524. raise NotImplementedError(
  525. f"DateOffset subclass {type(self).__name__} "
  526. "does not have a vectorized implementation"
  527. )
  528. def rollback(self, dt) -> datetime:
  529. """
  530. Roll provided date backward to next offset only if not on offset.
  531. Returns
  532. -------
  533. TimeStamp
  534. Rolled timestamp if not on offset, otherwise unchanged timestamp.
  535. """
  536. dt = Timestamp(dt)
  537. if not self.is_on_offset(dt):
  538. dt = dt - type(self)(1, normalize=self.normalize, **self.kwds)
  539. return dt
  540. def rollforward(self, dt) -> datetime:
  541. """
  542. Roll provided date forward to next offset only if not on offset.
  543. Returns
  544. -------
  545. TimeStamp
  546. Rolled timestamp if not on offset, otherwise unchanged timestamp.
  547. """
  548. dt = Timestamp(dt)
  549. if not self.is_on_offset(dt):
  550. dt = dt + type(self)(1, normalize=self.normalize, **self.kwds)
  551. return dt
  552. def _get_offset_day(self, other: datetime) -> int:
  553. # subclass must implement `_day_opt`; calling from the base class
  554. # will implicitly assume day_opt = "business_end", see get_day_of_month.
  555. cdef:
  556. npy_datetimestruct dts
  557. pydate_to_dtstruct(other, &dts)
  558. return get_day_of_month(&dts, self._day_opt)
  559. def is_on_offset(self, dt: datetime) -> bool:
  560. """
  561. Return boolean whether a timestamp intersects with this frequency.
  562. Parameters
  563. ----------
  564. dt : datetime.datetime
  565. Timestamp to check intersections with frequency.
  566. Examples
  567. --------
  568. >>> ts = pd.Timestamp(2022, 1, 1)
  569. >>> freq = pd.offsets.Day(1)
  570. >>> freq.is_on_offset(ts)
  571. True
  572. >>> ts = pd.Timestamp(2022, 8, 6)
  573. >>> ts.day_name()
  574. 'Saturday'
  575. >>> freq = pd.offsets.BusinessDay(1)
  576. >>> freq.is_on_offset(ts)
  577. False
  578. """
  579. if self.normalize and not _is_normalized(dt):
  580. return False
  581. # Default (slow) method for determining if some date is a member of the
  582. # date range generated by this offset. Subclasses may have this
  583. # re-implemented in a nicer way.
  584. a = dt
  585. b = (dt + self) - self
  586. return a == b
  587. # ------------------------------------------------------------------
  588. # Staticmethod so we can call from Tick.__init__, will be unnecessary
  589. # once BaseOffset is a cdef class and is inherited by Tick
  590. @staticmethod
  591. def _validate_n(n) -> int:
  592. """
  593. Require that `n` be an integer.
  594. Parameters
  595. ----------
  596. n : int
  597. Returns
  598. -------
  599. nint : int
  600. Raises
  601. ------
  602. TypeError if `int(n)` raises
  603. ValueError if n != int(n)
  604. """
  605. if util.is_timedelta64_object(n):
  606. raise TypeError(f"`n` argument must be an integer, got {type(n)}")
  607. try:
  608. nint = int(n)
  609. except (ValueError, TypeError):
  610. raise TypeError(f"`n` argument must be an integer, got {type(n)}")
  611. if n != nint:
  612. raise ValueError(f"`n` argument must be an integer, got {n}")
  613. return nint
  614. def __setstate__(self, state):
  615. """
  616. Reconstruct an instance from a pickled state
  617. """
  618. self.n = state.pop("n")
  619. self.normalize = state.pop("normalize")
  620. self._cache = state.pop("_cache", {})
  621. # At this point we expect state to be empty
  622. def __getstate__(self):
  623. """
  624. Return a pickleable state
  625. """
  626. state = {}
  627. state["n"] = self.n
  628. state["normalize"] = self.normalize
  629. # we don't want to actually pickle the calendar object
  630. # as its a np.busyday; we recreate on deserialization
  631. state.pop("calendar", None)
  632. if "kwds" in state:
  633. state["kwds"].pop("calendar", None)
  634. return state
  635. @property
  636. def nanos(self):
  637. raise ValueError(f"{self} is a non-fixed frequency")
  638. def is_anchored(self) -> bool:
  639. # TODO: Does this make sense for the general case? It would help
  640. # if there were a canonical docstring for what is_anchored means.
  641. """
  642. Return boolean whether the frequency is a unit frequency (n=1).
  643. Examples
  644. --------
  645. >>> pd.DateOffset().is_anchored()
  646. True
  647. >>> pd.DateOffset(2).is_anchored()
  648. False
  649. """
  650. return self.n == 1
  651. # ------------------------------------------------------------------
  652. def is_month_start(self, _Timestamp ts):
  653. """
  654. Return boolean whether a timestamp occurs on the month start.
  655. Examples
  656. --------
  657. >>> ts = pd.Timestamp(2022, 1, 1)
  658. >>> freq = pd.offsets.Hour(5)
  659. >>> freq.is_month_start(ts)
  660. True
  661. """
  662. return ts._get_start_end_field("is_month_start", self)
  663. def is_month_end(self, _Timestamp ts):
  664. """
  665. Return boolean whether a timestamp occurs on the month end.
  666. Examples
  667. --------
  668. >>> ts = pd.Timestamp(2022, 1, 1)
  669. >>> freq = pd.offsets.Hour(5)
  670. >>> freq.is_month_end(ts)
  671. False
  672. """
  673. return ts._get_start_end_field("is_month_end", self)
  674. def is_quarter_start(self, _Timestamp ts):
  675. """
  676. Return boolean whether a timestamp occurs on the quarter start.
  677. Examples
  678. --------
  679. >>> ts = pd.Timestamp(2022, 1, 1)
  680. >>> freq = pd.offsets.Hour(5)
  681. >>> freq.is_quarter_start(ts)
  682. True
  683. """
  684. return ts._get_start_end_field("is_quarter_start", self)
  685. def is_quarter_end(self, _Timestamp ts):
  686. """
  687. Return boolean whether a timestamp occurs on the quarter end.
  688. Examples
  689. --------
  690. >>> ts = pd.Timestamp(2022, 1, 1)
  691. >>> freq = pd.offsets.Hour(5)
  692. >>> freq.is_quarter_end(ts)
  693. False
  694. """
  695. return ts._get_start_end_field("is_quarter_end", self)
  696. def is_year_start(self, _Timestamp ts):
  697. """
  698. Return boolean whether a timestamp occurs on the year start.
  699. Examples
  700. --------
  701. >>> ts = pd.Timestamp(2022, 1, 1)
  702. >>> freq = pd.offsets.Hour(5)
  703. >>> freq.is_year_start(ts)
  704. True
  705. """
  706. return ts._get_start_end_field("is_year_start", self)
  707. def is_year_end(self, _Timestamp ts):
  708. """
  709. Return boolean whether a timestamp occurs on the year end.
  710. Examples
  711. --------
  712. >>> ts = pd.Timestamp(2022, 1, 1)
  713. >>> freq = pd.offsets.Hour(5)
  714. >>> freq.is_year_end(ts)
  715. False
  716. """
  717. return ts._get_start_end_field("is_year_end", self)
  718. cdef class SingleConstructorOffset(BaseOffset):
  719. @classmethod
  720. def _from_name(cls, suffix=None):
  721. # default _from_name calls cls with no args
  722. if suffix:
  723. raise ValueError(f"Bad freq suffix {suffix}")
  724. return cls()
  725. def __reduce__(self):
  726. # This __reduce__ implementation is for all BaseOffset subclasses
  727. # except for RelativeDeltaOffset
  728. # np.busdaycalendar objects do not pickle nicely, but we can reconstruct
  729. # from attributes that do get pickled.
  730. tup = tuple(
  731. getattr(self, attr) if attr != "calendar" else None
  732. for attr in self._attributes
  733. )
  734. return type(self), tup
  735. # ---------------------------------------------------------------------
  736. # Tick Offsets
  737. cdef class Tick(SingleConstructorOffset):
  738. _adjust_dst = False
  739. _prefix = "undefined"
  740. _td64_unit = "undefined"
  741. _attributes = tuple(["n", "normalize"])
  742. def __init__(self, n=1, normalize=False):
  743. n = self._validate_n(n)
  744. self.n = n
  745. self.normalize = False
  746. self._cache = {}
  747. if normalize:
  748. # GH#21427
  749. raise ValueError(
  750. "Tick offset with `normalize=True` are not allowed."
  751. )
  752. # Note: Without making this cpdef, we get AttributeError when calling
  753. # from __mul__
  754. cpdef Tick _next_higher_resolution(Tick self):
  755. if type(self) is Day:
  756. return Hour(self.n * 24)
  757. if type(self) is Hour:
  758. return Minute(self.n * 60)
  759. if type(self) is Minute:
  760. return Second(self.n * 60)
  761. if type(self) is Second:
  762. return Milli(self.n * 1000)
  763. if type(self) is Milli:
  764. return Micro(self.n * 1000)
  765. if type(self) is Micro:
  766. return Nano(self.n * 1000)
  767. raise ValueError("Could not convert to integer offset at any resolution")
  768. # --------------------------------------------------------------------
  769. def _repr_attrs(self) -> str:
  770. # Since cdef classes have no __dict__, we need to override
  771. return ""
  772. @property
  773. def delta(self):
  774. return self.n * Timedelta(self._nanos_inc)
  775. @property
  776. def nanos(self) -> int64_t:
  777. """
  778. Return an integer of the total number of nanoseconds.
  779. Raises
  780. ------
  781. ValueError
  782. If the frequency is non-fixed.
  783. Examples
  784. --------
  785. >>> pd.offsets.Hour(5).nanos
  786. 18000000000000
  787. """
  788. return self.n * self._nanos_inc
  789. def is_on_offset(self, dt: datetime) -> bool:
  790. return True
  791. def is_anchored(self) -> bool:
  792. return False
  793. # This is identical to BaseOffset.__hash__, but has to be redefined here
  794. # for Python 3, because we've redefined __eq__.
  795. def __hash__(self) -> int:
  796. return hash(self._params)
  797. # --------------------------------------------------------------------
  798. # Comparison and Arithmetic Methods
  799. def __eq__(self, other):
  800. if isinstance(other, str):
  801. try:
  802. # GH#23524 if to_offset fails, we are dealing with an
  803. # incomparable type so == is False and != is True
  804. other = to_offset(other)
  805. except ValueError:
  806. # e.g. "infer"
  807. return False
  808. return self.delta == other
  809. def __ne__(self, other):
  810. return not (self == other)
  811. def __le__(self, other):
  812. return self.delta.__le__(other)
  813. def __lt__(self, other):
  814. return self.delta.__lt__(other)
  815. def __ge__(self, other):
  816. return self.delta.__ge__(other)
  817. def __gt__(self, other):
  818. return self.delta.__gt__(other)
  819. def __mul__(self, other):
  820. if not isinstance(self, Tick):
  821. # TODO(cython3), remove this, this moved to __rmul__
  822. # cython semantics, this is __rmul__
  823. return other.__mul__(self)
  824. if is_float_object(other):
  825. n = other * self.n
  826. # If the new `n` is an integer, we can represent it using the
  827. # same Tick subclass as self, otherwise we need to move up
  828. # to a higher-resolution subclass
  829. if np.isclose(n % 1, 0):
  830. return type(self)(int(n))
  831. new_self = self._next_higher_resolution()
  832. return new_self * other
  833. return BaseOffset.__mul__(self, other)
  834. def __rmul__(self, other):
  835. return self.__mul__(other)
  836. def __truediv__(self, other):
  837. if not isinstance(self, Tick):
  838. # cython semantics mean the args are sometimes swapped
  839. result = other.delta.__rtruediv__(self)
  840. else:
  841. result = self.delta.__truediv__(other)
  842. return _wrap_timedelta_result(result)
  843. def __rtruediv__(self, other):
  844. result = self.delta.__rtruediv__(other)
  845. return _wrap_timedelta_result(result)
  846. def __add__(self, other):
  847. if not isinstance(self, Tick):
  848. # cython semantics; this is __radd__
  849. # TODO(cython3): remove this, this moved to __radd__
  850. return other.__add__(self)
  851. if isinstance(other, Tick):
  852. if type(self) == type(other):
  853. return type(self)(self.n + other.n)
  854. else:
  855. return delta_to_tick(self.delta + other.delta)
  856. try:
  857. return self._apply(other)
  858. except ApplyTypeError:
  859. # Includes pd.Period
  860. return NotImplemented
  861. except OverflowError as err:
  862. raise OverflowError(
  863. f"the add operation between {self} and {other} will overflow"
  864. ) from err
  865. def __radd__(self, other):
  866. return self.__add__(other)
  867. def _apply(self, other):
  868. # Timestamp can handle tz and nano sec, thus no need to use apply_wraps
  869. if isinstance(other, _Timestamp):
  870. # GH#15126
  871. return other + self.delta
  872. elif other is NaT:
  873. return NaT
  874. elif is_datetime64_object(other) or PyDate_Check(other):
  875. # PyDate_Check includes date, datetime
  876. return Timestamp(other) + self
  877. if util.is_timedelta64_object(other) or PyDelta_Check(other):
  878. return other + self.delta
  879. raise ApplyTypeError(f"Unhandled type: {type(other).__name__}")
  880. # --------------------------------------------------------------------
  881. # Pickle Methods
  882. def __setstate__(self, state):
  883. self.n = state["n"]
  884. self.normalize = False
  885. cdef class Day(Tick):
  886. _nanos_inc = 24 * 3600 * 1_000_000_000
  887. _prefix = "D"
  888. _td64_unit = "D"
  889. _period_dtype_code = PeriodDtypeCode.D
  890. _creso = NPY_DATETIMEUNIT.NPY_FR_D
  891. cdef class Hour(Tick):
  892. _nanos_inc = 3600 * 1_000_000_000
  893. _prefix = "H"
  894. _td64_unit = "h"
  895. _period_dtype_code = PeriodDtypeCode.H
  896. _creso = NPY_DATETIMEUNIT.NPY_FR_h
  897. cdef class Minute(Tick):
  898. _nanos_inc = 60 * 1_000_000_000
  899. _prefix = "T"
  900. _td64_unit = "m"
  901. _period_dtype_code = PeriodDtypeCode.T
  902. _creso = NPY_DATETIMEUNIT.NPY_FR_m
  903. cdef class Second(Tick):
  904. _nanos_inc = 1_000_000_000
  905. _prefix = "S"
  906. _td64_unit = "s"
  907. _period_dtype_code = PeriodDtypeCode.S
  908. _creso = NPY_DATETIMEUNIT.NPY_FR_s
  909. cdef class Milli(Tick):
  910. _nanos_inc = 1_000_000
  911. _prefix = "L"
  912. _td64_unit = "ms"
  913. _period_dtype_code = PeriodDtypeCode.L
  914. _creso = NPY_DATETIMEUNIT.NPY_FR_ms
  915. cdef class Micro(Tick):
  916. _nanos_inc = 1000
  917. _prefix = "U"
  918. _td64_unit = "us"
  919. _period_dtype_code = PeriodDtypeCode.U
  920. _creso = NPY_DATETIMEUNIT.NPY_FR_us
  921. cdef class Nano(Tick):
  922. _nanos_inc = 1
  923. _prefix = "N"
  924. _td64_unit = "ns"
  925. _period_dtype_code = PeriodDtypeCode.N
  926. _creso = NPY_DATETIMEUNIT.NPY_FR_ns
  927. def delta_to_tick(delta: timedelta) -> Tick:
  928. if delta.microseconds == 0 and getattr(delta, "nanoseconds", 0) == 0:
  929. # nanoseconds only for pd.Timedelta
  930. if delta.seconds == 0:
  931. return Day(delta.days)
  932. else:
  933. seconds = delta.days * 86400 + delta.seconds
  934. if seconds % 3600 == 0:
  935. return Hour(seconds / 3600)
  936. elif seconds % 60 == 0:
  937. return Minute(seconds / 60)
  938. else:
  939. return Second(seconds)
  940. else:
  941. nanos = delta_to_nanoseconds(delta)
  942. if nanos % 1_000_000 == 0:
  943. return Milli(nanos // 1_000_000)
  944. elif nanos % 1000 == 0:
  945. return Micro(nanos // 1000)
  946. else: # pragma: no cover
  947. return Nano(nanos)
  948. # --------------------------------------------------------------------
  949. cdef class RelativeDeltaOffset(BaseOffset):
  950. """
  951. DateOffset subclass backed by a dateutil relativedelta object.
  952. """
  953. _attributes = tuple(["n", "normalize"] + list(_relativedelta_kwds))
  954. _adjust_dst = False
  955. def __init__(self, n=1, normalize=False, **kwds):
  956. BaseOffset.__init__(self, n, normalize)
  957. off, use_rd = _determine_offset(kwds)
  958. object.__setattr__(self, "_offset", off)
  959. object.__setattr__(self, "_use_relativedelta", use_rd)
  960. for key in kwds:
  961. val = kwds[key]
  962. object.__setattr__(self, key, val)
  963. def __getstate__(self):
  964. """
  965. Return a pickleable state
  966. """
  967. # RelativeDeltaOffset (technically DateOffset) is the only non-cdef
  968. # class, so the only one with __dict__
  969. state = self.__dict__.copy()
  970. state["n"] = self.n
  971. state["normalize"] = self.normalize
  972. return state
  973. def __setstate__(self, state):
  974. """
  975. Reconstruct an instance from a pickled state
  976. """
  977. if "offset" in state:
  978. # Older (<0.22.0) versions have offset attribute instead of _offset
  979. if "_offset" in state: # pragma: no cover
  980. raise AssertionError("Unexpected key `_offset`")
  981. state["_offset"] = state.pop("offset")
  982. state["kwds"]["offset"] = state["_offset"]
  983. self.n = state.pop("n")
  984. self.normalize = state.pop("normalize")
  985. self._cache = state.pop("_cache", {})
  986. self.__dict__.update(state)
  987. @apply_wraps
  988. def _apply(self, other: datetime) -> datetime:
  989. if self._use_relativedelta:
  990. other = _as_datetime(other)
  991. if len(self.kwds) > 0:
  992. tzinfo = getattr(other, "tzinfo", None)
  993. if tzinfo is not None and self._use_relativedelta:
  994. # perform calculation in UTC
  995. other = other.replace(tzinfo=None)
  996. if hasattr(self, "nanoseconds"):
  997. td_nano = Timedelta(nanoseconds=self.nanoseconds)
  998. else:
  999. td_nano = Timedelta(0)
  1000. if self.n > 0:
  1001. for i in range(self.n):
  1002. other = other + self._offset + td_nano
  1003. else:
  1004. for i in range(-self.n):
  1005. other = other - self._offset - td_nano
  1006. if tzinfo is not None and self._use_relativedelta:
  1007. # bring tz back from UTC calculation
  1008. other = localize_pydatetime(other, tzinfo)
  1009. return Timestamp(other)
  1010. else:
  1011. return other + timedelta(self.n)
  1012. @apply_array_wraps
  1013. def _apply_array(self, dtarr):
  1014. reso = get_unit_from_dtype(dtarr.dtype)
  1015. dt64other = np.asarray(dtarr)
  1016. kwds = self.kwds
  1017. relativedelta_fast = {
  1018. "years",
  1019. "months",
  1020. "weeks",
  1021. "days",
  1022. "hours",
  1023. "minutes",
  1024. "seconds",
  1025. "microseconds",
  1026. }
  1027. # relativedelta/_offset path only valid for base DateOffset
  1028. if self._use_relativedelta and set(kwds).issubset(relativedelta_fast):
  1029. months = (kwds.get("years", 0) * 12 + kwds.get("months", 0)) * self.n
  1030. if months:
  1031. shifted = shift_months(dt64other.view("i8"), months, reso=reso)
  1032. dt64other = shifted.view(dtarr.dtype)
  1033. weeks = kwds.get("weeks", 0) * self.n
  1034. if weeks:
  1035. delta = Timedelta(days=7 * weeks)
  1036. td = (<_Timedelta>delta)._as_creso(reso)
  1037. dt64other = dt64other + td
  1038. timedelta_kwds = {
  1039. k: v
  1040. for k, v in kwds.items()
  1041. if k in ["days", "hours", "minutes", "seconds", "microseconds"]
  1042. }
  1043. if timedelta_kwds:
  1044. delta = Timedelta(**timedelta_kwds)
  1045. td = (<_Timedelta>delta)._as_creso(reso)
  1046. dt64other = dt64other + (self.n * td)
  1047. return dt64other
  1048. elif not self._use_relativedelta and hasattr(self, "_offset"):
  1049. # timedelta
  1050. num_nano = getattr(self, "nanoseconds", 0)
  1051. if num_nano != 0:
  1052. rem_nano = Timedelta(nanoseconds=num_nano)
  1053. delta = Timedelta((self._offset + rem_nano) * self.n)
  1054. else:
  1055. delta = Timedelta(self._offset * self.n)
  1056. td = (<_Timedelta>delta)._as_creso(reso)
  1057. return dt64other + td
  1058. else:
  1059. # relativedelta with other keywords
  1060. kwd = set(kwds) - relativedelta_fast
  1061. raise NotImplementedError(
  1062. "DateOffset with relativedelta "
  1063. f"keyword(s) {kwd} not able to be "
  1064. "applied vectorized"
  1065. )
  1066. def is_on_offset(self, dt: datetime) -> bool:
  1067. if self.normalize and not _is_normalized(dt):
  1068. return False
  1069. return True
  1070. class OffsetMeta(type):
  1071. """
  1072. Metaclass that allows us to pretend that all BaseOffset subclasses
  1073. inherit from DateOffset (which is needed for backward-compatibility).
  1074. """
  1075. @classmethod
  1076. def __instancecheck__(cls, obj) -> bool:
  1077. return isinstance(obj, BaseOffset)
  1078. @classmethod
  1079. def __subclasscheck__(cls, obj) -> bool:
  1080. return issubclass(obj, BaseOffset)
  1081. # TODO: figure out a way to use a metaclass with a cdef class
  1082. class DateOffset(RelativeDeltaOffset, metaclass=OffsetMeta):
  1083. """
  1084. Standard kind of date increment used for a date range.
  1085. Works exactly like the keyword argument form of relativedelta.
  1086. Note that the positional argument form of relativedelata is not
  1087. supported. Use of the keyword n is discouraged-- you would be better
  1088. off specifying n in the keywords you use, but regardless it is
  1089. there for you. n is needed for DateOffset subclasses.
  1090. DateOffset works as follows. Each offset specify a set of dates
  1091. that conform to the DateOffset. For example, Bday defines this
  1092. set to be the set of dates that are weekdays (M-F). To test if a
  1093. date is in the set of a DateOffset dateOffset we can use the
  1094. is_on_offset method: dateOffset.is_on_offset(date).
  1095. If a date is not on a valid date, the rollback and rollforward
  1096. methods can be used to roll the date to the nearest valid date
  1097. before/after the date.
  1098. DateOffsets can be created to move dates forward a given number of
  1099. valid dates. For example, Bday(2) can be added to a date to move
  1100. it two business days forward. If the date does not start on a
  1101. valid date, first it is moved to a valid date. Thus pseudo code
  1102. is::
  1103. def __add__(date):
  1104. date = rollback(date) # does nothing if date is valid
  1105. return date + <n number of periods>
  1106. When a date offset is created for a negative number of periods,
  1107. the date is first rolled forward. The pseudo code is::
  1108. def __add__(date):
  1109. date = rollforward(date) # does nothing if date is valid
  1110. return date + <n number of periods>
  1111. Zero presents a problem. Should it roll forward or back? We
  1112. arbitrarily have it rollforward:
  1113. date + BDay(0) == BDay.rollforward(date)
  1114. Since 0 is a bit weird, we suggest avoiding its use.
  1115. Besides, adding a DateOffsets specified by the singular form of the date
  1116. component can be used to replace certain component of the timestamp.
  1117. Parameters
  1118. ----------
  1119. n : int, default 1
  1120. The number of time periods the offset represents.
  1121. If specified without a temporal pattern, defaults to n days.
  1122. normalize : bool, default False
  1123. Whether to round the result of a DateOffset addition down to the
  1124. previous midnight.
  1125. **kwds
  1126. Temporal parameter that add to or replace the offset value.
  1127. Parameters that **add** to the offset (like Timedelta):
  1128. - years
  1129. - months
  1130. - weeks
  1131. - days
  1132. - hours
  1133. - minutes
  1134. - seconds
  1135. - milliseconds
  1136. - microseconds
  1137. - nanoseconds
  1138. Parameters that **replace** the offset value:
  1139. - year
  1140. - month
  1141. - day
  1142. - weekday
  1143. - hour
  1144. - minute
  1145. - second
  1146. - microsecond
  1147. - nanosecond.
  1148. See Also
  1149. --------
  1150. dateutil.relativedelta.relativedelta : The relativedelta type is designed
  1151. to be applied to an existing datetime an can replace specific components of
  1152. that datetime, or represents an interval of time.
  1153. Examples
  1154. --------
  1155. >>> from pandas.tseries.offsets import DateOffset
  1156. >>> ts = pd.Timestamp('2017-01-01 09:10:11')
  1157. >>> ts + DateOffset(months=3)
  1158. Timestamp('2017-04-01 09:10:11')
  1159. >>> ts = pd.Timestamp('2017-01-01 09:10:11')
  1160. >>> ts + DateOffset(months=2)
  1161. Timestamp('2017-03-01 09:10:11')
  1162. >>> ts + DateOffset(day=31)
  1163. Timestamp('2017-01-31 09:10:11')
  1164. >>> ts + pd.DateOffset(hour=8)
  1165. Timestamp('2017-01-01 08:10:11')
  1166. """
  1167. def __setattr__(self, name, value):
  1168. raise AttributeError("DateOffset objects are immutable.")
  1169. # --------------------------------------------------------------------
  1170. cdef class BusinessMixin(SingleConstructorOffset):
  1171. """
  1172. Mixin to business types to provide related functions.
  1173. """
  1174. cdef readonly:
  1175. timedelta _offset
  1176. # Only Custom subclasses use weekmask, holiday, calendar
  1177. object weekmask, holidays, calendar
  1178. def __init__(self, n=1, normalize=False, offset=timedelta(0)):
  1179. BaseOffset.__init__(self, n, normalize)
  1180. self._offset = offset
  1181. cpdef _init_custom(self, weekmask, holidays, calendar):
  1182. """
  1183. Additional __init__ for Custom subclasses.
  1184. """
  1185. calendar, holidays = _get_calendar(
  1186. weekmask=weekmask, holidays=holidays, calendar=calendar
  1187. )
  1188. # Custom offset instances are identified by the
  1189. # following two attributes. See DateOffset._params()
  1190. # holidays, weekmask
  1191. self.weekmask = weekmask
  1192. self.holidays = holidays
  1193. self.calendar = calendar
  1194. @property
  1195. def offset(self):
  1196. """
  1197. Alias for self._offset.
  1198. """
  1199. # Alias for backward compat
  1200. return self._offset
  1201. def _repr_attrs(self) -> str:
  1202. if self.offset:
  1203. attrs = [f"offset={repr(self.offset)}"]
  1204. else:
  1205. attrs = []
  1206. out = ""
  1207. if attrs:
  1208. out += ": " + ", ".join(attrs)
  1209. return out
  1210. cpdef __setstate__(self, state):
  1211. # We need to use a cdef/cpdef method to set the readonly _offset attribute
  1212. if "_offset" in state:
  1213. self._offset = state.pop("_offset")
  1214. elif "offset" in state:
  1215. # Older (<0.22.0) versions have offset attribute instead of _offset
  1216. self._offset = state.pop("offset")
  1217. if self._prefix.startswith("C"):
  1218. # i.e. this is a Custom class
  1219. weekmask = state.pop("weekmask")
  1220. holidays = state.pop("holidays")
  1221. calendar, holidays = _get_calendar(weekmask=weekmask,
  1222. holidays=holidays,
  1223. calendar=None)
  1224. self.weekmask = weekmask
  1225. self.calendar = calendar
  1226. self.holidays = holidays
  1227. BaseOffset.__setstate__(self, state)
  1228. cdef class BusinessDay(BusinessMixin):
  1229. """
  1230. DateOffset subclass representing possibly n business days.
  1231. Parameters
  1232. ----------
  1233. n : int, default 1
  1234. The number of days represented.
  1235. normalize : bool, default False
  1236. Normalize start/end dates to midnight.
  1237. Examples
  1238. --------
  1239. You can use the parameter ``n`` to represent a shift of n business days.
  1240. >>> ts = pd.Timestamp(2022, 12, 9, 15)
  1241. >>> ts.strftime('%a %d %b %Y %H:%M')
  1242. 'Fri 09 Dec 2022 15:00'
  1243. >>> (ts + pd.offsets.BusinessDay(n=5)).strftime('%a %d %b %Y %H:%M')
  1244. 'Fri 16 Dec 2022 15:00'
  1245. Passing the parameter ``normalize`` equal to True, you shift the start
  1246. of the next business day to midnight.
  1247. >>> ts = pd.Timestamp(2022, 12, 9, 15)
  1248. >>> ts + pd.offsets.BusinessDay(normalize=True)
  1249. Timestamp('2022-12-12 00:00:00')
  1250. """
  1251. _period_dtype_code = PeriodDtypeCode.B
  1252. _prefix = "B"
  1253. _attributes = tuple(["n", "normalize", "offset"])
  1254. cpdef __setstate__(self, state):
  1255. self.n = state.pop("n")
  1256. self.normalize = state.pop("normalize")
  1257. if "_offset" in state:
  1258. self._offset = state.pop("_offset")
  1259. elif "offset" in state:
  1260. self._offset = state.pop("offset")
  1261. self._cache = state.pop("_cache", {})
  1262. def _offset_str(self) -> str:
  1263. def get_str(td):
  1264. off_str = ""
  1265. if td.days > 0:
  1266. off_str += str(td.days) + "D"
  1267. if td.seconds > 0:
  1268. s = td.seconds
  1269. hrs = int(s / 3600)
  1270. if hrs != 0:
  1271. off_str += str(hrs) + "H"
  1272. s -= hrs * 3600
  1273. mts = int(s / 60)
  1274. if mts != 0:
  1275. off_str += str(mts) + "Min"
  1276. s -= mts * 60
  1277. if s != 0:
  1278. off_str += str(s) + "s"
  1279. if td.microseconds > 0:
  1280. off_str += str(td.microseconds) + "us"
  1281. return off_str
  1282. if PyDelta_Check(self.offset):
  1283. zero = timedelta(0, 0, 0)
  1284. if self.offset >= zero:
  1285. off_str = "+" + get_str(self.offset)
  1286. else:
  1287. off_str = "-" + get_str(-self.offset)
  1288. return off_str
  1289. else:
  1290. return "+" + repr(self.offset)
  1291. @apply_wraps
  1292. def _apply(self, other):
  1293. if PyDateTime_Check(other):
  1294. n = self.n
  1295. wday = other.weekday()
  1296. # avoid slowness below by operating on weeks first
  1297. weeks = n // 5
  1298. days = self._adjust_ndays(wday, weeks)
  1299. result = other + timedelta(days=7 * weeks + days)
  1300. if self.offset:
  1301. result = result + self.offset
  1302. return result
  1303. elif is_any_td_scalar(other):
  1304. td = Timedelta(self.offset) + other
  1305. return BusinessDay(
  1306. self.n, offset=td.to_pytimedelta(), normalize=self.normalize
  1307. )
  1308. else:
  1309. raise ApplyTypeError(
  1310. "Only know how to combine business day with datetime or timedelta."
  1311. )
  1312. @cython.wraparound(False)
  1313. @cython.boundscheck(False)
  1314. cdef ndarray _shift_bdays(
  1315. self,
  1316. ndarray i8other,
  1317. NPY_DATETIMEUNIT reso=NPY_DATETIMEUNIT.NPY_FR_ns,
  1318. ):
  1319. """
  1320. Implementation of BusinessDay.apply_offset.
  1321. Parameters
  1322. ----------
  1323. i8other : const int64_t[:]
  1324. reso : NPY_DATETIMEUNIT, default NPY_FR_ns
  1325. Returns
  1326. -------
  1327. ndarray[int64_t]
  1328. """
  1329. cdef:
  1330. int periods = self.n
  1331. Py_ssize_t i, n = i8other.size
  1332. ndarray result = cnp.PyArray_EMPTY(
  1333. i8other.ndim, i8other.shape, cnp.NPY_INT64, 0
  1334. )
  1335. int64_t val, res_val
  1336. int wday, days
  1337. npy_datetimestruct dts
  1338. int64_t DAY_PERIODS = periods_per_day(reso)
  1339. cnp.broadcast mi = cnp.PyArray_MultiIterNew2(result, i8other)
  1340. for i in range(n):
  1341. # Analogous to: val = i8other[i]
  1342. val = (<int64_t*>cnp.PyArray_MultiIter_DATA(mi, 1))[0]
  1343. if val == NPY_NAT:
  1344. res_val = NPY_NAT
  1345. else:
  1346. # The rest of this is effectively a copy of BusinessDay.apply
  1347. weeks = periods // 5
  1348. pandas_datetime_to_datetimestruct(val, reso, &dts)
  1349. wday = dayofweek(dts.year, dts.month, dts.day)
  1350. days = self._adjust_ndays(wday, weeks)
  1351. res_val = val + (7 * weeks + days) * DAY_PERIODS
  1352. # Analogous to: out[i] = res_val
  1353. (<int64_t*>cnp.PyArray_MultiIter_DATA(mi, 0))[0] = res_val
  1354. cnp.PyArray_MultiIter_NEXT(mi)
  1355. return result
  1356. cdef int _adjust_ndays(self, int wday, int weeks):
  1357. cdef:
  1358. int n = self.n
  1359. int days
  1360. if n <= 0 and wday > 4:
  1361. # roll forward
  1362. n += 1
  1363. n -= 5 * weeks
  1364. # n is always >= 0 at this point
  1365. if n == 0 and wday > 4:
  1366. # roll back
  1367. days = 4 - wday
  1368. elif wday > 4:
  1369. # roll forward
  1370. days = (7 - wday) + (n - 1)
  1371. elif wday + n <= 4:
  1372. # shift by n days without leaving the current week
  1373. days = n
  1374. else:
  1375. # shift by n days plus 2 to get past the weekend
  1376. days = n + 2
  1377. return days
  1378. @apply_array_wraps
  1379. def _apply_array(self, dtarr):
  1380. i8other = dtarr.view("i8")
  1381. reso = get_unit_from_dtype(dtarr.dtype)
  1382. res = self._shift_bdays(i8other, reso=reso)
  1383. if self.offset:
  1384. res = res.view(dtarr.dtype) + Timedelta(self.offset)
  1385. res = res.view("i8")
  1386. return res
  1387. def is_on_offset(self, dt: datetime) -> bool:
  1388. if self.normalize and not _is_normalized(dt):
  1389. return False
  1390. return dt.weekday() < 5
  1391. cdef class BusinessHour(BusinessMixin):
  1392. """
  1393. DateOffset subclass representing possibly n business hours.
  1394. Parameters
  1395. ----------
  1396. n : int, default 1
  1397. The number of hours represented.
  1398. normalize : bool, default False
  1399. Normalize start/end dates to midnight before generating date range.
  1400. start : str, time, or list of str/time, default "09:00"
  1401. Start time of your custom business hour in 24h format.
  1402. end : str, time, or list of str/time, default: "17:00"
  1403. End time of your custom business hour in 24h format.
  1404. Examples
  1405. --------
  1406. You can use the parameter ``n`` to represent a shift of n hours.
  1407. >>> ts = pd.Timestamp(2022, 12, 9, 8)
  1408. >>> ts + pd.offsets.BusinessHour(n=5)
  1409. Timestamp('2022-12-09 14:00:00')
  1410. You can also change the start and the end of business hours.
  1411. >>> ts = pd.Timestamp(2022, 8, 5, 16)
  1412. >>> ts + pd.offsets.BusinessHour(start="11:00")
  1413. Timestamp('2022-08-08 11:00:00')
  1414. >>> from datetime import time as dt_time
  1415. >>> ts = pd.Timestamp(2022, 8, 5, 22)
  1416. >>> ts + pd.offsets.BusinessHour(end=dt_time(19, 0))
  1417. Timestamp('2022-08-08 10:00:00')
  1418. Passing the parameter ``normalize`` equal to True, you shift the start
  1419. of the next business hour to midnight.
  1420. >>> ts = pd.Timestamp(2022, 12, 9, 8)
  1421. >>> ts + pd.offsets.BusinessHour(normalize=True)
  1422. Timestamp('2022-12-09 00:00:00')
  1423. You can divide your business day hours into several parts.
  1424. >>> import datetime as dt
  1425. >>> freq = pd.offsets.BusinessHour(start=["06:00", "10:00", "15:00"],
  1426. ... end=["08:00", "12:00", "17:00"])
  1427. >>> pd.date_range(dt.datetime(2022, 12, 9), dt.datetime(2022, 12, 13), freq=freq)
  1428. DatetimeIndex(['2022-12-09 06:00:00', '2022-12-09 07:00:00',
  1429. '2022-12-09 10:00:00', '2022-12-09 11:00:00',
  1430. '2022-12-09 15:00:00', '2022-12-09 16:00:00',
  1431. '2022-12-12 06:00:00', '2022-12-12 07:00:00',
  1432. '2022-12-12 10:00:00', '2022-12-12 11:00:00',
  1433. '2022-12-12 15:00:00', '2022-12-12 16:00:00'],
  1434. dtype='datetime64[ns]', freq='BH')
  1435. """
  1436. _prefix = "BH"
  1437. _anchor = 0
  1438. _attributes = tuple(["n", "normalize", "start", "end", "offset"])
  1439. _adjust_dst = False
  1440. cdef readonly:
  1441. tuple start, end
  1442. def __init__(
  1443. self, n=1, normalize=False, start="09:00", end="17:00", offset=timedelta(0)
  1444. ):
  1445. BusinessMixin.__init__(self, n, normalize, offset)
  1446. # must be validated here to equality check
  1447. if np.ndim(start) == 0:
  1448. # i.e. not is_list_like
  1449. start = [start]
  1450. if not len(start):
  1451. raise ValueError("Must include at least 1 start time")
  1452. if np.ndim(end) == 0:
  1453. # i.e. not is_list_like
  1454. end = [end]
  1455. if not len(end):
  1456. raise ValueError("Must include at least 1 end time")
  1457. start = np.array([_validate_business_time(x) for x in start])
  1458. end = np.array([_validate_business_time(x) for x in end])
  1459. # Validation of input
  1460. if len(start) != len(end):
  1461. raise ValueError("number of starting time and ending time must be the same")
  1462. num_openings = len(start)
  1463. # sort starting and ending time by starting time
  1464. index = np.argsort(start)
  1465. # convert to tuple so that start and end are hashable
  1466. start = tuple(start[index])
  1467. end = tuple(end[index])
  1468. total_secs = 0
  1469. for i in range(num_openings):
  1470. total_secs += self._get_business_hours_by_sec(start[i], end[i])
  1471. total_secs += self._get_business_hours_by_sec(
  1472. end[i], start[(i + 1) % num_openings]
  1473. )
  1474. if total_secs != 24 * 60 * 60:
  1475. raise ValueError(
  1476. "invalid starting and ending time(s): "
  1477. "opening hours should not touch or overlap with "
  1478. "one another"
  1479. )
  1480. self.start = start
  1481. self.end = end
  1482. cpdef __setstate__(self, state):
  1483. start = state.pop("start")
  1484. start = (start,) if np.ndim(start) == 0 else tuple(start)
  1485. end = state.pop("end")
  1486. end = (end,) if np.ndim(end) == 0 else tuple(end)
  1487. self.start = start
  1488. self.end = end
  1489. state.pop("kwds", {})
  1490. state.pop("next_bday", None)
  1491. BusinessMixin.__setstate__(self, state)
  1492. def _repr_attrs(self) -> str:
  1493. out = super()._repr_attrs()
  1494. # Use python string formatting to be faster than strftime
  1495. hours = ",".join(
  1496. f"{st.hour:02d}:{st.minute:02d}-{en.hour:02d}:{en.minute:02d}"
  1497. for st, en in zip(self.start, self.end)
  1498. )
  1499. attrs = [f"{self._prefix}={hours}"]
  1500. out += ": " + ", ".join(attrs)
  1501. return out
  1502. def _get_business_hours_by_sec(self, start, end):
  1503. """
  1504. Return business hours in a day by seconds.
  1505. """
  1506. # create dummy datetime to calculate business hours in a day
  1507. dtstart = datetime(2014, 4, 1, start.hour, start.minute)
  1508. day = 1 if start < end else 2
  1509. until = datetime(2014, 4, day, end.hour, end.minute)
  1510. return int((until - dtstart).total_seconds())
  1511. def _get_closing_time(self, dt: datetime) -> datetime:
  1512. """
  1513. Get the closing time of a business hour interval by its opening time.
  1514. Parameters
  1515. ----------
  1516. dt : datetime
  1517. Opening time of a business hour interval.
  1518. Returns
  1519. -------
  1520. result : datetime
  1521. Corresponding closing time.
  1522. """
  1523. for i, st in enumerate(self.start):
  1524. if st.hour == dt.hour and st.minute == dt.minute:
  1525. return dt + timedelta(
  1526. seconds=self._get_business_hours_by_sec(st, self.end[i])
  1527. )
  1528. assert False
  1529. @cache_readonly
  1530. def next_bday(self):
  1531. """
  1532. Used for moving to next business day.
  1533. """
  1534. if self.n >= 0:
  1535. nb_offset = 1
  1536. else:
  1537. nb_offset = -1
  1538. if self._prefix.startswith("C"):
  1539. # CustomBusinessHour
  1540. return CustomBusinessDay(
  1541. n=nb_offset,
  1542. weekmask=self.weekmask,
  1543. holidays=self.holidays,
  1544. calendar=self.calendar,
  1545. )
  1546. else:
  1547. return BusinessDay(n=nb_offset)
  1548. def _next_opening_time(self, other, sign=1):
  1549. """
  1550. If self.n and sign have the same sign, return the earliest opening time
  1551. later than or equal to current time.
  1552. Otherwise the latest opening time earlier than or equal to current
  1553. time.
  1554. Opening time always locates on BusinessDay.
  1555. However, closing time may not if business hour extends over midnight.
  1556. Parameters
  1557. ----------
  1558. other : datetime
  1559. Current time.
  1560. sign : int, default 1.
  1561. Either 1 or -1. Going forward in time if it has the same sign as
  1562. self.n. Going backward in time otherwise.
  1563. Returns
  1564. -------
  1565. result : datetime
  1566. Next opening time.
  1567. """
  1568. earliest_start = self.start[0]
  1569. latest_start = self.start[-1]
  1570. if self.n == 0:
  1571. is_same_sign = sign > 0
  1572. else:
  1573. is_same_sign = self.n * sign >= 0
  1574. if not self.next_bday.is_on_offset(other):
  1575. # today is not business day
  1576. other = other + sign * self.next_bday
  1577. if is_same_sign:
  1578. hour, minute = earliest_start.hour, earliest_start.minute
  1579. else:
  1580. hour, minute = latest_start.hour, latest_start.minute
  1581. else:
  1582. if is_same_sign:
  1583. if latest_start < other.time():
  1584. # current time is after latest starting time in today
  1585. other = other + sign * self.next_bday
  1586. hour, minute = earliest_start.hour, earliest_start.minute
  1587. else:
  1588. # find earliest starting time no earlier than current time
  1589. for st in self.start:
  1590. if other.time() <= st:
  1591. hour, minute = st.hour, st.minute
  1592. break
  1593. else:
  1594. if other.time() < earliest_start:
  1595. # current time is before earliest starting time in today
  1596. other = other + sign * self.next_bday
  1597. hour, minute = latest_start.hour, latest_start.minute
  1598. else:
  1599. # find latest starting time no later than current time
  1600. for st in reversed(self.start):
  1601. if other.time() >= st:
  1602. hour, minute = st.hour, st.minute
  1603. break
  1604. return datetime(other.year, other.month, other.day, hour, minute)
  1605. def _prev_opening_time(self, other: datetime) -> datetime:
  1606. """
  1607. If n is positive, return the latest opening time earlier than or equal
  1608. to current time.
  1609. Otherwise the earliest opening time later than or equal to current
  1610. time.
  1611. Parameters
  1612. ----------
  1613. other : datetime
  1614. Current time.
  1615. Returns
  1616. -------
  1617. result : datetime
  1618. Previous opening time.
  1619. """
  1620. return self._next_opening_time(other, sign=-1)
  1621. @apply_wraps
  1622. def rollback(self, dt: datetime) -> datetime:
  1623. """
  1624. Roll provided date backward to next offset only if not on offset.
  1625. """
  1626. if not self.is_on_offset(dt):
  1627. if self.n >= 0:
  1628. dt = self._prev_opening_time(dt)
  1629. else:
  1630. dt = self._next_opening_time(dt)
  1631. return self._get_closing_time(dt)
  1632. return dt
  1633. @apply_wraps
  1634. def rollforward(self, dt: datetime) -> datetime:
  1635. """
  1636. Roll provided date forward to next offset only if not on offset.
  1637. """
  1638. if not self.is_on_offset(dt):
  1639. if self.n >= 0:
  1640. return self._next_opening_time(dt)
  1641. else:
  1642. return self._prev_opening_time(dt)
  1643. return dt
  1644. @apply_wraps
  1645. def _apply(self, other: datetime) -> datetime:
  1646. # used for detecting edge condition
  1647. nanosecond = getattr(other, "nanosecond", 0)
  1648. # reset timezone and nanosecond
  1649. # other may be a Timestamp, thus not use replace
  1650. other = datetime(
  1651. other.year,
  1652. other.month,
  1653. other.day,
  1654. other.hour,
  1655. other.minute,
  1656. other.second,
  1657. other.microsecond,
  1658. )
  1659. n = self.n
  1660. # adjust other to reduce number of cases to handle
  1661. if n >= 0:
  1662. if other.time() in self.end or not self._is_on_offset(other):
  1663. other = self._next_opening_time(other)
  1664. else:
  1665. if other.time() in self.start:
  1666. # adjustment to move to previous business day
  1667. other = other - timedelta(seconds=1)
  1668. if not self._is_on_offset(other):
  1669. other = self._next_opening_time(other)
  1670. other = self._get_closing_time(other)
  1671. # get total business hours by sec in one business day
  1672. businesshours = sum(
  1673. self._get_business_hours_by_sec(st, en)
  1674. for st, en in zip(self.start, self.end)
  1675. )
  1676. bd, r = divmod(abs(n * 60), businesshours // 60)
  1677. if n < 0:
  1678. bd, r = -bd, -r
  1679. # adjust by business days first
  1680. if bd != 0:
  1681. if self._prefix.startswith("C"):
  1682. # GH#30593 this is a Custom offset
  1683. skip_bd = CustomBusinessDay(
  1684. n=bd,
  1685. weekmask=self.weekmask,
  1686. holidays=self.holidays,
  1687. calendar=self.calendar,
  1688. )
  1689. else:
  1690. skip_bd = BusinessDay(n=bd)
  1691. # midnight business hour may not on BusinessDay
  1692. if not self.next_bday.is_on_offset(other):
  1693. prev_open = self._prev_opening_time(other)
  1694. remain = other - prev_open
  1695. other = prev_open + skip_bd + remain
  1696. else:
  1697. other = other + skip_bd
  1698. # remaining business hours to adjust
  1699. bhour_remain = timedelta(minutes=r)
  1700. if n >= 0:
  1701. while bhour_remain != timedelta(0):
  1702. # business hour left in this business time interval
  1703. bhour = (
  1704. self._get_closing_time(self._prev_opening_time(other)) - other
  1705. )
  1706. if bhour_remain < bhour:
  1707. # finish adjusting if possible
  1708. other += bhour_remain
  1709. bhour_remain = timedelta(0)
  1710. else:
  1711. # go to next business time interval
  1712. bhour_remain -= bhour
  1713. other = self._next_opening_time(other + bhour)
  1714. else:
  1715. while bhour_remain != timedelta(0):
  1716. # business hour left in this business time interval
  1717. bhour = self._next_opening_time(other) - other
  1718. if (
  1719. bhour_remain > bhour
  1720. or bhour_remain == bhour
  1721. and nanosecond != 0
  1722. ):
  1723. # finish adjusting if possible
  1724. other += bhour_remain
  1725. bhour_remain = timedelta(0)
  1726. else:
  1727. # go to next business time interval
  1728. bhour_remain -= bhour
  1729. other = self._get_closing_time(
  1730. self._next_opening_time(
  1731. other + bhour - timedelta(seconds=1)
  1732. )
  1733. )
  1734. return other
  1735. def is_on_offset(self, dt: datetime) -> bool:
  1736. if self.normalize and not _is_normalized(dt):
  1737. return False
  1738. if dt.tzinfo is not None:
  1739. dt = datetime(
  1740. dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, dt.microsecond
  1741. )
  1742. # Valid BH can be on the different BusinessDay during midnight
  1743. # Distinguish by the time spent from previous opening time
  1744. return self._is_on_offset(dt)
  1745. def _is_on_offset(self, dt: datetime) -> bool:
  1746. """
  1747. Slight speedups using calculated values.
  1748. """
  1749. # if self.normalize and not _is_normalized(dt):
  1750. # return False
  1751. # Valid BH can be on the different BusinessDay during midnight
  1752. # Distinguish by the time spent from previous opening time
  1753. if self.n >= 0:
  1754. op = self._prev_opening_time(dt)
  1755. else:
  1756. op = self._next_opening_time(dt)
  1757. span = (dt - op).total_seconds()
  1758. businesshours = 0
  1759. for i, st in enumerate(self.start):
  1760. if op.hour == st.hour and op.minute == st.minute:
  1761. businesshours = self._get_business_hours_by_sec(st, self.end[i])
  1762. if span <= businesshours:
  1763. return True
  1764. else:
  1765. return False
  1766. cdef class WeekOfMonthMixin(SingleConstructorOffset):
  1767. """
  1768. Mixin for methods common to WeekOfMonth and LastWeekOfMonth.
  1769. """
  1770. cdef readonly:
  1771. int weekday, week
  1772. def __init__(self, n=1, normalize=False, weekday=0):
  1773. BaseOffset.__init__(self, n, normalize)
  1774. self.weekday = weekday
  1775. if weekday < 0 or weekday > 6:
  1776. raise ValueError(f"Day must be 0<=day<=6, got {weekday}")
  1777. @apply_wraps
  1778. def _apply(self, other: datetime) -> datetime:
  1779. compare_day = self._get_offset_day(other)
  1780. months = self.n
  1781. months = roll_convention(other.day, months, compare_day)
  1782. shifted = shift_month(other, months, "start")
  1783. to_day = self._get_offset_day(shifted)
  1784. return _shift_day(shifted, to_day - shifted.day)
  1785. def is_on_offset(self, dt: datetime) -> bool:
  1786. if self.normalize and not _is_normalized(dt):
  1787. return False
  1788. return dt.day == self._get_offset_day(dt)
  1789. @property
  1790. def rule_code(self) -> str:
  1791. weekday = int_to_weekday.get(self.weekday, "")
  1792. if self.week == -1:
  1793. # LastWeekOfMonth
  1794. return f"{self._prefix}-{weekday}"
  1795. return f"{self._prefix}-{self.week + 1}{weekday}"
  1796. # ----------------------------------------------------------------------
  1797. # Year-Based Offset Classes
  1798. cdef class YearOffset(SingleConstructorOffset):
  1799. """
  1800. DateOffset that just needs a month.
  1801. """
  1802. _attributes = tuple(["n", "normalize", "month"])
  1803. # FIXME(cython#4446): python annotation here gives compile-time errors
  1804. # _default_month: int
  1805. cdef readonly:
  1806. int month
  1807. def __init__(self, n=1, normalize=False, month=None):
  1808. BaseOffset.__init__(self, n, normalize)
  1809. month = month if month is not None else self._default_month
  1810. self.month = month
  1811. if month < 1 or month > 12:
  1812. raise ValueError("Month must go from 1 to 12")
  1813. cpdef __setstate__(self, state):
  1814. self.month = state.pop("month")
  1815. self.n = state.pop("n")
  1816. self.normalize = state.pop("normalize")
  1817. self._cache = {}
  1818. @classmethod
  1819. def _from_name(cls, suffix=None):
  1820. kwargs = {}
  1821. if suffix:
  1822. kwargs["month"] = MONTH_TO_CAL_NUM[suffix]
  1823. return cls(**kwargs)
  1824. @property
  1825. def rule_code(self) -> str:
  1826. month = MONTH_ALIASES[self.month]
  1827. return f"{self._prefix}-{month}"
  1828. def is_on_offset(self, dt: datetime) -> bool:
  1829. if self.normalize and not _is_normalized(dt):
  1830. return False
  1831. return dt.month == self.month and dt.day == self._get_offset_day(dt)
  1832. def _get_offset_day(self, other: datetime) -> int:
  1833. # override BaseOffset method to use self.month instead of other.month
  1834. cdef:
  1835. npy_datetimestruct dts
  1836. pydate_to_dtstruct(other, &dts)
  1837. dts.month = self.month
  1838. return get_day_of_month(&dts, self._day_opt)
  1839. @apply_wraps
  1840. def _apply(self, other: datetime) -> datetime:
  1841. years = roll_qtrday(other, self.n, self.month, self._day_opt, modby=12)
  1842. months = years * 12 + (self.month - other.month)
  1843. return shift_month(other, months, self._day_opt)
  1844. @apply_array_wraps
  1845. def _apply_array(self, dtarr):
  1846. reso = get_unit_from_dtype(dtarr.dtype)
  1847. shifted = shift_quarters(
  1848. dtarr.view("i8"), self.n, self.month, self._day_opt, modby=12, reso=reso
  1849. )
  1850. return shifted
  1851. cdef class BYearEnd(YearOffset):
  1852. """
  1853. DateOffset increments between the last business day of the year.
  1854. Examples
  1855. --------
  1856. >>> from pandas.tseries.offsets import BYearEnd
  1857. >>> ts = pd.Timestamp('2020-05-24 05:01:15')
  1858. >>> ts - BYearEnd()
  1859. Timestamp('2019-12-31 05:01:15')
  1860. >>> ts + BYearEnd()
  1861. Timestamp('2020-12-31 05:01:15')
  1862. >>> ts + BYearEnd(3)
  1863. Timestamp('2022-12-30 05:01:15')
  1864. >>> ts + BYearEnd(-3)
  1865. Timestamp('2017-12-29 05:01:15')
  1866. >>> ts + BYearEnd(month=11)
  1867. Timestamp('2020-11-30 05:01:15')
  1868. """
  1869. _outputName = "BusinessYearEnd"
  1870. _default_month = 12
  1871. _prefix = "BA"
  1872. _day_opt = "business_end"
  1873. cdef class BYearBegin(YearOffset):
  1874. """
  1875. DateOffset increments between the first business day of the year.
  1876. Examples
  1877. --------
  1878. >>> from pandas.tseries.offsets import BYearBegin
  1879. >>> ts = pd.Timestamp('2020-05-24 05:01:15')
  1880. >>> ts + BYearBegin()
  1881. Timestamp('2021-01-01 05:01:15')
  1882. >>> ts - BYearBegin()
  1883. Timestamp('2020-01-01 05:01:15')
  1884. >>> ts + BYearBegin(-1)
  1885. Timestamp('2020-01-01 05:01:15')
  1886. >>> ts + BYearBegin(2)
  1887. Timestamp('2022-01-03 05:01:15')
  1888. """
  1889. _outputName = "BusinessYearBegin"
  1890. _default_month = 1
  1891. _prefix = "BAS"
  1892. _day_opt = "business_start"
  1893. cdef class YearEnd(YearOffset):
  1894. """
  1895. DateOffset increments between calendar year ends.
  1896. Examples
  1897. --------
  1898. >>> ts = pd.Timestamp(2022, 1, 1)
  1899. >>> ts + pd.offsets.YearEnd()
  1900. Timestamp('2022-12-31 00:00:00')
  1901. """
  1902. _default_month = 12
  1903. _prefix = "A"
  1904. _day_opt = "end"
  1905. cdef readonly:
  1906. int _period_dtype_code
  1907. def __init__(self, n=1, normalize=False, month=None):
  1908. # Because YearEnd can be the freq for a Period, define its
  1909. # _period_dtype_code at construction for performance
  1910. YearOffset.__init__(self, n, normalize, month)
  1911. self._period_dtype_code = PeriodDtypeCode.A + self.month % 12
  1912. cdef class YearBegin(YearOffset):
  1913. """
  1914. DateOffset of one year at beginning.
  1915. YearBegin goes to the next date which is a start of the year.
  1916. See Also
  1917. --------
  1918. :class:`~pandas.tseries.offsets.DateOffset` : Standard kind of date increment.
  1919. Examples
  1920. --------
  1921. >>> ts = pd.Timestamp(2022, 12, 1)
  1922. >>> ts + pd.offsets.YearBegin()
  1923. Timestamp('2023-01-01 00:00:00')
  1924. >>> ts = pd.Timestamp(2023, 1, 1)
  1925. >>> ts + pd.offsets.YearBegin()
  1926. Timestamp('2024-01-01 00:00:00')
  1927. If you want to get the start of the current year:
  1928. >>> ts = pd.Timestamp(2023, 1, 1)
  1929. >>> pd.offsets.YearBegin().rollback(ts)
  1930. Timestamp('2023-01-01 00:00:00')
  1931. """
  1932. _default_month = 1
  1933. _prefix = "AS"
  1934. _day_opt = "start"
  1935. # ----------------------------------------------------------------------
  1936. # Quarter-Based Offset Classes
  1937. cdef class QuarterOffset(SingleConstructorOffset):
  1938. _attributes = tuple(["n", "normalize", "startingMonth"])
  1939. # TODO: Consider combining QuarterOffset and YearOffset __init__ at some
  1940. # point. Also apply_index, is_on_offset, rule_code if
  1941. # startingMonth vs month attr names are resolved
  1942. # FIXME(cython#4446): python annotation here gives compile-time errors
  1943. # _default_starting_month: int
  1944. # _from_name_starting_month: int
  1945. cdef readonly:
  1946. int startingMonth
  1947. def __init__(self, n=1, normalize=False, startingMonth=None):
  1948. BaseOffset.__init__(self, n, normalize)
  1949. if startingMonth is None:
  1950. startingMonth = self._default_starting_month
  1951. self.startingMonth = startingMonth
  1952. cpdef __setstate__(self, state):
  1953. self.startingMonth = state.pop("startingMonth")
  1954. self.n = state.pop("n")
  1955. self.normalize = state.pop("normalize")
  1956. @classmethod
  1957. def _from_name(cls, suffix=None):
  1958. kwargs = {}
  1959. if suffix:
  1960. kwargs["startingMonth"] = MONTH_TO_CAL_NUM[suffix]
  1961. else:
  1962. if cls._from_name_starting_month is not None:
  1963. kwargs["startingMonth"] = cls._from_name_starting_month
  1964. return cls(**kwargs)
  1965. @property
  1966. def rule_code(self) -> str:
  1967. month = MONTH_ALIASES[self.startingMonth]
  1968. return f"{self._prefix}-{month}"
  1969. def is_anchored(self) -> bool:
  1970. return self.n == 1 and self.startingMonth is not None
  1971. def is_on_offset(self, dt: datetime) -> bool:
  1972. if self.normalize and not _is_normalized(dt):
  1973. return False
  1974. mod_month = (dt.month - self.startingMonth) % 3
  1975. return mod_month == 0 and dt.day == self._get_offset_day(dt)
  1976. @apply_wraps
  1977. def _apply(self, other: datetime) -> datetime:
  1978. # months_since: find the calendar quarter containing other.month,
  1979. # e.g. if other.month == 8, the calendar quarter is [Jul, Aug, Sep].
  1980. # Then find the month in that quarter containing an is_on_offset date for
  1981. # self. `months_since` is the number of months to shift other.month
  1982. # to get to this on-offset month.
  1983. months_since = other.month % 3 - self.startingMonth % 3
  1984. qtrs = roll_qtrday(
  1985. other, self.n, self.startingMonth, day_opt=self._day_opt, modby=3
  1986. )
  1987. months = qtrs * 3 - months_since
  1988. return shift_month(other, months, self._day_opt)
  1989. @apply_array_wraps
  1990. def _apply_array(self, dtarr):
  1991. reso = get_unit_from_dtype(dtarr.dtype)
  1992. shifted = shift_quarters(
  1993. dtarr.view("i8"),
  1994. self.n,
  1995. self.startingMonth,
  1996. self._day_opt,
  1997. modby=3,
  1998. reso=reso,
  1999. )
  2000. return shifted
  2001. cdef class BQuarterEnd(QuarterOffset):
  2002. """
  2003. DateOffset increments between the last business day of each Quarter.
  2004. startingMonth = 1 corresponds to dates like 1/31/2007, 4/30/2007, ...
  2005. startingMonth = 2 corresponds to dates like 2/28/2007, 5/31/2007, ...
  2006. startingMonth = 3 corresponds to dates like 3/30/2007, 6/29/2007, ...
  2007. Examples
  2008. --------
  2009. >>> from pandas.tseries.offsets import BQuarterEnd
  2010. >>> ts = pd.Timestamp('2020-05-24 05:01:15')
  2011. >>> ts + BQuarterEnd()
  2012. Timestamp('2020-06-30 05:01:15')
  2013. >>> ts + BQuarterEnd(2)
  2014. Timestamp('2020-09-30 05:01:15')
  2015. >>> ts + BQuarterEnd(1, startingMonth=2)
  2016. Timestamp('2020-05-29 05:01:15')
  2017. >>> ts + BQuarterEnd(startingMonth=2)
  2018. Timestamp('2020-05-29 05:01:15')
  2019. """
  2020. _output_name = "BusinessQuarterEnd"
  2021. _default_starting_month = 3
  2022. _from_name_starting_month = 12
  2023. _prefix = "BQ"
  2024. _day_opt = "business_end"
  2025. cdef class BQuarterBegin(QuarterOffset):
  2026. """
  2027. DateOffset increments between the first business day of each Quarter.
  2028. startingMonth = 1 corresponds to dates like 1/01/2007, 4/01/2007, ...
  2029. startingMonth = 2 corresponds to dates like 2/01/2007, 5/01/2007, ...
  2030. startingMonth = 3 corresponds to dates like 3/01/2007, 6/01/2007, ...
  2031. Examples
  2032. --------
  2033. >>> from pandas.tseries.offsets import BQuarterBegin
  2034. >>> ts = pd.Timestamp('2020-05-24 05:01:15')
  2035. >>> ts + BQuarterBegin()
  2036. Timestamp('2020-06-01 05:01:15')
  2037. >>> ts + BQuarterBegin(2)
  2038. Timestamp('2020-09-01 05:01:15')
  2039. >>> ts + BQuarterBegin(startingMonth=2)
  2040. Timestamp('2020-08-03 05:01:15')
  2041. >>> ts + BQuarterBegin(-1)
  2042. Timestamp('2020-03-02 05:01:15')
  2043. """
  2044. _output_name = "BusinessQuarterBegin"
  2045. _default_starting_month = 3
  2046. _from_name_starting_month = 1
  2047. _prefix = "BQS"
  2048. _day_opt = "business_start"
  2049. cdef class QuarterEnd(QuarterOffset):
  2050. """
  2051. DateOffset increments between Quarter end dates.
  2052. startingMonth = 1 corresponds to dates like 1/31/2007, 4/30/2007, ...
  2053. startingMonth = 2 corresponds to dates like 2/28/2007, 5/31/2007, ...
  2054. startingMonth = 3 corresponds to dates like 3/31/2007, 6/30/2007, ...
  2055. Examples
  2056. --------
  2057. >>> ts = pd.Timestamp(2022, 1, 1)
  2058. >>> ts + pd.offsets.QuarterEnd()
  2059. Timestamp('2022-03-31 00:00:00')
  2060. """
  2061. _default_starting_month = 3
  2062. _prefix = "Q"
  2063. _day_opt = "end"
  2064. cdef readonly:
  2065. int _period_dtype_code
  2066. def __init__(self, n=1, normalize=False, startingMonth=None):
  2067. # Because QuarterEnd can be the freq for a Period, define its
  2068. # _period_dtype_code at construction for performance
  2069. QuarterOffset.__init__(self, n, normalize, startingMonth)
  2070. self._period_dtype_code = PeriodDtypeCode.Q_DEC + self.startingMonth % 12
  2071. cdef class QuarterBegin(QuarterOffset):
  2072. """
  2073. DateOffset increments between Quarter start dates.
  2074. startingMonth = 1 corresponds to dates like 1/01/2007, 4/01/2007, ...
  2075. startingMonth = 2 corresponds to dates like 2/01/2007, 5/01/2007, ...
  2076. startingMonth = 3 corresponds to dates like 3/01/2007, 6/01/2007, ...
  2077. Examples
  2078. --------
  2079. >>> ts = pd.Timestamp(2022, 1, 1)
  2080. >>> ts + pd.offsets.QuarterBegin()
  2081. Timestamp('2022-03-01 00:00:00')
  2082. """
  2083. _default_starting_month = 3
  2084. _from_name_starting_month = 1
  2085. _prefix = "QS"
  2086. _day_opt = "start"
  2087. # ----------------------------------------------------------------------
  2088. # Month-Based Offset Classes
  2089. cdef class MonthOffset(SingleConstructorOffset):
  2090. def is_on_offset(self, dt: datetime) -> bool:
  2091. if self.normalize and not _is_normalized(dt):
  2092. return False
  2093. return dt.day == self._get_offset_day(dt)
  2094. @apply_wraps
  2095. def _apply(self, other: datetime) -> datetime:
  2096. compare_day = self._get_offset_day(other)
  2097. n = roll_convention(other.day, self.n, compare_day)
  2098. return shift_month(other, n, self._day_opt)
  2099. @apply_array_wraps
  2100. def _apply_array(self, dtarr):
  2101. reso = get_unit_from_dtype(dtarr.dtype)
  2102. shifted = shift_months(dtarr.view("i8"), self.n, self._day_opt, reso=reso)
  2103. return shifted
  2104. cpdef __setstate__(self, state):
  2105. state.pop("_use_relativedelta", False)
  2106. state.pop("offset", None)
  2107. state.pop("_offset", None)
  2108. state.pop("kwds", {})
  2109. BaseOffset.__setstate__(self, state)
  2110. cdef class MonthEnd(MonthOffset):
  2111. """
  2112. DateOffset of one month end.
  2113. MonthEnd goes to the next date which is an end of the month.
  2114. See Also
  2115. --------
  2116. :class:`~pandas.tseries.offsets.DateOffset` : Standard kind of date increment.
  2117. Examples
  2118. --------
  2119. >>> ts = pd.Timestamp(2022, 1, 30)
  2120. >>> ts + pd.offsets.MonthEnd()
  2121. Timestamp('2022-01-31 00:00:00')
  2122. >>> ts = pd.Timestamp(2022, 1, 31)
  2123. >>> ts + pd.offsets.MonthEnd()
  2124. Timestamp('2022-02-28 00:00:00')
  2125. If you want to get the end of the current month:
  2126. >>> ts = pd.Timestamp(2022, 1, 31)
  2127. >>> pd.offsets.MonthEnd().rollforward(ts)
  2128. Timestamp('2022-01-31 00:00:00')
  2129. """
  2130. _period_dtype_code = PeriodDtypeCode.M
  2131. _prefix = "M"
  2132. _day_opt = "end"
  2133. cdef class MonthBegin(MonthOffset):
  2134. """
  2135. DateOffset of one month at beginning.
  2136. Examples
  2137. --------
  2138. >>> ts = pd.Timestamp(2022, 1, 1)
  2139. >>> ts + pd.offsets.MonthBegin()
  2140. Timestamp('2022-02-01 00:00:00')
  2141. """
  2142. _prefix = "MS"
  2143. _day_opt = "start"
  2144. cdef class BusinessMonthEnd(MonthOffset):
  2145. """
  2146. DateOffset increments between the last business day of the month.
  2147. BusinessMonthEnd goes to the next date which is the last business day of the month.
  2148. Examples
  2149. --------
  2150. >>> ts = pd.Timestamp(2022, 11, 29)
  2151. >>> ts + pd.offsets.BMonthEnd()
  2152. Timestamp('2022-11-30 00:00:00')
  2153. >>> ts = pd.Timestamp(2022, 11, 30)
  2154. >>> ts + pd.offsets.BMonthEnd()
  2155. Timestamp('2022-12-30 00:00:00')
  2156. If you want to get the end of the current business month:
  2157. >>> ts = pd.Timestamp(2022, 11, 30)
  2158. >>> pd.offsets.BMonthEnd().rollforward(ts)
  2159. Timestamp('2022-11-30 00:00:00')
  2160. """
  2161. _prefix = "BM"
  2162. _day_opt = "business_end"
  2163. cdef class BusinessMonthBegin(MonthOffset):
  2164. """
  2165. DateOffset of one month at the first business day.
  2166. Examples
  2167. --------
  2168. >>> from pandas.tseries.offsets import BMonthBegin
  2169. >>> ts=pd.Timestamp('2020-05-24 05:01:15')
  2170. >>> ts + BMonthBegin()
  2171. Timestamp('2020-06-01 05:01:15')
  2172. >>> ts + BMonthBegin(2)
  2173. Timestamp('2020-07-01 05:01:15')
  2174. >>> ts + BMonthBegin(-3)
  2175. Timestamp('2020-03-02 05:01:15')
  2176. """
  2177. _prefix = "BMS"
  2178. _day_opt = "business_start"
  2179. # ---------------------------------------------------------------------
  2180. # Semi-Month Based Offsets
  2181. cdef class SemiMonthOffset(SingleConstructorOffset):
  2182. _default_day_of_month = 15
  2183. _min_day_of_month = 2
  2184. _attributes = tuple(["n", "normalize", "day_of_month"])
  2185. cdef readonly:
  2186. int day_of_month
  2187. def __init__(self, n=1, normalize=False, day_of_month=None):
  2188. BaseOffset.__init__(self, n, normalize)
  2189. if day_of_month is None:
  2190. day_of_month = self._default_day_of_month
  2191. self.day_of_month = int(day_of_month)
  2192. if not self._min_day_of_month <= self.day_of_month <= 27:
  2193. raise ValueError(
  2194. "day_of_month must be "
  2195. f"{self._min_day_of_month}<=day_of_month<=27, "
  2196. f"got {self.day_of_month}"
  2197. )
  2198. cpdef __setstate__(self, state):
  2199. self.n = state.pop("n")
  2200. self.normalize = state.pop("normalize")
  2201. self.day_of_month = state.pop("day_of_month")
  2202. @classmethod
  2203. def _from_name(cls, suffix=None):
  2204. return cls(day_of_month=suffix)
  2205. @property
  2206. def rule_code(self) -> str:
  2207. suffix = f"-{self.day_of_month}"
  2208. return self._prefix + suffix
  2209. @apply_wraps
  2210. def _apply(self, other: datetime) -> datetime:
  2211. is_start = isinstance(self, SemiMonthBegin)
  2212. # shift `other` to self.day_of_month, incrementing `n` if necessary
  2213. n = roll_convention(other.day, self.n, self.day_of_month)
  2214. days_in_month = get_days_in_month(other.year, other.month)
  2215. # For SemiMonthBegin on other.day == 1 and
  2216. # SemiMonthEnd on other.day == days_in_month,
  2217. # shifting `other` to `self.day_of_month` _always_ requires
  2218. # incrementing/decrementing `n`, regardless of whether it is
  2219. # initially positive.
  2220. if is_start and (self.n <= 0 and other.day == 1):
  2221. n -= 1
  2222. elif (not is_start) and (self.n > 0 and other.day == days_in_month):
  2223. n += 1
  2224. if is_start:
  2225. months = n // 2 + n % 2
  2226. to_day = 1 if n % 2 else self.day_of_month
  2227. else:
  2228. months = n // 2
  2229. to_day = 31 if n % 2 else self.day_of_month
  2230. return shift_month(other, months, to_day)
  2231. @apply_array_wraps
  2232. @cython.wraparound(False)
  2233. @cython.boundscheck(False)
  2234. def _apply_array(self, dtarr):
  2235. cdef:
  2236. ndarray i8other = dtarr.view("i8")
  2237. Py_ssize_t i, count = dtarr.size
  2238. int64_t val, res_val
  2239. ndarray out = cnp.PyArray_EMPTY(
  2240. i8other.ndim, i8other.shape, cnp.NPY_INT64, 0
  2241. )
  2242. npy_datetimestruct dts
  2243. int months, to_day, nadj, n = self.n
  2244. int days_in_month, day, anchor_dom = self.day_of_month
  2245. bint is_start = isinstance(self, SemiMonthBegin)
  2246. NPY_DATETIMEUNIT reso = get_unit_from_dtype(dtarr.dtype)
  2247. cnp.broadcast mi = cnp.PyArray_MultiIterNew2(out, i8other)
  2248. with nogil:
  2249. for i in range(count):
  2250. # Analogous to: val = i8other[i]
  2251. val = (<int64_t*>cnp.PyArray_MultiIter_DATA(mi, 1))[0]
  2252. if val == NPY_NAT:
  2253. res_val = NPY_NAT
  2254. else:
  2255. pandas_datetime_to_datetimestruct(val, reso, &dts)
  2256. day = dts.day
  2257. # Adjust so that we are always looking at self.day_of_month,
  2258. # incrementing/decrementing n if necessary.
  2259. nadj = roll_convention(day, n, anchor_dom)
  2260. days_in_month = get_days_in_month(dts.year, dts.month)
  2261. # For SemiMonthBegin on other.day == 1 and
  2262. # SemiMonthEnd on other.day == days_in_month,
  2263. # shifting `other` to `self.day_of_month` _always_ requires
  2264. # incrementing/decrementing `n`, regardless of whether it is
  2265. # initially positive.
  2266. if is_start and (n <= 0 and day == 1):
  2267. nadj -= 1
  2268. elif (not is_start) and (n > 0 and day == days_in_month):
  2269. nadj += 1
  2270. if is_start:
  2271. # See also: SemiMonthBegin._apply
  2272. months = nadj // 2 + nadj % 2
  2273. to_day = 1 if nadj % 2 else anchor_dom
  2274. else:
  2275. # See also: SemiMonthEnd._apply
  2276. months = nadj // 2
  2277. to_day = 31 if nadj % 2 else anchor_dom
  2278. dts.year = year_add_months(dts, months)
  2279. dts.month = month_add_months(dts, months)
  2280. days_in_month = get_days_in_month(dts.year, dts.month)
  2281. dts.day = min(to_day, days_in_month)
  2282. res_val = npy_datetimestruct_to_datetime(reso, &dts)
  2283. # Analogous to: out[i] = res_val
  2284. (<int64_t*>cnp.PyArray_MultiIter_DATA(mi, 0))[0] = res_val
  2285. cnp.PyArray_MultiIter_NEXT(mi)
  2286. return out
  2287. cdef class SemiMonthEnd(SemiMonthOffset):
  2288. """
  2289. Two DateOffset's per month repeating on the last day of the month & day_of_month.
  2290. Parameters
  2291. ----------
  2292. n : int
  2293. normalize : bool, default False
  2294. day_of_month : int, {1, 3,...,27}, default 15
  2295. Examples
  2296. --------
  2297. >>> ts = pd.Timestamp(2022, 1, 14)
  2298. >>> ts + pd.offsets.SemiMonthEnd()
  2299. Timestamp('2022-01-15 00:00:00')
  2300. >>> ts = pd.Timestamp(2022, 1, 15)
  2301. >>> ts + pd.offsets.SemiMonthEnd()
  2302. Timestamp('2022-01-31 00:00:00')
  2303. >>> ts = pd.Timestamp(2022, 1, 31)
  2304. >>> ts + pd.offsets.SemiMonthEnd()
  2305. Timestamp('2022-02-15 00:00:00')
  2306. If you want to get the result for the current month:
  2307. >>> ts = pd.Timestamp(2022, 1, 15)
  2308. >>> pd.offsets.SemiMonthEnd().rollforward(ts)
  2309. Timestamp('2022-01-15 00:00:00')
  2310. """
  2311. _prefix = "SM"
  2312. _min_day_of_month = 1
  2313. def is_on_offset(self, dt: datetime) -> bool:
  2314. if self.normalize and not _is_normalized(dt):
  2315. return False
  2316. days_in_month = get_days_in_month(dt.year, dt.month)
  2317. return dt.day in (self.day_of_month, days_in_month)
  2318. cdef class SemiMonthBegin(SemiMonthOffset):
  2319. """
  2320. Two DateOffset's per month repeating on the first day of the month & day_of_month.
  2321. Parameters
  2322. ----------
  2323. n : int
  2324. normalize : bool, default False
  2325. day_of_month : int, {2, 3,...,27}, default 15
  2326. Examples
  2327. --------
  2328. >>> ts = pd.Timestamp(2022, 1, 1)
  2329. >>> ts + pd.offsets.SemiMonthBegin()
  2330. Timestamp('2022-01-15 00:00:00')
  2331. """
  2332. _prefix = "SMS"
  2333. def is_on_offset(self, dt: datetime) -> bool:
  2334. if self.normalize and not _is_normalized(dt):
  2335. return False
  2336. return dt.day in (1, self.day_of_month)
  2337. # ---------------------------------------------------------------------
  2338. # Week-Based Offset Classes
  2339. cdef class Week(SingleConstructorOffset):
  2340. """
  2341. Weekly offset.
  2342. Parameters
  2343. ----------
  2344. weekday : int or None, default None
  2345. Always generate specific day of week.
  2346. 0 for Monday and 6 for Sunday.
  2347. See Also
  2348. --------
  2349. pd.tseries.offsets.WeekOfMonth :
  2350. Describes monthly dates like, the Tuesday of the
  2351. 2nd week of each month.
  2352. Examples
  2353. --------
  2354. >>> date_object = pd.Timestamp("2023-01-13")
  2355. >>> date_object
  2356. Timestamp('2023-01-13 00:00:00')
  2357. >>> date_plus_one_week = date_object + pd.tseries.offsets.Week(n=1)
  2358. >>> date_plus_one_week
  2359. Timestamp('2023-01-20 00:00:00')
  2360. >>> date_next_monday = date_object + pd.tseries.offsets.Week(weekday=0)
  2361. >>> date_next_monday
  2362. Timestamp('2023-01-16 00:00:00')
  2363. >>> date_next_sunday = date_object + pd.tseries.offsets.Week(weekday=6)
  2364. >>> date_next_sunday
  2365. Timestamp('2023-01-15 00:00:00')
  2366. """
  2367. _inc = timedelta(weeks=1)
  2368. _prefix = "W"
  2369. _attributes = tuple(["n", "normalize", "weekday"])
  2370. cdef readonly:
  2371. object weekday # int or None
  2372. int _period_dtype_code
  2373. def __init__(self, n=1, normalize=False, weekday=None):
  2374. BaseOffset.__init__(self, n, normalize)
  2375. self.weekday = weekday
  2376. if self.weekday is not None:
  2377. if self.weekday < 0 or self.weekday > 6:
  2378. raise ValueError(f"Day must be 0<=day<=6, got {self.weekday}")
  2379. self._period_dtype_code = PeriodDtypeCode.W_SUN + (weekday + 1) % 7
  2380. cpdef __setstate__(self, state):
  2381. self.n = state.pop("n")
  2382. self.normalize = state.pop("normalize")
  2383. self.weekday = state.pop("weekday")
  2384. self._cache = state.pop("_cache", {})
  2385. def is_anchored(self) -> bool:
  2386. return self.n == 1 and self.weekday is not None
  2387. @apply_wraps
  2388. def _apply(self, other):
  2389. if self.weekday is None:
  2390. return other + self.n * self._inc
  2391. if not PyDateTime_Check(other):
  2392. raise TypeError(
  2393. f"Cannot add {type(other).__name__} to {type(self).__name__}"
  2394. )
  2395. k = self.n
  2396. otherDay = other.weekday()
  2397. if otherDay != self.weekday:
  2398. other = other + timedelta((self.weekday - otherDay) % 7)
  2399. if k > 0:
  2400. k -= 1
  2401. return other + timedelta(weeks=k)
  2402. @apply_array_wraps
  2403. def _apply_array(self, dtarr):
  2404. if self.weekday is None:
  2405. td = timedelta(days=7 * self.n)
  2406. td64 = np.timedelta64(td, "ns")
  2407. return dtarr + td64
  2408. else:
  2409. reso = get_unit_from_dtype(dtarr.dtype)
  2410. i8other = dtarr.view("i8")
  2411. return self._end_apply_index(i8other, reso=reso)
  2412. @cython.wraparound(False)
  2413. @cython.boundscheck(False)
  2414. cdef ndarray _end_apply_index(self, ndarray i8other, NPY_DATETIMEUNIT reso):
  2415. """
  2416. Add self to the given DatetimeIndex, specialized for case where
  2417. self.weekday is non-null.
  2418. Parameters
  2419. ----------
  2420. i8other : const int64_t[:]
  2421. reso : NPY_DATETIMEUNIT
  2422. Returns
  2423. -------
  2424. ndarray[int64_t]
  2425. """
  2426. cdef:
  2427. Py_ssize_t i, count = i8other.size
  2428. int64_t val, res_val
  2429. ndarray out = cnp.PyArray_EMPTY(
  2430. i8other.ndim, i8other.shape, cnp.NPY_INT64, 0
  2431. )
  2432. npy_datetimestruct dts
  2433. int wday, days, weeks, n = self.n
  2434. int anchor_weekday = self.weekday
  2435. int64_t DAY_PERIODS = periods_per_day(reso)
  2436. cnp.broadcast mi = cnp.PyArray_MultiIterNew2(out, i8other)
  2437. with nogil:
  2438. for i in range(count):
  2439. # Analogous to: val = i8other[i]
  2440. val = (<int64_t*>cnp.PyArray_MultiIter_DATA(mi, 1))[0]
  2441. if val == NPY_NAT:
  2442. res_val = NPY_NAT
  2443. else:
  2444. pandas_datetime_to_datetimestruct(val, reso, &dts)
  2445. wday = dayofweek(dts.year, dts.month, dts.day)
  2446. days = 0
  2447. weeks = n
  2448. if wday != anchor_weekday:
  2449. days = (anchor_weekday - wday) % 7
  2450. if weeks > 0:
  2451. weeks -= 1
  2452. res_val = val + (7 * weeks + days) * DAY_PERIODS
  2453. # Analogous to: out[i] = res_val
  2454. (<int64_t*>cnp.PyArray_MultiIter_DATA(mi, 0))[0] = res_val
  2455. cnp.PyArray_MultiIter_NEXT(mi)
  2456. return out
  2457. def is_on_offset(self, dt: datetime) -> bool:
  2458. if self.normalize and not _is_normalized(dt):
  2459. return False
  2460. elif self.weekday is None:
  2461. return True
  2462. return dt.weekday() == self.weekday
  2463. @property
  2464. def rule_code(self) -> str:
  2465. suffix = ""
  2466. if self.weekday is not None:
  2467. weekday = int_to_weekday[self.weekday]
  2468. suffix = f"-{weekday}"
  2469. return self._prefix + suffix
  2470. @classmethod
  2471. def _from_name(cls, suffix=None):
  2472. if not suffix:
  2473. weekday = None
  2474. else:
  2475. weekday = weekday_to_int[suffix]
  2476. return cls(weekday=weekday)
  2477. cdef class WeekOfMonth(WeekOfMonthMixin):
  2478. """
  2479. Describes monthly dates like "the Tuesday of the 2nd week of each month".
  2480. Parameters
  2481. ----------
  2482. n : int
  2483. week : int {0, 1, 2, 3, ...}, default 0
  2484. A specific integer for the week of the month.
  2485. e.g. 0 is 1st week of month, 1 is the 2nd week, etc.
  2486. weekday : int {0, 1, ..., 6}, default 0
  2487. A specific integer for the day of the week.
  2488. - 0 is Monday
  2489. - 1 is Tuesday
  2490. - 2 is Wednesday
  2491. - 3 is Thursday
  2492. - 4 is Friday
  2493. - 5 is Saturday
  2494. - 6 is Sunday.
  2495. Examples
  2496. --------
  2497. >>> ts = pd.Timestamp(2022, 1, 1)
  2498. >>> ts + pd.offsets.WeekOfMonth()
  2499. Timestamp('2022-01-03 00:00:00')
  2500. """
  2501. _prefix = "WOM"
  2502. _attributes = tuple(["n", "normalize", "week", "weekday"])
  2503. def __init__(self, n=1, normalize=False, week=0, weekday=0):
  2504. WeekOfMonthMixin.__init__(self, n, normalize, weekday)
  2505. self.week = week
  2506. if self.week < 0 or self.week > 3:
  2507. raise ValueError(f"Week must be 0<=week<=3, got {self.week}")
  2508. cpdef __setstate__(self, state):
  2509. self.n = state.pop("n")
  2510. self.normalize = state.pop("normalize")
  2511. self.weekday = state.pop("weekday")
  2512. self.week = state.pop("week")
  2513. def _get_offset_day(self, other: datetime) -> int:
  2514. """
  2515. Find the day in the same month as other that has the same
  2516. weekday as self.weekday and is the self.week'th such day in the month.
  2517. Parameters
  2518. ----------
  2519. other : datetime
  2520. Returns
  2521. -------
  2522. day : int
  2523. """
  2524. mstart = datetime(other.year, other.month, 1)
  2525. wday = mstart.weekday()
  2526. shift_days = (self.weekday - wday) % 7
  2527. return 1 + shift_days + self.week * 7
  2528. @classmethod
  2529. def _from_name(cls, suffix=None):
  2530. if not suffix:
  2531. raise ValueError(f"Prefix {repr(cls._prefix)} requires a suffix.")
  2532. # only one digit weeks (1 --> week 0, 2 --> week 1, etc.)
  2533. week = int(suffix[0]) - 1
  2534. weekday = weekday_to_int[suffix[1:]]
  2535. return cls(week=week, weekday=weekday)
  2536. cdef class LastWeekOfMonth(WeekOfMonthMixin):
  2537. """
  2538. Describes monthly dates in last week of month.
  2539. For example "the last Tuesday of each month".
  2540. Parameters
  2541. ----------
  2542. n : int, default 1
  2543. weekday : int {0, 1, ..., 6}, default 0
  2544. A specific integer for the day of the week.
  2545. - 0 is Monday
  2546. - 1 is Tuesday
  2547. - 2 is Wednesday
  2548. - 3 is Thursday
  2549. - 4 is Friday
  2550. - 5 is Saturday
  2551. - 6 is Sunday.
  2552. Examples
  2553. --------
  2554. >>> ts = pd.Timestamp(2022, 1, 1)
  2555. >>> ts + pd.offsets.LastWeekOfMonth()
  2556. Timestamp('2022-01-31 00:00:00')
  2557. """
  2558. _prefix = "LWOM"
  2559. _attributes = tuple(["n", "normalize", "weekday"])
  2560. def __init__(self, n=1, normalize=False, weekday=0):
  2561. WeekOfMonthMixin.__init__(self, n, normalize, weekday)
  2562. self.week = -1
  2563. if self.n == 0:
  2564. raise ValueError("N cannot be 0")
  2565. cpdef __setstate__(self, state):
  2566. self.n = state.pop("n")
  2567. self.normalize = state.pop("normalize")
  2568. self.weekday = state.pop("weekday")
  2569. self.week = -1
  2570. def _get_offset_day(self, other: datetime) -> int:
  2571. """
  2572. Find the day in the same month as other that has the same
  2573. weekday as self.weekday and is the last such day in the month.
  2574. Parameters
  2575. ----------
  2576. other: datetime
  2577. Returns
  2578. -------
  2579. day: int
  2580. """
  2581. dim = get_days_in_month(other.year, other.month)
  2582. mend = datetime(other.year, other.month, dim)
  2583. wday = mend.weekday()
  2584. shift_days = (wday - self.weekday) % 7
  2585. return dim - shift_days
  2586. @classmethod
  2587. def _from_name(cls, suffix=None):
  2588. if not suffix:
  2589. raise ValueError(f"Prefix {repr(cls._prefix)} requires a suffix.")
  2590. weekday = weekday_to_int[suffix]
  2591. return cls(weekday=weekday)
  2592. # ---------------------------------------------------------------------
  2593. # Special Offset Classes
  2594. cdef class FY5253Mixin(SingleConstructorOffset):
  2595. cdef readonly:
  2596. int startingMonth
  2597. int weekday
  2598. str variation
  2599. def __init__(
  2600. self, n=1, normalize=False, weekday=0, startingMonth=1, variation="nearest"
  2601. ):
  2602. BaseOffset.__init__(self, n, normalize)
  2603. self.startingMonth = startingMonth
  2604. self.weekday = weekday
  2605. self.variation = variation
  2606. if self.n == 0:
  2607. raise ValueError("N cannot be 0")
  2608. if self.variation not in ["nearest", "last"]:
  2609. raise ValueError(f"{self.variation} is not a valid variation")
  2610. cpdef __setstate__(self, state):
  2611. self.n = state.pop("n")
  2612. self.normalize = state.pop("normalize")
  2613. self.weekday = state.pop("weekday")
  2614. self.variation = state.pop("variation")
  2615. def is_anchored(self) -> bool:
  2616. return (
  2617. self.n == 1 and self.startingMonth is not None and self.weekday is not None
  2618. )
  2619. # --------------------------------------------------------------------
  2620. # Name-related methods
  2621. @property
  2622. def rule_code(self) -> str:
  2623. prefix = self._prefix
  2624. suffix = self.get_rule_code_suffix()
  2625. return f"{prefix}-{suffix}"
  2626. def _get_suffix_prefix(self) -> str:
  2627. if self.variation == "nearest":
  2628. return "N"
  2629. else:
  2630. return "L"
  2631. def get_rule_code_suffix(self) -> str:
  2632. prefix = self._get_suffix_prefix()
  2633. month = MONTH_ALIASES[self.startingMonth]
  2634. weekday = int_to_weekday[self.weekday]
  2635. return f"{prefix}-{month}-{weekday}"
  2636. cdef class FY5253(FY5253Mixin):
  2637. """
  2638. Describes 52-53 week fiscal year. This is also known as a 4-4-5 calendar.
  2639. It is used by companies that desire that their
  2640. fiscal year always end on the same day of the week.
  2641. It is a method of managing accounting periods.
  2642. It is a common calendar structure for some industries,
  2643. such as retail, manufacturing and parking industry.
  2644. For more information see:
  2645. https://en.wikipedia.org/wiki/4-4-5_calendar
  2646. The year may either:
  2647. - end on the last X day of the Y month.
  2648. - end on the last X day closest to the last day of the Y month.
  2649. X is a specific day of the week.
  2650. Y is a certain month of the year
  2651. Parameters
  2652. ----------
  2653. n : int
  2654. weekday : int {0, 1, ..., 6}, default 0
  2655. A specific integer for the day of the week.
  2656. - 0 is Monday
  2657. - 1 is Tuesday
  2658. - 2 is Wednesday
  2659. - 3 is Thursday
  2660. - 4 is Friday
  2661. - 5 is Saturday
  2662. - 6 is Sunday.
  2663. startingMonth : int {1, 2, ... 12}, default 1
  2664. The month in which the fiscal year ends.
  2665. variation : str, default "nearest"
  2666. Method of employing 4-4-5 calendar.
  2667. There are two options:
  2668. - "nearest" means year end is **weekday** closest to last day of month in year.
  2669. - "last" means year end is final **weekday** of the final month in fiscal year.
  2670. Examples
  2671. --------
  2672. >>> ts = pd.Timestamp(2022, 1, 1)
  2673. >>> ts + pd.offsets.FY5253()
  2674. Timestamp('2022-01-31 00:00:00')
  2675. """
  2676. _prefix = "RE"
  2677. _attributes = tuple(["n", "normalize", "weekday", "startingMonth", "variation"])
  2678. def is_on_offset(self, dt: datetime) -> bool:
  2679. if self.normalize and not _is_normalized(dt):
  2680. return False
  2681. dt = datetime(dt.year, dt.month, dt.day)
  2682. year_end = self.get_year_end(dt)
  2683. if self.variation == "nearest":
  2684. # We have to check the year end of "this" cal year AND the previous
  2685. return year_end == dt or self.get_year_end(shift_month(dt, -1, None)) == dt
  2686. else:
  2687. return year_end == dt
  2688. @apply_wraps
  2689. def _apply(self, other: datetime) -> datetime:
  2690. norm = Timestamp(other).normalize()
  2691. n = self.n
  2692. prev_year = self.get_year_end(datetime(other.year - 1, self.startingMonth, 1))
  2693. cur_year = self.get_year_end(datetime(other.year, self.startingMonth, 1))
  2694. next_year = self.get_year_end(datetime(other.year + 1, self.startingMonth, 1))
  2695. prev_year = localize_pydatetime(prev_year, other.tzinfo)
  2696. cur_year = localize_pydatetime(cur_year, other.tzinfo)
  2697. next_year = localize_pydatetime(next_year, other.tzinfo)
  2698. # Note: next_year.year == other.year + 1, so we will always
  2699. # have other < next_year
  2700. if norm == prev_year:
  2701. n -= 1
  2702. elif norm == cur_year:
  2703. pass
  2704. elif n > 0:
  2705. if norm < prev_year:
  2706. n -= 2
  2707. elif prev_year < norm < cur_year:
  2708. n -= 1
  2709. elif cur_year < norm < next_year:
  2710. pass
  2711. else:
  2712. if cur_year < norm < next_year:
  2713. n += 1
  2714. elif prev_year < norm < cur_year:
  2715. pass
  2716. elif (
  2717. norm.year == prev_year.year
  2718. and norm < prev_year
  2719. and prev_year - norm <= timedelta(6)
  2720. ):
  2721. # GH#14774, error when next_year.year == cur_year.year
  2722. # e.g. prev_year == datetime(2004, 1, 3),
  2723. # other == datetime(2004, 1, 1)
  2724. n -= 1
  2725. else:
  2726. assert False
  2727. shifted = datetime(other.year + n, self.startingMonth, 1)
  2728. result = self.get_year_end(shifted)
  2729. result = datetime(
  2730. result.year,
  2731. result.month,
  2732. result.day,
  2733. other.hour,
  2734. other.minute,
  2735. other.second,
  2736. other.microsecond,
  2737. )
  2738. return result
  2739. def get_year_end(self, dt: datetime) -> datetime:
  2740. assert dt.tzinfo is None
  2741. dim = get_days_in_month(dt.year, self.startingMonth)
  2742. target_date = datetime(dt.year, self.startingMonth, dim)
  2743. wkday_diff = self.weekday - target_date.weekday()
  2744. if wkday_diff == 0:
  2745. # year_end is the same for "last" and "nearest" cases
  2746. return target_date
  2747. if self.variation == "last":
  2748. days_forward = (wkday_diff % 7) - 7
  2749. # days_forward is always negative, so we always end up
  2750. # in the same year as dt
  2751. return target_date + timedelta(days=days_forward)
  2752. else:
  2753. # variation == "nearest":
  2754. days_forward = wkday_diff % 7
  2755. if days_forward <= 3:
  2756. # The upcoming self.weekday is closer than the previous one
  2757. return target_date + timedelta(days_forward)
  2758. else:
  2759. # The previous self.weekday is closer than the upcoming one
  2760. return target_date + timedelta(days_forward - 7)
  2761. @classmethod
  2762. def _parse_suffix(cls, varion_code, startingMonth_code, weekday_code):
  2763. if varion_code == "N":
  2764. variation = "nearest"
  2765. elif varion_code == "L":
  2766. variation = "last"
  2767. else:
  2768. raise ValueError(f"Unable to parse varion_code: {varion_code}")
  2769. startingMonth = MONTH_TO_CAL_NUM[startingMonth_code]
  2770. weekday = weekday_to_int[weekday_code]
  2771. return {
  2772. "weekday": weekday,
  2773. "startingMonth": startingMonth,
  2774. "variation": variation,
  2775. }
  2776. @classmethod
  2777. def _from_name(cls, *args):
  2778. return cls(**cls._parse_suffix(*args))
  2779. cdef class FY5253Quarter(FY5253Mixin):
  2780. """
  2781. DateOffset increments between business quarter dates for 52-53 week fiscal year.
  2782. Also known as a 4-4-5 calendar.
  2783. It is used by companies that desire that their
  2784. fiscal year always end on the same day of the week.
  2785. It is a method of managing accounting periods.
  2786. It is a common calendar structure for some industries,
  2787. such as retail, manufacturing and parking industry.
  2788. For more information see:
  2789. https://en.wikipedia.org/wiki/4-4-5_calendar
  2790. The year may either:
  2791. - end on the last X day of the Y month.
  2792. - end on the last X day closest to the last day of the Y month.
  2793. X is a specific day of the week.
  2794. Y is a certain month of the year
  2795. startingMonth = 1 corresponds to dates like 1/31/2007, 4/30/2007, ...
  2796. startingMonth = 2 corresponds to dates like 2/28/2007, 5/31/2007, ...
  2797. startingMonth = 3 corresponds to dates like 3/30/2007, 6/29/2007, ...
  2798. Parameters
  2799. ----------
  2800. n : int
  2801. weekday : int {0, 1, ..., 6}, default 0
  2802. A specific integer for the day of the week.
  2803. - 0 is Monday
  2804. - 1 is Tuesday
  2805. - 2 is Wednesday
  2806. - 3 is Thursday
  2807. - 4 is Friday
  2808. - 5 is Saturday
  2809. - 6 is Sunday.
  2810. startingMonth : int {1, 2, ..., 12}, default 1
  2811. The month in which fiscal years end.
  2812. qtr_with_extra_week : int {1, 2, 3, 4}, default 1
  2813. The quarter number that has the leap or 14 week when needed.
  2814. variation : str, default "nearest"
  2815. Method of employing 4-4-5 calendar.
  2816. There are two options:
  2817. - "nearest" means year end is **weekday** closest to last day of month in year.
  2818. - "last" means year end is final **weekday** of the final month in fiscal year.
  2819. Examples
  2820. --------
  2821. >>> ts = pd.Timestamp(2022, 1, 1)
  2822. >>> ts + pd.offsets.FY5253Quarter()
  2823. Timestamp('2022-01-31 00:00:00')
  2824. """
  2825. _prefix = "REQ"
  2826. _attributes = tuple(
  2827. [
  2828. "n",
  2829. "normalize",
  2830. "weekday",
  2831. "startingMonth",
  2832. "qtr_with_extra_week",
  2833. "variation",
  2834. ]
  2835. )
  2836. cdef readonly:
  2837. int qtr_with_extra_week
  2838. def __init__(
  2839. self,
  2840. n=1,
  2841. normalize=False,
  2842. weekday=0,
  2843. startingMonth=1,
  2844. qtr_with_extra_week=1,
  2845. variation="nearest",
  2846. ):
  2847. FY5253Mixin.__init__(
  2848. self, n, normalize, weekday, startingMonth, variation
  2849. )
  2850. self.qtr_with_extra_week = qtr_with_extra_week
  2851. cpdef __setstate__(self, state):
  2852. FY5253Mixin.__setstate__(self, state)
  2853. self.qtr_with_extra_week = state.pop("qtr_with_extra_week")
  2854. @cache_readonly
  2855. def _offset(self):
  2856. return FY5253(
  2857. startingMonth=self.startingMonth,
  2858. weekday=self.weekday,
  2859. variation=self.variation,
  2860. )
  2861. def _rollback_to_year(self, other: datetime):
  2862. """
  2863. Roll `other` back to the most recent date that was on a fiscal year
  2864. end.
  2865. Return the date of that year-end, the number of full quarters
  2866. elapsed between that year-end and other, and the remaining Timedelta
  2867. since the most recent quarter-end.
  2868. Parameters
  2869. ----------
  2870. other : datetime or Timestamp
  2871. Returns
  2872. -------
  2873. tuple of
  2874. prev_year_end : Timestamp giving most recent fiscal year end
  2875. num_qtrs : int
  2876. tdelta : Timedelta
  2877. """
  2878. num_qtrs = 0
  2879. norm = Timestamp(other).tz_localize(None)
  2880. start = self._offset.rollback(norm)
  2881. # Note: start <= norm and self._offset.is_on_offset(start)
  2882. if start < norm:
  2883. # roll adjustment
  2884. qtr_lens = self.get_weeks(norm)
  2885. # check that qtr_lens is consistent with self._offset addition
  2886. end = _shift_day(start, days=7 * sum(qtr_lens))
  2887. assert self._offset.is_on_offset(end), (start, end, qtr_lens)
  2888. tdelta = norm - start
  2889. for qlen in qtr_lens:
  2890. if qlen * 7 <= tdelta.days:
  2891. num_qtrs += 1
  2892. tdelta -= (
  2893. <_Timedelta>Timedelta(days=qlen * 7)
  2894. )._as_creso(norm._creso)
  2895. else:
  2896. break
  2897. else:
  2898. tdelta = Timedelta(0)
  2899. # Note: we always have tdelta._value>= 0
  2900. return start, num_qtrs, tdelta
  2901. @apply_wraps
  2902. def _apply(self, other: datetime) -> datetime:
  2903. # Note: self.n == 0 is not allowed.
  2904. n = self.n
  2905. prev_year_end, num_qtrs, tdelta = self._rollback_to_year(other)
  2906. res = prev_year_end
  2907. n += num_qtrs
  2908. if self.n <= 0 and tdelta._value > 0:
  2909. n += 1
  2910. # Possible speedup by handling years first.
  2911. years = n // 4
  2912. if years:
  2913. res += self._offset * years
  2914. n -= years * 4
  2915. # Add an extra day to make *sure* we are getting the quarter lengths
  2916. # for the upcoming year, not the previous year
  2917. qtr_lens = self.get_weeks(res + Timedelta(days=1))
  2918. # Note: we always have 0 <= n < 4
  2919. weeks = sum(qtr_lens[:n])
  2920. if weeks:
  2921. res = _shift_day(res, days=weeks * 7)
  2922. return res
  2923. def get_weeks(self, dt: datetime):
  2924. ret = [13] * 4
  2925. year_has_extra_week = self.year_has_extra_week(dt)
  2926. if year_has_extra_week:
  2927. ret[self.qtr_with_extra_week - 1] = 14
  2928. return ret
  2929. def year_has_extra_week(self, dt: datetime) -> bool:
  2930. # Avoid round-down errors --> normalize to get
  2931. # e.g. '370D' instead of '360D23H'
  2932. norm = Timestamp(dt).normalize().tz_localize(None)
  2933. next_year_end = self._offset.rollforward(norm)
  2934. prev_year_end = norm - self._offset
  2935. weeks_in_year = (next_year_end - prev_year_end).days / 7
  2936. assert weeks_in_year in [52, 53], weeks_in_year
  2937. return weeks_in_year == 53
  2938. def is_on_offset(self, dt: datetime) -> bool:
  2939. if self.normalize and not _is_normalized(dt):
  2940. return False
  2941. if self._offset.is_on_offset(dt):
  2942. return True
  2943. next_year_end = dt - self._offset
  2944. qtr_lens = self.get_weeks(dt)
  2945. current = next_year_end
  2946. for qtr_len in qtr_lens:
  2947. current = _shift_day(current, days=qtr_len * 7)
  2948. if dt == current:
  2949. return True
  2950. return False
  2951. @property
  2952. def rule_code(self) -> str:
  2953. suffix = FY5253Mixin.rule_code.__get__(self)
  2954. qtr = self.qtr_with_extra_week
  2955. return f"{suffix}-{qtr}"
  2956. @classmethod
  2957. def _from_name(cls, *args):
  2958. return cls(
  2959. **dict(FY5253._parse_suffix(*args[:-1]), qtr_with_extra_week=int(args[-1]))
  2960. )
  2961. cdef class Easter(SingleConstructorOffset):
  2962. """
  2963. DateOffset for the Easter holiday using logic defined in dateutil.
  2964. Right now uses the revised method which is valid in years 1583-4099.
  2965. Examples
  2966. --------
  2967. >>> ts = pd.Timestamp(2022, 1, 1)
  2968. >>> ts + pd.offsets.Easter()
  2969. Timestamp('2022-04-17 00:00:00')
  2970. """
  2971. cpdef __setstate__(self, state):
  2972. self.n = state.pop("n")
  2973. self.normalize = state.pop("normalize")
  2974. @apply_wraps
  2975. def _apply(self, other: datetime) -> datetime:
  2976. current_easter = easter(other.year)
  2977. current_easter = datetime(
  2978. current_easter.year, current_easter.month, current_easter.day
  2979. )
  2980. current_easter = localize_pydatetime(current_easter, other.tzinfo)
  2981. n = self.n
  2982. if n >= 0 and other < current_easter:
  2983. n -= 1
  2984. elif n < 0 and other > current_easter:
  2985. n += 1
  2986. # TODO: Why does this handle the 0 case the opposite of others?
  2987. # NOTE: easter returns a datetime.date so we have to convert to type of
  2988. # other
  2989. new = easter(other.year + n)
  2990. new = datetime(
  2991. new.year,
  2992. new.month,
  2993. new.day,
  2994. other.hour,
  2995. other.minute,
  2996. other.second,
  2997. other.microsecond,
  2998. )
  2999. return new
  3000. def is_on_offset(self, dt: datetime) -> bool:
  3001. if self.normalize and not _is_normalized(dt):
  3002. return False
  3003. return date(dt.year, dt.month, dt.day) == easter(dt.year)
  3004. # ----------------------------------------------------------------------
  3005. # Custom Offset classes
  3006. cdef class CustomBusinessDay(BusinessDay):
  3007. """
  3008. DateOffset subclass representing custom business days excluding holidays.
  3009. Parameters
  3010. ----------
  3011. n : int, default 1
  3012. The number of days represented.
  3013. normalize : bool, default False
  3014. Normalize start/end dates to midnight before generating date range.
  3015. weekmask : str, Default 'Mon Tue Wed Thu Fri'
  3016. Weekmask of valid business days, passed to ``numpy.busdaycalendar``.
  3017. holidays : list
  3018. List/array of dates to exclude from the set of valid business days,
  3019. passed to ``numpy.busdaycalendar``.
  3020. calendar : np.busdaycalendar
  3021. offset : timedelta, default timedelta(0)
  3022. Examples
  3023. --------
  3024. >>> ts = pd.Timestamp(2022, 8, 5)
  3025. >>> ts + pd.offsets.CustomBusinessDay(1)
  3026. Timestamp('2022-08-08 00:00:00')
  3027. """
  3028. _prefix = "C"
  3029. _attributes = tuple(
  3030. ["n", "normalize", "weekmask", "holidays", "calendar", "offset"]
  3031. )
  3032. _apply_array = BaseOffset._apply_array
  3033. def __init__(
  3034. self,
  3035. n=1,
  3036. normalize=False,
  3037. weekmask="Mon Tue Wed Thu Fri",
  3038. holidays=None,
  3039. calendar=None,
  3040. offset=timedelta(0),
  3041. ):
  3042. BusinessDay.__init__(self, n, normalize, offset)
  3043. self._init_custom(weekmask, holidays, calendar)
  3044. cpdef __setstate__(self, state):
  3045. self.holidays = state.pop("holidays")
  3046. self.weekmask = state.pop("weekmask")
  3047. BusinessDay.__setstate__(self, state)
  3048. @apply_wraps
  3049. def _apply(self, other):
  3050. if self.n <= 0:
  3051. roll = "forward"
  3052. else:
  3053. roll = "backward"
  3054. if PyDateTime_Check(other):
  3055. date_in = other
  3056. np_dt = np.datetime64(date_in.date())
  3057. np_incr_dt = np.busday_offset(
  3058. np_dt, self.n, roll=roll, busdaycal=self.calendar
  3059. )
  3060. dt_date = np_incr_dt.astype(datetime)
  3061. result = datetime.combine(dt_date, date_in.time())
  3062. if self.offset:
  3063. result = result + self.offset
  3064. return result
  3065. elif is_any_td_scalar(other):
  3066. td = Timedelta(self.offset) + other
  3067. return BDay(self.n, offset=td.to_pytimedelta(), normalize=self.normalize)
  3068. else:
  3069. raise ApplyTypeError(
  3070. "Only know how to combine trading day with "
  3071. "datetime, datetime64 or timedelta."
  3072. )
  3073. def is_on_offset(self, dt: datetime) -> bool:
  3074. if self.normalize and not _is_normalized(dt):
  3075. return False
  3076. day64 = _to_dt64D(dt)
  3077. return np.is_busday(day64, busdaycal=self.calendar)
  3078. cdef class CustomBusinessHour(BusinessHour):
  3079. """
  3080. DateOffset subclass representing possibly n custom business days.
  3081. In CustomBusinessHour we can use custom weekmask, holidays, and calendar.
  3082. Parameters
  3083. ----------
  3084. n : int, default 1
  3085. The number of hours represented.
  3086. normalize : bool, default False
  3087. Normalize start/end dates to midnight before generating date range.
  3088. weekmask : str, Default 'Mon Tue Wed Thu Fri'
  3089. Weekmask of valid business days, passed to ``numpy.busdaycalendar``.
  3090. holidays : list
  3091. List/array of dates to exclude from the set of valid business days,
  3092. passed to ``numpy.busdaycalendar``.
  3093. calendar : np.busdaycalendar
  3094. Calendar to integrate.
  3095. start : str, time, or list of str/time, default "09:00"
  3096. Start time of your custom business hour in 24h format.
  3097. end : str, time, or list of str/time, default: "17:00"
  3098. End time of your custom business hour in 24h format.
  3099. Examples
  3100. --------
  3101. In the example below the default parameters give the next business hour.
  3102. >>> ts = pd.Timestamp(2022, 8, 5, 16)
  3103. >>> ts + pd.offsets.CustomBusinessHour()
  3104. Timestamp('2022-08-08 09:00:00')
  3105. We can also change the start and the end of business hours.
  3106. >>> ts = pd.Timestamp(2022, 8, 5, 16)
  3107. >>> ts + pd.offsets.CustomBusinessHour(start="11:00")
  3108. Timestamp('2022-08-08 11:00:00')
  3109. >>> from datetime import time as dt_time
  3110. >>> ts = pd.Timestamp(2022, 8, 5, 16)
  3111. >>> ts + pd.offsets.CustomBusinessHour(end=dt_time(19, 0))
  3112. Timestamp('2022-08-05 17:00:00')
  3113. >>> ts = pd.Timestamp(2022, 8, 5, 22)
  3114. >>> ts + pd.offsets.CustomBusinessHour(end=dt_time(19, 0))
  3115. Timestamp('2022-08-08 10:00:00')
  3116. You can divide your business day hours into several parts.
  3117. >>> import datetime as dt
  3118. >>> freq = pd.offsets.CustomBusinessHour(start=["06:00", "10:00", "15:00"],
  3119. ... end=["08:00", "12:00", "17:00"])
  3120. >>> pd.date_range(dt.datetime(2022, 12, 9), dt.datetime(2022, 12, 13), freq=freq)
  3121. DatetimeIndex(['2022-12-09 06:00:00', '2022-12-09 07:00:00',
  3122. '2022-12-09 10:00:00', '2022-12-09 11:00:00',
  3123. '2022-12-09 15:00:00', '2022-12-09 16:00:00',
  3124. '2022-12-12 06:00:00', '2022-12-12 07:00:00',
  3125. '2022-12-12 10:00:00', '2022-12-12 11:00:00',
  3126. '2022-12-12 15:00:00', '2022-12-12 16:00:00'],
  3127. dtype='datetime64[ns]', freq='CBH')
  3128. Business days can be specified by ``weekmask`` parameter. To convert
  3129. the returned datetime object to its string representation
  3130. the function strftime() is used in the next example.
  3131. >>> import datetime as dt
  3132. >>> freq = pd.offsets.CustomBusinessHour(weekmask="Mon Wed Fri",
  3133. ... start="10:00", end="13:00")
  3134. >>> pd.date_range(dt.datetime(2022, 12, 10), dt.datetime(2022, 12, 18),
  3135. ... freq=freq).strftime('%a %d %b %Y %H:%M')
  3136. Index(['Mon 12 Dec 2022 10:00', 'Mon 12 Dec 2022 11:00',
  3137. 'Mon 12 Dec 2022 12:00', 'Wed 14 Dec 2022 10:00',
  3138. 'Wed 14 Dec 2022 11:00', 'Wed 14 Dec 2022 12:00',
  3139. 'Fri 16 Dec 2022 10:00', 'Fri 16 Dec 2022 11:00',
  3140. 'Fri 16 Dec 2022 12:00'],
  3141. dtype='object')
  3142. Using NumPy business day calendar you can define custom holidays.
  3143. >>> import datetime as dt
  3144. >>> bdc = np.busdaycalendar(holidays=['2022-12-12', '2022-12-14'])
  3145. >>> freq = pd.offsets.CustomBusinessHour(calendar=bdc, start="10:00", end="13:00")
  3146. >>> pd.date_range(dt.datetime(2022, 12, 10), dt.datetime(2022, 12, 18), freq=freq)
  3147. DatetimeIndex(['2022-12-13 10:00:00', '2022-12-13 11:00:00',
  3148. '2022-12-13 12:00:00', '2022-12-15 10:00:00',
  3149. '2022-12-15 11:00:00', '2022-12-15 12:00:00',
  3150. '2022-12-16 10:00:00', '2022-12-16 11:00:00',
  3151. '2022-12-16 12:00:00'],
  3152. dtype='datetime64[ns]', freq='CBH')
  3153. """
  3154. _prefix = "CBH"
  3155. _anchor = 0
  3156. _attributes = tuple(
  3157. ["n", "normalize", "weekmask", "holidays", "calendar", "start", "end", "offset"]
  3158. )
  3159. def __init__(
  3160. self,
  3161. n=1,
  3162. normalize=False,
  3163. weekmask="Mon Tue Wed Thu Fri",
  3164. holidays=None,
  3165. calendar=None,
  3166. start="09:00",
  3167. end="17:00",
  3168. offset=timedelta(0),
  3169. ):
  3170. BusinessHour.__init__(self, n, normalize, start=start, end=end, offset=offset)
  3171. self._init_custom(weekmask, holidays, calendar)
  3172. cdef class _CustomBusinessMonth(BusinessMixin):
  3173. """
  3174. DateOffset subclass representing custom business month(s).
  3175. Increments between beginning/end of month dates.
  3176. Parameters
  3177. ----------
  3178. n : int, default 1
  3179. The number of months represented.
  3180. normalize : bool, default False
  3181. Normalize start/end dates to midnight before generating date range.
  3182. weekmask : str, Default 'Mon Tue Wed Thu Fri'
  3183. Weekmask of valid business days, passed to ``numpy.busdaycalendar``.
  3184. holidays : list
  3185. List/array of dates to exclude from the set of valid business days,
  3186. passed to ``numpy.busdaycalendar``.
  3187. calendar : np.busdaycalendar
  3188. Calendar to integrate.
  3189. offset : timedelta, default timedelta(0)
  3190. Time offset to apply.
  3191. """
  3192. _attributes = tuple(
  3193. ["n", "normalize", "weekmask", "holidays", "calendar", "offset"]
  3194. )
  3195. def __init__(
  3196. self,
  3197. n=1,
  3198. normalize=False,
  3199. weekmask="Mon Tue Wed Thu Fri",
  3200. holidays=None,
  3201. calendar=None,
  3202. offset=timedelta(0),
  3203. ):
  3204. BusinessMixin.__init__(self, n, normalize, offset)
  3205. self._init_custom(weekmask, holidays, calendar)
  3206. @cache_readonly
  3207. def cbday_roll(self):
  3208. """
  3209. Define default roll function to be called in apply method.
  3210. """
  3211. cbday_kwds = self.kwds.copy()
  3212. cbday_kwds["offset"] = timedelta(0)
  3213. cbday = CustomBusinessDay(n=1, normalize=False, **cbday_kwds)
  3214. if self._prefix.endswith("S"):
  3215. # MonthBegin
  3216. roll_func = cbday.rollforward
  3217. else:
  3218. # MonthEnd
  3219. roll_func = cbday.rollback
  3220. return roll_func
  3221. @cache_readonly
  3222. def m_offset(self):
  3223. if self._prefix.endswith("S"):
  3224. # MonthBegin
  3225. moff = MonthBegin(n=1, normalize=False)
  3226. else:
  3227. # MonthEnd
  3228. moff = MonthEnd(n=1, normalize=False)
  3229. return moff
  3230. @cache_readonly
  3231. def month_roll(self):
  3232. """
  3233. Define default roll function to be called in apply method.
  3234. """
  3235. if self._prefix.endswith("S"):
  3236. # MonthBegin
  3237. roll_func = self.m_offset.rollback
  3238. else:
  3239. # MonthEnd
  3240. roll_func = self.m_offset.rollforward
  3241. return roll_func
  3242. @apply_wraps
  3243. def _apply(self, other: datetime) -> datetime:
  3244. # First move to month offset
  3245. cur_month_offset_date = self.month_roll(other)
  3246. # Find this custom month offset
  3247. compare_date = self.cbday_roll(cur_month_offset_date)
  3248. n = roll_convention(other.day, self.n, compare_date.day)
  3249. new = cur_month_offset_date + n * self.m_offset
  3250. result = self.cbday_roll(new)
  3251. if self.offset:
  3252. result = result + self.offset
  3253. return result
  3254. cdef class CustomBusinessMonthEnd(_CustomBusinessMonth):
  3255. _prefix = "CBM"
  3256. cdef class CustomBusinessMonthBegin(_CustomBusinessMonth):
  3257. _prefix = "CBMS"
  3258. BDay = BusinessDay
  3259. BMonthEnd = BusinessMonthEnd
  3260. BMonthBegin = BusinessMonthBegin
  3261. CBMonthEnd = CustomBusinessMonthEnd
  3262. CBMonthBegin = CustomBusinessMonthBegin
  3263. CDay = CustomBusinessDay
  3264. # ----------------------------------------------------------------------
  3265. # to_offset helpers
  3266. prefix_mapping = {
  3267. offset._prefix: offset
  3268. for offset in [
  3269. YearBegin, # 'AS'
  3270. YearEnd, # 'A'
  3271. BYearBegin, # 'BAS'
  3272. BYearEnd, # 'BA'
  3273. BusinessDay, # 'B'
  3274. BusinessMonthBegin, # 'BMS'
  3275. BusinessMonthEnd, # 'BM'
  3276. BQuarterEnd, # 'BQ'
  3277. BQuarterBegin, # 'BQS'
  3278. BusinessHour, # 'BH'
  3279. CustomBusinessDay, # 'C'
  3280. CustomBusinessMonthEnd, # 'CBM'
  3281. CustomBusinessMonthBegin, # 'CBMS'
  3282. CustomBusinessHour, # 'CBH'
  3283. MonthEnd, # 'M'
  3284. MonthBegin, # 'MS'
  3285. Nano, # 'N'
  3286. SemiMonthEnd, # 'SM'
  3287. SemiMonthBegin, # 'SMS'
  3288. Week, # 'W'
  3289. Second, # 'S'
  3290. Minute, # 'T'
  3291. Micro, # 'U'
  3292. QuarterEnd, # 'Q'
  3293. QuarterBegin, # 'QS'
  3294. Milli, # 'L'
  3295. Hour, # 'H'
  3296. Day, # 'D'
  3297. WeekOfMonth, # 'WOM'
  3298. FY5253,
  3299. FY5253Quarter,
  3300. ]
  3301. }
  3302. # hack to handle WOM-1MON
  3303. opattern = re.compile(
  3304. r"([+\-]?\d*|[+\-]?\d*\.\d*)\s*([A-Za-z]+([\-][\dA-Za-z\-]+)?)"
  3305. )
  3306. _lite_rule_alias = {
  3307. "W": "W-SUN",
  3308. "Q": "Q-DEC",
  3309. "A": "A-DEC", # YearEnd(month=12),
  3310. "Y": "A-DEC",
  3311. "AS": "AS-JAN", # YearBegin(month=1),
  3312. "YS": "AS-JAN",
  3313. "BA": "BA-DEC", # BYearEnd(month=12),
  3314. "BY": "BA-DEC",
  3315. "BAS": "BAS-JAN", # BYearBegin(month=1),
  3316. "BYS": "BAS-JAN",
  3317. "Min": "T",
  3318. "min": "T",
  3319. "ms": "L",
  3320. "us": "U",
  3321. "ns": "N",
  3322. }
  3323. _dont_uppercase = {"MS", "ms"}
  3324. INVALID_FREQ_ERR_MSG = "Invalid frequency: {0}"
  3325. # TODO: still needed?
  3326. # cache of previously seen offsets
  3327. _offset_map = {}
  3328. # TODO: better name?
  3329. def _get_offset(name: str) -> BaseOffset:
  3330. """
  3331. Return DateOffset object associated with rule name.
  3332. Examples
  3333. --------
  3334. _get_offset('EOM') --> BMonthEnd(1)
  3335. """
  3336. if name not in _dont_uppercase:
  3337. name = name.upper()
  3338. name = _lite_rule_alias.get(name, name)
  3339. name = _lite_rule_alias.get(name.lower(), name)
  3340. else:
  3341. name = _lite_rule_alias.get(name, name)
  3342. if name not in _offset_map:
  3343. try:
  3344. split = name.split("-")
  3345. klass = prefix_mapping[split[0]]
  3346. # handles case where there's no suffix (and will TypeError if too
  3347. # many '-')
  3348. offset = klass._from_name(*split[1:])
  3349. except (ValueError, TypeError, KeyError) as err:
  3350. # bad prefix or suffix
  3351. raise ValueError(INVALID_FREQ_ERR_MSG.format(name)) from err
  3352. # cache
  3353. _offset_map[name] = offset
  3354. return _offset_map[name]
  3355. cpdef to_offset(freq):
  3356. """
  3357. Return DateOffset object from string or datetime.timedelta object.
  3358. Parameters
  3359. ----------
  3360. freq : str, datetime.timedelta, BaseOffset or None
  3361. Returns
  3362. -------
  3363. DateOffset or None
  3364. Raises
  3365. ------
  3366. ValueError
  3367. If freq is an invalid frequency
  3368. See Also
  3369. --------
  3370. BaseOffset : Standard kind of date increment used for a date range.
  3371. Examples
  3372. --------
  3373. >>> to_offset("5min")
  3374. <5 * Minutes>
  3375. >>> to_offset("1D1H")
  3376. <25 * Hours>
  3377. >>> to_offset("2W")
  3378. <2 * Weeks: weekday=6>
  3379. >>> to_offset("2B")
  3380. <2 * BusinessDays>
  3381. >>> to_offset(pd.Timedelta(days=1))
  3382. <Day>
  3383. >>> to_offset(Hour())
  3384. <Hour>
  3385. """
  3386. if freq is None:
  3387. return None
  3388. if isinstance(freq, BaseOffset):
  3389. return freq
  3390. if isinstance(freq, tuple):
  3391. raise TypeError(
  3392. f"to_offset does not support tuples {freq}, pass as a string instead"
  3393. )
  3394. elif PyDelta_Check(freq):
  3395. return delta_to_tick(freq)
  3396. elif isinstance(freq, str):
  3397. delta = None
  3398. stride_sign = None
  3399. try:
  3400. split = opattern.split(freq)
  3401. if split[-1] != "" and not split[-1].isspace():
  3402. # the last element must be blank
  3403. raise ValueError("last element must be blank")
  3404. tups = zip(split[0::4], split[1::4], split[2::4])
  3405. for n, (sep, stride, name) in enumerate(tups):
  3406. if sep != "" and not sep.isspace():
  3407. raise ValueError("separator must be spaces")
  3408. prefix = _lite_rule_alias.get(name) or name
  3409. if stride_sign is None:
  3410. stride_sign = -1 if stride.startswith("-") else 1
  3411. if not stride:
  3412. stride = 1
  3413. if prefix in {"D", "H", "T", "S", "L", "U", "N"}:
  3414. # For these prefixes, we have something like "3H" or
  3415. # "2.5T", so we can construct a Timedelta with the
  3416. # matching unit and get our offset from delta_to_tick
  3417. td = Timedelta(1, unit=prefix)
  3418. off = delta_to_tick(td)
  3419. offset = off * float(stride)
  3420. if n != 0:
  3421. # If n==0, then stride_sign is already incorporated
  3422. # into the offset
  3423. offset *= stride_sign
  3424. else:
  3425. stride = int(stride)
  3426. offset = _get_offset(name)
  3427. offset = offset * int(np.fabs(stride) * stride_sign)
  3428. if delta is None:
  3429. delta = offset
  3430. else:
  3431. delta = delta + offset
  3432. except (ValueError, TypeError) as err:
  3433. raise ValueError(INVALID_FREQ_ERR_MSG.format(freq)) from err
  3434. else:
  3435. delta = None
  3436. if delta is None:
  3437. raise ValueError(INVALID_FREQ_ERR_MSG.format(freq))
  3438. return delta
  3439. # ----------------------------------------------------------------------
  3440. # RelativeDelta Arithmetic
  3441. cdef datetime _shift_day(datetime other, int days):
  3442. """
  3443. Increment the datetime `other` by the given number of days, retaining
  3444. the time-portion of the datetime. For tz-naive datetimes this is
  3445. equivalent to adding a timedelta. For tz-aware datetimes it is similar to
  3446. dateutil's relativedelta.__add__, but handles pytz tzinfo objects.
  3447. Parameters
  3448. ----------
  3449. other : datetime or Timestamp
  3450. days : int
  3451. Returns
  3452. -------
  3453. shifted: datetime or Timestamp
  3454. """
  3455. if other.tzinfo is None:
  3456. return other + timedelta(days=days)
  3457. tz = other.tzinfo
  3458. naive = other.replace(tzinfo=None)
  3459. shifted = naive + timedelta(days=days)
  3460. return localize_pydatetime(shifted, tz)
  3461. cdef int year_add_months(npy_datetimestruct dts, int months) nogil:
  3462. """
  3463. New year number after shifting npy_datetimestruct number of months.
  3464. """
  3465. return dts.year + (dts.month + months - 1) // 12
  3466. cdef int month_add_months(npy_datetimestruct dts, int months) nogil:
  3467. """
  3468. New month number after shifting npy_datetimestruct
  3469. number of months.
  3470. """
  3471. cdef:
  3472. int new_month = (dts.month + months) % 12
  3473. return 12 if new_month == 0 else new_month
  3474. @cython.wraparound(False)
  3475. @cython.boundscheck(False)
  3476. cdef ndarray shift_quarters(
  3477. ndarray dtindex,
  3478. int quarters,
  3479. int q1start_month,
  3480. str day_opt,
  3481. int modby=3,
  3482. NPY_DATETIMEUNIT reso=NPY_DATETIMEUNIT.NPY_FR_ns,
  3483. ):
  3484. """
  3485. Given an int64 array representing nanosecond timestamps, shift all elements
  3486. by the specified number of quarters using DateOffset semantics.
  3487. Parameters
  3488. ----------
  3489. dtindex : int64_t[:] timestamps for input dates
  3490. quarters : int number of quarters to shift
  3491. q1start_month : int month in which Q1 begins by convention
  3492. day_opt : {'start', 'end', 'business_start', 'business_end'}
  3493. modby : int (3 for quarters, 12 for years)
  3494. reso : NPY_DATETIMEUNIT, default NPY_FR_ns
  3495. Returns
  3496. -------
  3497. out : ndarray[int64_t]
  3498. """
  3499. if day_opt not in ["start", "end", "business_start", "business_end"]:
  3500. raise ValueError("day must be None, 'start', 'end', "
  3501. "'business_start', or 'business_end'")
  3502. cdef:
  3503. Py_ssize_t count = dtindex.size
  3504. ndarray out = cnp.PyArray_EMPTY(dtindex.ndim, dtindex.shape, cnp.NPY_INT64, 0)
  3505. Py_ssize_t i
  3506. int64_t val, res_val
  3507. int months_since, n
  3508. npy_datetimestruct dts
  3509. cnp.broadcast mi = cnp.PyArray_MultiIterNew2(out, dtindex)
  3510. with nogil:
  3511. for i in range(count):
  3512. # Analogous to: val = dtindex[i]
  3513. val = (<int64_t*>cnp.PyArray_MultiIter_DATA(mi, 1))[0]
  3514. if val == NPY_NAT:
  3515. res_val = NPY_NAT
  3516. else:
  3517. pandas_datetime_to_datetimestruct(val, reso, &dts)
  3518. n = quarters
  3519. months_since = (dts.month - q1start_month) % modby
  3520. n = _roll_qtrday(&dts, n, months_since, day_opt)
  3521. dts.year = year_add_months(dts, modby * n - months_since)
  3522. dts.month = month_add_months(dts, modby * n - months_since)
  3523. dts.day = get_day_of_month(&dts, day_opt)
  3524. res_val = npy_datetimestruct_to_datetime(reso, &dts)
  3525. # Analogous to: out[i] = res_val
  3526. (<int64_t*>cnp.PyArray_MultiIter_DATA(mi, 0))[0] = res_val
  3527. cnp.PyArray_MultiIter_NEXT(mi)
  3528. return out
  3529. @cython.wraparound(False)
  3530. @cython.boundscheck(False)
  3531. def shift_months(
  3532. ndarray dtindex, # int64_t, arbitrary ndim
  3533. int months,
  3534. str day_opt=None,
  3535. NPY_DATETIMEUNIT reso=NPY_DATETIMEUNIT.NPY_FR_ns,
  3536. ):
  3537. """
  3538. Given an int64-based datetime index, shift all elements
  3539. specified number of months using DateOffset semantics
  3540. day_opt: {None, 'start', 'end', 'business_start', 'business_end'}
  3541. * None: day of month
  3542. * 'start' 1st day of month
  3543. * 'end' last day of month
  3544. """
  3545. cdef:
  3546. Py_ssize_t i
  3547. npy_datetimestruct dts
  3548. int count = dtindex.size
  3549. ndarray out = cnp.PyArray_EMPTY(dtindex.ndim, dtindex.shape, cnp.NPY_INT64, 0)
  3550. int months_to_roll
  3551. int64_t val, res_val
  3552. cnp.broadcast mi = cnp.PyArray_MultiIterNew2(out, dtindex)
  3553. if day_opt is not None and day_opt not in {
  3554. "start", "end", "business_start", "business_end"
  3555. }:
  3556. raise ValueError("day must be None, 'start', 'end', "
  3557. "'business_start', or 'business_end'")
  3558. if day_opt is None:
  3559. # TODO: can we combine this with the non-None case?
  3560. with nogil:
  3561. for i in range(count):
  3562. # Analogous to: val = i8other[i]
  3563. val = (<int64_t*>cnp.PyArray_MultiIter_DATA(mi, 1))[0]
  3564. if val == NPY_NAT:
  3565. res_val = NPY_NAT
  3566. else:
  3567. pandas_datetime_to_datetimestruct(val, reso, &dts)
  3568. dts.year = year_add_months(dts, months)
  3569. dts.month = month_add_months(dts, months)
  3570. dts.day = min(dts.day, get_days_in_month(dts.year, dts.month))
  3571. res_val = npy_datetimestruct_to_datetime(reso, &dts)
  3572. # Analogous to: out[i] = res_val
  3573. (<int64_t*>cnp.PyArray_MultiIter_DATA(mi, 0))[0] = res_val
  3574. cnp.PyArray_MultiIter_NEXT(mi)
  3575. else:
  3576. with nogil:
  3577. for i in range(count):
  3578. # Analogous to: val = i8other[i]
  3579. val = (<int64_t*>cnp.PyArray_MultiIter_DATA(mi, 1))[0]
  3580. if val == NPY_NAT:
  3581. res_val = NPY_NAT
  3582. else:
  3583. pandas_datetime_to_datetimestruct(val, reso, &dts)
  3584. months_to_roll = months
  3585. months_to_roll = _roll_qtrday(&dts, months_to_roll, 0, day_opt)
  3586. dts.year = year_add_months(dts, months_to_roll)
  3587. dts.month = month_add_months(dts, months_to_roll)
  3588. dts.day = get_day_of_month(&dts, day_opt)
  3589. res_val = npy_datetimestruct_to_datetime(reso, &dts)
  3590. # Analogous to: out[i] = res_val
  3591. (<int64_t*>cnp.PyArray_MultiIter_DATA(mi, 0))[0] = res_val
  3592. cnp.PyArray_MultiIter_NEXT(mi)
  3593. return out
  3594. def shift_month(stamp: datetime, months: int, day_opt: object = None) -> datetime:
  3595. """
  3596. Given a datetime (or Timestamp) `stamp`, an integer `months` and an
  3597. option `day_opt`, return a new datetimelike that many months later,
  3598. with day determined by `day_opt` using relativedelta semantics.
  3599. Scalar analogue of shift_months.
  3600. Parameters
  3601. ----------
  3602. stamp : datetime or Timestamp
  3603. months : int
  3604. day_opt : None, 'start', 'end', 'business_start', 'business_end', or int
  3605. None: returned datetimelike has the same day as the input, or the
  3606. last day of the month if the new month is too short
  3607. 'start': returned datetimelike has day=1
  3608. 'end': returned datetimelike has day on the last day of the month
  3609. 'business_start': returned datetimelike has day on the first
  3610. business day of the month
  3611. 'business_end': returned datetimelike has day on the last
  3612. business day of the month
  3613. int: returned datetimelike has day equal to day_opt
  3614. Returns
  3615. -------
  3616. shifted : datetime or Timestamp (same as input `stamp`)
  3617. """
  3618. cdef:
  3619. int year, month, day
  3620. int days_in_month, dy
  3621. dy = (stamp.month + months) // 12
  3622. month = (stamp.month + months) % 12
  3623. if month == 0:
  3624. month = 12
  3625. dy -= 1
  3626. year = stamp.year + dy
  3627. if day_opt is None:
  3628. days_in_month = get_days_in_month(year, month)
  3629. day = min(stamp.day, days_in_month)
  3630. elif day_opt == "start":
  3631. day = 1
  3632. elif day_opt == "end":
  3633. day = get_days_in_month(year, month)
  3634. elif day_opt == "business_start":
  3635. # first business day of month
  3636. day = get_firstbday(year, month)
  3637. elif day_opt == "business_end":
  3638. # last business day of month
  3639. day = get_lastbday(year, month)
  3640. elif is_integer_object(day_opt):
  3641. days_in_month = get_days_in_month(year, month)
  3642. day = min(day_opt, days_in_month)
  3643. else:
  3644. raise ValueError(day_opt)
  3645. return stamp.replace(year=year, month=month, day=day)
  3646. cdef int get_day_of_month(npy_datetimestruct* dts, str day_opt) nogil:
  3647. """
  3648. Find the day in `other`'s month that satisfies a DateOffset's is_on_offset
  3649. policy, as described by the `day_opt` argument.
  3650. Parameters
  3651. ----------
  3652. dts : npy_datetimestruct*
  3653. day_opt : {'start', 'end', 'business_start', 'business_end'}
  3654. 'start': returns 1
  3655. 'end': returns last day of the month
  3656. 'business_start': returns the first business day of the month
  3657. 'business_end': returns the last business day of the month
  3658. Returns
  3659. -------
  3660. day_of_month : int
  3661. Examples
  3662. -------
  3663. >>> other = datetime(2017, 11, 14)
  3664. >>> get_day_of_month(other, 'start')
  3665. 1
  3666. >>> get_day_of_month(other, 'end')
  3667. 30
  3668. Notes
  3669. -----
  3670. Caller is responsible for ensuring one of the four accepted day_opt values
  3671. is passed.
  3672. """
  3673. if day_opt == "start":
  3674. return 1
  3675. elif day_opt == "end":
  3676. return get_days_in_month(dts.year, dts.month)
  3677. elif day_opt == "business_start":
  3678. # first business day of month
  3679. return get_firstbday(dts.year, dts.month)
  3680. else:
  3681. # i.e. day_opt == "business_end":
  3682. # last business day of month
  3683. return get_lastbday(dts.year, dts.month)
  3684. cpdef int roll_convention(int other, int n, int compare) nogil:
  3685. """
  3686. Possibly increment or decrement the number of periods to shift
  3687. based on rollforward/rollbackward conventions.
  3688. Parameters
  3689. ----------
  3690. other : int, generally the day component of a datetime
  3691. n : number of periods to increment, before adjusting for rolling
  3692. compare : int, generally the day component of a datetime, in the same
  3693. month as the datetime form which `other` was taken.
  3694. Returns
  3695. -------
  3696. n : int number of periods to increment
  3697. """
  3698. if n > 0 and other < compare:
  3699. n -= 1
  3700. elif n <= 0 and other > compare:
  3701. # as if rolled forward already
  3702. n += 1
  3703. return n
  3704. def roll_qtrday(other: datetime, n: int, month: int,
  3705. day_opt: str, modby: int) -> int:
  3706. """
  3707. Possibly increment or decrement the number of periods to shift
  3708. based on rollforward/rollbackward conventions.
  3709. Parameters
  3710. ----------
  3711. other : datetime or Timestamp
  3712. n : number of periods to increment, before adjusting for rolling
  3713. month : int reference month giving the first month of the year
  3714. day_opt : {'start', 'end', 'business_start', 'business_end'}
  3715. The convention to use in finding the day in a given month against
  3716. which to compare for rollforward/rollbackward decisions.
  3717. modby : int 3 for quarters, 12 for years
  3718. Returns
  3719. -------
  3720. n : int number of periods to increment
  3721. See Also
  3722. --------
  3723. get_day_of_month : Find the day in a month provided an offset.
  3724. """
  3725. cdef:
  3726. int months_since
  3727. npy_datetimestruct dts
  3728. if day_opt not in ["start", "end", "business_start", "business_end"]:
  3729. raise ValueError(day_opt)
  3730. pydate_to_dtstruct(other, &dts)
  3731. if modby == 12:
  3732. # We care about the month-of-year, not month-of-quarter, so skip mod
  3733. months_since = other.month - month
  3734. else:
  3735. months_since = other.month % modby - month % modby
  3736. return _roll_qtrday(&dts, n, months_since, day_opt)
  3737. cdef int _roll_qtrday(npy_datetimestruct* dts,
  3738. int n,
  3739. int months_since,
  3740. str day_opt) except? -1 nogil:
  3741. """
  3742. See roll_qtrday.__doc__
  3743. """
  3744. if n > 0:
  3745. if months_since < 0 or (months_since == 0 and
  3746. dts.day < get_day_of_month(dts, day_opt)):
  3747. # pretend to roll back if on same month but
  3748. # before compare_day
  3749. n -= 1
  3750. else:
  3751. if months_since > 0 or (months_since == 0 and
  3752. dts.day > get_day_of_month(dts, day_opt)):
  3753. # make sure to roll forward, so negate
  3754. n += 1
  3755. return n