test_offsets.py 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074
  1. """
  2. Tests of pandas.tseries.offsets
  3. """
  4. from __future__ import annotations
  5. from datetime import (
  6. datetime,
  7. timedelta,
  8. )
  9. from typing import (
  10. Dict,
  11. List,
  12. Tuple,
  13. )
  14. import numpy as np
  15. import pytest
  16. from pandas._libs.tslibs import (
  17. NaT,
  18. Timedelta,
  19. Timestamp,
  20. conversion,
  21. timezones,
  22. )
  23. import pandas._libs.tslibs.offsets as liboffsets
  24. from pandas._libs.tslibs.offsets import (
  25. _get_offset,
  26. _offset_map,
  27. )
  28. from pandas._libs.tslibs.period import INVALID_FREQ_ERR_MSG
  29. from pandas.errors import PerformanceWarning
  30. from pandas import (
  31. DatetimeIndex,
  32. Series,
  33. date_range,
  34. )
  35. import pandas._testing as tm
  36. from pandas.tests.tseries.offsets.common import WeekDay
  37. from pandas.tseries import offsets
  38. from pandas.tseries.offsets import (
  39. FY5253,
  40. BaseOffset,
  41. BDay,
  42. BMonthEnd,
  43. BusinessHour,
  44. CustomBusinessDay,
  45. CustomBusinessHour,
  46. CustomBusinessMonthBegin,
  47. CustomBusinessMonthEnd,
  48. DateOffset,
  49. Easter,
  50. FY5253Quarter,
  51. LastWeekOfMonth,
  52. MonthBegin,
  53. Nano,
  54. Tick,
  55. Week,
  56. WeekOfMonth,
  57. )
  58. _ApplyCases = List[Tuple[BaseOffset, Dict[datetime, datetime]]]
  59. _ARITHMETIC_DATE_OFFSET = [
  60. "years",
  61. "months",
  62. "weeks",
  63. "days",
  64. "hours",
  65. "minutes",
  66. "seconds",
  67. "milliseconds",
  68. "microseconds",
  69. ]
  70. def _create_offset(klass, value=1, normalize=False):
  71. # create instance from offset class
  72. if klass is FY5253:
  73. klass = klass(
  74. n=value,
  75. startingMonth=1,
  76. weekday=1,
  77. variation="last",
  78. normalize=normalize,
  79. )
  80. elif klass is FY5253Quarter:
  81. klass = klass(
  82. n=value,
  83. startingMonth=1,
  84. weekday=1,
  85. qtr_with_extra_week=1,
  86. variation="last",
  87. normalize=normalize,
  88. )
  89. elif klass is LastWeekOfMonth:
  90. klass = klass(n=value, weekday=5, normalize=normalize)
  91. elif klass is WeekOfMonth:
  92. klass = klass(n=value, week=1, weekday=5, normalize=normalize)
  93. elif klass is Week:
  94. klass = klass(n=value, weekday=5, normalize=normalize)
  95. elif klass is DateOffset:
  96. klass = klass(days=value, normalize=normalize)
  97. else:
  98. klass = klass(value, normalize=normalize)
  99. return klass
  100. @pytest.fixture
  101. def dt():
  102. return Timestamp(datetime(2008, 1, 2))
  103. @pytest.fixture
  104. def expecteds():
  105. # executed value created by _create_offset
  106. # are applied to 2011/01/01 09:00 (Saturday)
  107. # used for .apply and .rollforward
  108. return {
  109. "Day": Timestamp("2011-01-02 09:00:00"),
  110. "DateOffset": Timestamp("2011-01-02 09:00:00"),
  111. "BusinessDay": Timestamp("2011-01-03 09:00:00"),
  112. "CustomBusinessDay": Timestamp("2011-01-03 09:00:00"),
  113. "CustomBusinessMonthEnd": Timestamp("2011-01-31 09:00:00"),
  114. "CustomBusinessMonthBegin": Timestamp("2011-01-03 09:00:00"),
  115. "MonthBegin": Timestamp("2011-02-01 09:00:00"),
  116. "BusinessMonthBegin": Timestamp("2011-01-03 09:00:00"),
  117. "MonthEnd": Timestamp("2011-01-31 09:00:00"),
  118. "SemiMonthEnd": Timestamp("2011-01-15 09:00:00"),
  119. "SemiMonthBegin": Timestamp("2011-01-15 09:00:00"),
  120. "BusinessMonthEnd": Timestamp("2011-01-31 09:00:00"),
  121. "YearBegin": Timestamp("2012-01-01 09:00:00"),
  122. "BYearBegin": Timestamp("2011-01-03 09:00:00"),
  123. "YearEnd": Timestamp("2011-12-31 09:00:00"),
  124. "BYearEnd": Timestamp("2011-12-30 09:00:00"),
  125. "QuarterBegin": Timestamp("2011-03-01 09:00:00"),
  126. "BQuarterBegin": Timestamp("2011-03-01 09:00:00"),
  127. "QuarterEnd": Timestamp("2011-03-31 09:00:00"),
  128. "BQuarterEnd": Timestamp("2011-03-31 09:00:00"),
  129. "BusinessHour": Timestamp("2011-01-03 10:00:00"),
  130. "CustomBusinessHour": Timestamp("2011-01-03 10:00:00"),
  131. "WeekOfMonth": Timestamp("2011-01-08 09:00:00"),
  132. "LastWeekOfMonth": Timestamp("2011-01-29 09:00:00"),
  133. "FY5253Quarter": Timestamp("2011-01-25 09:00:00"),
  134. "FY5253": Timestamp("2011-01-25 09:00:00"),
  135. "Week": Timestamp("2011-01-08 09:00:00"),
  136. "Easter": Timestamp("2011-04-24 09:00:00"),
  137. "Hour": Timestamp("2011-01-01 10:00:00"),
  138. "Minute": Timestamp("2011-01-01 09:01:00"),
  139. "Second": Timestamp("2011-01-01 09:00:01"),
  140. "Milli": Timestamp("2011-01-01 09:00:00.001000"),
  141. "Micro": Timestamp("2011-01-01 09:00:00.000001"),
  142. "Nano": Timestamp("2011-01-01T09:00:00.000000001"),
  143. }
  144. class TestCommon:
  145. def test_immutable(self, offset_types):
  146. # GH#21341 check that __setattr__ raises
  147. offset = _create_offset(offset_types)
  148. msg = "objects is not writable|DateOffset objects are immutable"
  149. with pytest.raises(AttributeError, match=msg):
  150. offset.normalize = True
  151. with pytest.raises(AttributeError, match=msg):
  152. offset.n = 91
  153. def test_return_type(self, offset_types):
  154. offset = _create_offset(offset_types)
  155. # make sure that we are returning a Timestamp
  156. result = Timestamp("20080101") + offset
  157. assert isinstance(result, Timestamp)
  158. # make sure that we are returning NaT
  159. assert NaT + offset is NaT
  160. assert offset + NaT is NaT
  161. assert NaT - offset is NaT
  162. assert (-offset)._apply(NaT) is NaT
  163. def test_offset_n(self, offset_types):
  164. offset = _create_offset(offset_types)
  165. assert offset.n == 1
  166. neg_offset = offset * -1
  167. assert neg_offset.n == -1
  168. mul_offset = offset * 3
  169. assert mul_offset.n == 3
  170. def test_offset_timedelta64_arg(self, offset_types):
  171. # check that offset._validate_n raises TypeError on a timedelt64
  172. # object
  173. off = _create_offset(offset_types)
  174. td64 = np.timedelta64(4567, "s")
  175. with pytest.raises(TypeError, match="argument must be an integer"):
  176. type(off)(n=td64, **off.kwds)
  177. def test_offset_mul_ndarray(self, offset_types):
  178. off = _create_offset(offset_types)
  179. expected = np.array([[off, off * 2], [off * 3, off * 4]])
  180. result = np.array([[1, 2], [3, 4]]) * off
  181. tm.assert_numpy_array_equal(result, expected)
  182. result = off * np.array([[1, 2], [3, 4]])
  183. tm.assert_numpy_array_equal(result, expected)
  184. def test_offset_freqstr(self, offset_types):
  185. offset = _create_offset(offset_types)
  186. freqstr = offset.freqstr
  187. if freqstr not in ("<Easter>", "<DateOffset: days=1>", "LWOM-SAT"):
  188. code = _get_offset(freqstr)
  189. assert offset.rule_code == code
  190. def _check_offsetfunc_works(self, offset, funcname, dt, expected, normalize=False):
  191. if normalize and issubclass(offset, Tick):
  192. # normalize=True disallowed for Tick subclasses GH#21427
  193. return
  194. offset_s = _create_offset(offset, normalize=normalize)
  195. func = getattr(offset_s, funcname)
  196. result = func(dt)
  197. assert isinstance(result, Timestamp)
  198. assert result == expected
  199. result = func(Timestamp(dt))
  200. assert isinstance(result, Timestamp)
  201. assert result == expected
  202. # see gh-14101
  203. exp_warning = None
  204. ts = Timestamp(dt) + Nano(5)
  205. if (
  206. type(offset_s).__name__ == "DateOffset"
  207. and (funcname in ["apply", "_apply"] or normalize)
  208. and ts.nanosecond > 0
  209. ):
  210. exp_warning = UserWarning
  211. # test nanosecond is preserved
  212. with tm.assert_produces_warning(exp_warning):
  213. result = func(ts)
  214. assert isinstance(result, Timestamp)
  215. if normalize is False:
  216. assert result == expected + Nano(5)
  217. else:
  218. assert result == expected
  219. if isinstance(dt, np.datetime64):
  220. # test tz when input is datetime or Timestamp
  221. return
  222. for tz in [
  223. None,
  224. "UTC",
  225. "Asia/Tokyo",
  226. "US/Eastern",
  227. "dateutil/Asia/Tokyo",
  228. "dateutil/US/Pacific",
  229. ]:
  230. expected_localize = expected.tz_localize(tz)
  231. tz_obj = timezones.maybe_get_tz(tz)
  232. dt_tz = conversion.localize_pydatetime(dt, tz_obj)
  233. result = func(dt_tz)
  234. assert isinstance(result, Timestamp)
  235. assert result == expected_localize
  236. result = func(Timestamp(dt, tz=tz))
  237. assert isinstance(result, Timestamp)
  238. assert result == expected_localize
  239. # see gh-14101
  240. exp_warning = None
  241. ts = Timestamp(dt, tz=tz) + Nano(5)
  242. if (
  243. type(offset_s).__name__ == "DateOffset"
  244. and (funcname in ["apply", "_apply"] or normalize)
  245. and ts.nanosecond > 0
  246. ):
  247. exp_warning = UserWarning
  248. # test nanosecond is preserved
  249. with tm.assert_produces_warning(exp_warning):
  250. result = func(ts)
  251. assert isinstance(result, Timestamp)
  252. if normalize is False:
  253. assert result == expected_localize + Nano(5)
  254. else:
  255. assert result == expected_localize
  256. def test_apply(self, offset_types, expecteds):
  257. sdt = datetime(2011, 1, 1, 9, 0)
  258. ndt = np.datetime64("2011-01-01 09:00")
  259. expected = expecteds[offset_types.__name__]
  260. expected_norm = Timestamp(expected.date())
  261. for dt in [sdt, ndt]:
  262. self._check_offsetfunc_works(offset_types, "_apply", dt, expected)
  263. self._check_offsetfunc_works(
  264. offset_types, "_apply", dt, expected_norm, normalize=True
  265. )
  266. def test_rollforward(self, offset_types, expecteds):
  267. expecteds = expecteds.copy()
  268. # result will not be changed if the target is on the offset
  269. no_changes = [
  270. "Day",
  271. "MonthBegin",
  272. "SemiMonthBegin",
  273. "YearBegin",
  274. "Week",
  275. "Hour",
  276. "Minute",
  277. "Second",
  278. "Milli",
  279. "Micro",
  280. "Nano",
  281. "DateOffset",
  282. ]
  283. for n in no_changes:
  284. expecteds[n] = Timestamp("2011/01/01 09:00")
  285. expecteds["BusinessHour"] = Timestamp("2011-01-03 09:00:00")
  286. expecteds["CustomBusinessHour"] = Timestamp("2011-01-03 09:00:00")
  287. # but be changed when normalize=True
  288. norm_expected = expecteds.copy()
  289. for k in norm_expected:
  290. norm_expected[k] = Timestamp(norm_expected[k].date())
  291. normalized = {
  292. "Day": Timestamp("2011-01-02 00:00:00"),
  293. "DateOffset": Timestamp("2011-01-02 00:00:00"),
  294. "MonthBegin": Timestamp("2011-02-01 00:00:00"),
  295. "SemiMonthBegin": Timestamp("2011-01-15 00:00:00"),
  296. "YearBegin": Timestamp("2012-01-01 00:00:00"),
  297. "Week": Timestamp("2011-01-08 00:00:00"),
  298. "Hour": Timestamp("2011-01-01 00:00:00"),
  299. "Minute": Timestamp("2011-01-01 00:00:00"),
  300. "Second": Timestamp("2011-01-01 00:00:00"),
  301. "Milli": Timestamp("2011-01-01 00:00:00"),
  302. "Micro": Timestamp("2011-01-01 00:00:00"),
  303. }
  304. norm_expected.update(normalized)
  305. sdt = datetime(2011, 1, 1, 9, 0)
  306. ndt = np.datetime64("2011-01-01 09:00")
  307. for dt in [sdt, ndt]:
  308. expected = expecteds[offset_types.__name__]
  309. self._check_offsetfunc_works(offset_types, "rollforward", dt, expected)
  310. expected = norm_expected[offset_types.__name__]
  311. self._check_offsetfunc_works(
  312. offset_types, "rollforward", dt, expected, normalize=True
  313. )
  314. def test_rollback(self, offset_types):
  315. expecteds = {
  316. "BusinessDay": Timestamp("2010-12-31 09:00:00"),
  317. "CustomBusinessDay": Timestamp("2010-12-31 09:00:00"),
  318. "CustomBusinessMonthEnd": Timestamp("2010-12-31 09:00:00"),
  319. "CustomBusinessMonthBegin": Timestamp("2010-12-01 09:00:00"),
  320. "BusinessMonthBegin": Timestamp("2010-12-01 09:00:00"),
  321. "MonthEnd": Timestamp("2010-12-31 09:00:00"),
  322. "SemiMonthEnd": Timestamp("2010-12-31 09:00:00"),
  323. "BusinessMonthEnd": Timestamp("2010-12-31 09:00:00"),
  324. "BYearBegin": Timestamp("2010-01-01 09:00:00"),
  325. "YearEnd": Timestamp("2010-12-31 09:00:00"),
  326. "BYearEnd": Timestamp("2010-12-31 09:00:00"),
  327. "QuarterBegin": Timestamp("2010-12-01 09:00:00"),
  328. "BQuarterBegin": Timestamp("2010-12-01 09:00:00"),
  329. "QuarterEnd": Timestamp("2010-12-31 09:00:00"),
  330. "BQuarterEnd": Timestamp("2010-12-31 09:00:00"),
  331. "BusinessHour": Timestamp("2010-12-31 17:00:00"),
  332. "CustomBusinessHour": Timestamp("2010-12-31 17:00:00"),
  333. "WeekOfMonth": Timestamp("2010-12-11 09:00:00"),
  334. "LastWeekOfMonth": Timestamp("2010-12-25 09:00:00"),
  335. "FY5253Quarter": Timestamp("2010-10-26 09:00:00"),
  336. "FY5253": Timestamp("2010-01-26 09:00:00"),
  337. "Easter": Timestamp("2010-04-04 09:00:00"),
  338. }
  339. # result will not be changed if the target is on the offset
  340. for n in [
  341. "Day",
  342. "MonthBegin",
  343. "SemiMonthBegin",
  344. "YearBegin",
  345. "Week",
  346. "Hour",
  347. "Minute",
  348. "Second",
  349. "Milli",
  350. "Micro",
  351. "Nano",
  352. "DateOffset",
  353. ]:
  354. expecteds[n] = Timestamp("2011/01/01 09:00")
  355. # but be changed when normalize=True
  356. norm_expected = expecteds.copy()
  357. for k in norm_expected:
  358. norm_expected[k] = Timestamp(norm_expected[k].date())
  359. normalized = {
  360. "Day": Timestamp("2010-12-31 00:00:00"),
  361. "DateOffset": Timestamp("2010-12-31 00:00:00"),
  362. "MonthBegin": Timestamp("2010-12-01 00:00:00"),
  363. "SemiMonthBegin": Timestamp("2010-12-15 00:00:00"),
  364. "YearBegin": Timestamp("2010-01-01 00:00:00"),
  365. "Week": Timestamp("2010-12-25 00:00:00"),
  366. "Hour": Timestamp("2011-01-01 00:00:00"),
  367. "Minute": Timestamp("2011-01-01 00:00:00"),
  368. "Second": Timestamp("2011-01-01 00:00:00"),
  369. "Milli": Timestamp("2011-01-01 00:00:00"),
  370. "Micro": Timestamp("2011-01-01 00:00:00"),
  371. }
  372. norm_expected.update(normalized)
  373. sdt = datetime(2011, 1, 1, 9, 0)
  374. ndt = np.datetime64("2011-01-01 09:00")
  375. for dt in [sdt, ndt]:
  376. expected = expecteds[offset_types.__name__]
  377. self._check_offsetfunc_works(offset_types, "rollback", dt, expected)
  378. expected = norm_expected[offset_types.__name__]
  379. self._check_offsetfunc_works(
  380. offset_types, "rollback", dt, expected, normalize=True
  381. )
  382. def test_is_on_offset(self, offset_types, expecteds):
  383. dt = expecteds[offset_types.__name__]
  384. offset_s = _create_offset(offset_types)
  385. assert offset_s.is_on_offset(dt)
  386. # when normalize=True, is_on_offset checks time is 00:00:00
  387. if issubclass(offset_types, Tick):
  388. # normalize=True disallowed for Tick subclasses GH#21427
  389. return
  390. offset_n = _create_offset(offset_types, normalize=True)
  391. assert not offset_n.is_on_offset(dt)
  392. if offset_types in (BusinessHour, CustomBusinessHour):
  393. # In default BusinessHour (9:00-17:00), normalized time
  394. # cannot be in business hour range
  395. return
  396. date = datetime(dt.year, dt.month, dt.day)
  397. assert offset_n.is_on_offset(date)
  398. def test_add(self, offset_types, tz_naive_fixture, expecteds):
  399. tz = tz_naive_fixture
  400. dt = datetime(2011, 1, 1, 9, 0)
  401. offset_s = _create_offset(offset_types)
  402. expected = expecteds[offset_types.__name__]
  403. result_dt = dt + offset_s
  404. result_ts = Timestamp(dt) + offset_s
  405. for result in [result_dt, result_ts]:
  406. assert isinstance(result, Timestamp)
  407. assert result == expected
  408. expected_localize = expected.tz_localize(tz)
  409. result = Timestamp(dt, tz=tz) + offset_s
  410. assert isinstance(result, Timestamp)
  411. assert result == expected_localize
  412. # normalize=True, disallowed for Tick subclasses GH#21427
  413. if issubclass(offset_types, Tick):
  414. return
  415. offset_s = _create_offset(offset_types, normalize=True)
  416. expected = Timestamp(expected.date())
  417. result_dt = dt + offset_s
  418. result_ts = Timestamp(dt) + offset_s
  419. for result in [result_dt, result_ts]:
  420. assert isinstance(result, Timestamp)
  421. assert result == expected
  422. expected_localize = expected.tz_localize(tz)
  423. result = Timestamp(dt, tz=tz) + offset_s
  424. assert isinstance(result, Timestamp)
  425. assert result == expected_localize
  426. def test_add_empty_datetimeindex(self, offset_types, tz_naive_fixture):
  427. # GH#12724, GH#30336
  428. offset_s = _create_offset(offset_types)
  429. dti = DatetimeIndex([], tz=tz_naive_fixture)
  430. warn = None
  431. if isinstance(
  432. offset_s,
  433. (
  434. Easter,
  435. WeekOfMonth,
  436. LastWeekOfMonth,
  437. CustomBusinessDay,
  438. BusinessHour,
  439. CustomBusinessHour,
  440. CustomBusinessMonthBegin,
  441. CustomBusinessMonthEnd,
  442. FY5253,
  443. FY5253Quarter,
  444. ),
  445. ):
  446. # We don't have an optimized apply_index
  447. warn = PerformanceWarning
  448. with tm.assert_produces_warning(warn):
  449. result = dti + offset_s
  450. tm.assert_index_equal(result, dti)
  451. with tm.assert_produces_warning(warn):
  452. result = offset_s + dti
  453. tm.assert_index_equal(result, dti)
  454. dta = dti._data
  455. with tm.assert_produces_warning(warn):
  456. result = dta + offset_s
  457. tm.assert_equal(result, dta)
  458. with tm.assert_produces_warning(warn):
  459. result = offset_s + dta
  460. tm.assert_equal(result, dta)
  461. def test_pickle_roundtrip(self, offset_types):
  462. off = _create_offset(offset_types)
  463. res = tm.round_trip_pickle(off)
  464. assert off == res
  465. if type(off) is not DateOffset:
  466. for attr in off._attributes:
  467. if attr == "calendar":
  468. # np.busdaycalendar __eq__ will return False;
  469. # we check holidays and weekmask attrs so are OK
  470. continue
  471. # Make sure nothings got lost from _params (which __eq__) is based on
  472. assert getattr(off, attr) == getattr(res, attr)
  473. def test_pickle_dateoffset_odd_inputs(self):
  474. # GH#34511
  475. off = DateOffset(months=12)
  476. res = tm.round_trip_pickle(off)
  477. assert off == res
  478. base_dt = datetime(2020, 1, 1)
  479. assert base_dt + off == base_dt + res
  480. def test_offsets_hashable(self, offset_types):
  481. # GH: 37267
  482. off = _create_offset(offset_types)
  483. assert hash(off) is not None
  484. @pytest.mark.filterwarnings(
  485. "ignore:Non-vectorized DateOffset being applied to Series or DatetimeIndex"
  486. )
  487. @pytest.mark.parametrize("unit", ["s", "ms", "us"])
  488. def test_add_dt64_ndarray_non_nano(self, offset_types, unit, request):
  489. # check that the result with non-nano matches nano
  490. off = _create_offset(offset_types)
  491. dti = date_range("2016-01-01", periods=35, freq="D")
  492. arr = dti._data._ndarray.astype(f"M8[{unit}]")
  493. dta = type(dti._data)._simple_new(arr, dtype=arr.dtype)
  494. expected = dti._data + off
  495. result = dta + off
  496. exp_unit = unit
  497. if isinstance(off, Tick) and off._creso > dta._creso:
  498. # cast to higher reso like we would with Timedelta scalar
  499. exp_unit = Timedelta(off).unit
  500. expected = expected.as_unit(exp_unit)
  501. tm.assert_numpy_array_equal(result._ndarray, expected._ndarray)
  502. class TestDateOffset:
  503. def setup_method(self):
  504. _offset_map.clear()
  505. def test_repr(self):
  506. repr(DateOffset())
  507. repr(DateOffset(2))
  508. repr(2 * DateOffset())
  509. repr(2 * DateOffset(months=2))
  510. def test_mul(self):
  511. assert DateOffset(2) == 2 * DateOffset(1)
  512. assert DateOffset(2) == DateOffset(1) * 2
  513. @pytest.mark.parametrize("kwd", sorted(liboffsets._relativedelta_kwds))
  514. def test_constructor(self, kwd, request):
  515. if kwd == "millisecond":
  516. request.node.add_marker(
  517. pytest.mark.xfail(
  518. raises=NotImplementedError,
  519. reason="Constructing DateOffset object with `millisecond` is not "
  520. "yet supported.",
  521. )
  522. )
  523. offset = DateOffset(**{kwd: 2})
  524. assert offset.kwds == {kwd: 2}
  525. assert getattr(offset, kwd) == 2
  526. def test_default_constructor(self, dt):
  527. assert (dt + DateOffset(2)) == datetime(2008, 1, 4)
  528. def test_is_anchored(self):
  529. assert not DateOffset(2).is_anchored()
  530. assert DateOffset(1).is_anchored()
  531. def test_copy(self):
  532. assert DateOffset(months=2).copy() == DateOffset(months=2)
  533. assert DateOffset(milliseconds=1).copy() == DateOffset(milliseconds=1)
  534. @pytest.mark.parametrize(
  535. "arithmatic_offset_type, expected",
  536. zip(
  537. _ARITHMETIC_DATE_OFFSET,
  538. [
  539. "2009-01-02",
  540. "2008-02-02",
  541. "2008-01-09",
  542. "2008-01-03",
  543. "2008-01-02 01:00:00",
  544. "2008-01-02 00:01:00",
  545. "2008-01-02 00:00:01",
  546. "2008-01-02 00:00:00.001000000",
  547. "2008-01-02 00:00:00.000001000",
  548. ],
  549. ),
  550. )
  551. def test_add(self, arithmatic_offset_type, expected, dt):
  552. assert DateOffset(**{arithmatic_offset_type: 1}) + dt == Timestamp(expected)
  553. assert dt + DateOffset(**{arithmatic_offset_type: 1}) == Timestamp(expected)
  554. @pytest.mark.parametrize(
  555. "arithmatic_offset_type, expected",
  556. zip(
  557. _ARITHMETIC_DATE_OFFSET,
  558. [
  559. "2007-01-02",
  560. "2007-12-02",
  561. "2007-12-26",
  562. "2008-01-01",
  563. "2008-01-01 23:00:00",
  564. "2008-01-01 23:59:00",
  565. "2008-01-01 23:59:59",
  566. "2008-01-01 23:59:59.999000000",
  567. "2008-01-01 23:59:59.999999000",
  568. ],
  569. ),
  570. )
  571. def test_sub(self, arithmatic_offset_type, expected, dt):
  572. assert dt - DateOffset(**{arithmatic_offset_type: 1}) == Timestamp(expected)
  573. with pytest.raises(TypeError, match="Cannot subtract datetime from offset"):
  574. DateOffset(**{arithmatic_offset_type: 1}) - dt
  575. @pytest.mark.parametrize(
  576. "arithmatic_offset_type, n, expected",
  577. zip(
  578. _ARITHMETIC_DATE_OFFSET,
  579. range(1, 10),
  580. [
  581. "2009-01-02",
  582. "2008-03-02",
  583. "2008-01-23",
  584. "2008-01-06",
  585. "2008-01-02 05:00:00",
  586. "2008-01-02 00:06:00",
  587. "2008-01-02 00:00:07",
  588. "2008-01-02 00:00:00.008000000",
  589. "2008-01-02 00:00:00.000009000",
  590. ],
  591. ),
  592. )
  593. def test_mul_add(self, arithmatic_offset_type, n, expected, dt):
  594. assert DateOffset(**{arithmatic_offset_type: 1}) * n + dt == Timestamp(expected)
  595. assert n * DateOffset(**{arithmatic_offset_type: 1}) + dt == Timestamp(expected)
  596. assert dt + DateOffset(**{arithmatic_offset_type: 1}) * n == Timestamp(expected)
  597. assert dt + n * DateOffset(**{arithmatic_offset_type: 1}) == Timestamp(expected)
  598. @pytest.mark.parametrize(
  599. "arithmatic_offset_type, n, expected",
  600. zip(
  601. _ARITHMETIC_DATE_OFFSET,
  602. range(1, 10),
  603. [
  604. "2007-01-02",
  605. "2007-11-02",
  606. "2007-12-12",
  607. "2007-12-29",
  608. "2008-01-01 19:00:00",
  609. "2008-01-01 23:54:00",
  610. "2008-01-01 23:59:53",
  611. "2008-01-01 23:59:59.992000000",
  612. "2008-01-01 23:59:59.999991000",
  613. ],
  614. ),
  615. )
  616. def test_mul_sub(self, arithmatic_offset_type, n, expected, dt):
  617. assert dt - DateOffset(**{arithmatic_offset_type: 1}) * n == Timestamp(expected)
  618. assert dt - n * DateOffset(**{arithmatic_offset_type: 1}) == Timestamp(expected)
  619. def test_leap_year(self):
  620. d = datetime(2008, 1, 31)
  621. assert (d + DateOffset(months=1)) == datetime(2008, 2, 29)
  622. def test_eq(self):
  623. offset1 = DateOffset(days=1)
  624. offset2 = DateOffset(days=365)
  625. assert offset1 != offset2
  626. assert DateOffset(milliseconds=3) != DateOffset(milliseconds=7)
  627. @pytest.mark.parametrize(
  628. "offset_kwargs, expected_arg",
  629. [
  630. ({"microseconds": 1, "milliseconds": 1}, "2022-01-01 00:00:00.001001"),
  631. ({"seconds": 1, "milliseconds": 1}, "2022-01-01 00:00:01.001"),
  632. ({"minutes": 1, "milliseconds": 1}, "2022-01-01 00:01:00.001"),
  633. ({"hours": 1, "milliseconds": 1}, "2022-01-01 01:00:00.001"),
  634. ({"days": 1, "milliseconds": 1}, "2022-01-02 00:00:00.001"),
  635. ({"weeks": 1, "milliseconds": 1}, "2022-01-08 00:00:00.001"),
  636. ({"months": 1, "milliseconds": 1}, "2022-02-01 00:00:00.001"),
  637. ({"years": 1, "milliseconds": 1}, "2023-01-01 00:00:00.001"),
  638. ],
  639. )
  640. def test_milliseconds_combination(self, offset_kwargs, expected_arg):
  641. # GH 49897
  642. offset = DateOffset(**offset_kwargs)
  643. ts = Timestamp("2022-01-01")
  644. result = ts + offset
  645. expected = Timestamp(expected_arg)
  646. assert result == expected
  647. def test_offset_invalid_arguments(self):
  648. msg = "^Invalid argument/s or bad combination of arguments"
  649. with pytest.raises(ValueError, match=msg):
  650. DateOffset(picoseconds=1)
  651. class TestOffsetNames:
  652. def test_get_offset_name(self):
  653. assert BDay().freqstr == "B"
  654. assert BDay(2).freqstr == "2B"
  655. assert BMonthEnd().freqstr == "BM"
  656. assert Week(weekday=0).freqstr == "W-MON"
  657. assert Week(weekday=1).freqstr == "W-TUE"
  658. assert Week(weekday=2).freqstr == "W-WED"
  659. assert Week(weekday=3).freqstr == "W-THU"
  660. assert Week(weekday=4).freqstr == "W-FRI"
  661. assert LastWeekOfMonth(weekday=WeekDay.SUN).freqstr == "LWOM-SUN"
  662. def test_get_offset():
  663. with pytest.raises(ValueError, match=INVALID_FREQ_ERR_MSG):
  664. _get_offset("gibberish")
  665. with pytest.raises(ValueError, match=INVALID_FREQ_ERR_MSG):
  666. _get_offset("QS-JAN-B")
  667. pairs = [
  668. ("B", BDay()),
  669. ("b", BDay()),
  670. ("bm", BMonthEnd()),
  671. ("Bm", BMonthEnd()),
  672. ("W-MON", Week(weekday=0)),
  673. ("W-TUE", Week(weekday=1)),
  674. ("W-WED", Week(weekday=2)),
  675. ("W-THU", Week(weekday=3)),
  676. ("W-FRI", Week(weekday=4)),
  677. ]
  678. for name, expected in pairs:
  679. offset = _get_offset(name)
  680. assert offset == expected, (
  681. f"Expected {repr(name)} to yield {repr(expected)} "
  682. f"(actual: {repr(offset)})"
  683. )
  684. def test_get_offset_legacy():
  685. pairs = [("w@Sat", Week(weekday=5))]
  686. for name, expected in pairs:
  687. with pytest.raises(ValueError, match=INVALID_FREQ_ERR_MSG):
  688. _get_offset(name)
  689. class TestOffsetAliases:
  690. def setup_method(self):
  691. _offset_map.clear()
  692. def test_alias_equality(self):
  693. for k, v in _offset_map.items():
  694. if v is None:
  695. continue
  696. assert k == v.copy()
  697. def test_rule_code(self):
  698. lst = ["M", "MS", "BM", "BMS", "D", "B", "H", "T", "S", "L", "U"]
  699. for k in lst:
  700. assert k == _get_offset(k).rule_code
  701. # should be cached - this is kind of an internals test...
  702. assert k in _offset_map
  703. assert k == (_get_offset(k) * 3).rule_code
  704. suffix_lst = ["MON", "TUE", "WED", "THU", "FRI", "SAT", "SUN"]
  705. base = "W"
  706. for v in suffix_lst:
  707. alias = "-".join([base, v])
  708. assert alias == _get_offset(alias).rule_code
  709. assert alias == (_get_offset(alias) * 5).rule_code
  710. suffix_lst = [
  711. "JAN",
  712. "FEB",
  713. "MAR",
  714. "APR",
  715. "MAY",
  716. "JUN",
  717. "JUL",
  718. "AUG",
  719. "SEP",
  720. "OCT",
  721. "NOV",
  722. "DEC",
  723. ]
  724. base_lst = ["A", "AS", "BA", "BAS", "Q", "QS", "BQ", "BQS"]
  725. for base in base_lst:
  726. for v in suffix_lst:
  727. alias = "-".join([base, v])
  728. assert alias == _get_offset(alias).rule_code
  729. assert alias == (_get_offset(alias) * 5).rule_code
  730. def test_freq_offsets():
  731. off = BDay(1, offset=timedelta(0, 1800))
  732. assert off.freqstr == "B+30Min"
  733. off = BDay(1, offset=timedelta(0, -1800))
  734. assert off.freqstr == "B-30Min"
  735. class TestReprNames:
  736. def test_str_for_named_is_name(self):
  737. # look at all the amazing combinations!
  738. month_prefixes = ["A", "AS", "BA", "BAS", "Q", "BQ", "BQS", "QS"]
  739. names = [
  740. prefix + "-" + month
  741. for prefix in month_prefixes
  742. for month in [
  743. "JAN",
  744. "FEB",
  745. "MAR",
  746. "APR",
  747. "MAY",
  748. "JUN",
  749. "JUL",
  750. "AUG",
  751. "SEP",
  752. "OCT",
  753. "NOV",
  754. "DEC",
  755. ]
  756. ]
  757. days = ["MON", "TUE", "WED", "THU", "FRI", "SAT", "SUN"]
  758. names += ["W-" + day for day in days]
  759. names += ["WOM-" + week + day for week in ("1", "2", "3", "4") for day in days]
  760. _offset_map.clear()
  761. for name in names:
  762. offset = _get_offset(name)
  763. assert offset.freqstr == name
  764. # ---------------------------------------------------------------------
  765. def test_valid_default_arguments(offset_types):
  766. # GH#19142 check that the calling the constructors without passing
  767. # any keyword arguments produce valid offsets
  768. cls = offset_types
  769. cls()
  770. @pytest.mark.parametrize("kwd", sorted(liboffsets._relativedelta_kwds))
  771. def test_valid_month_attributes(kwd, month_classes):
  772. # GH#18226
  773. cls = month_classes
  774. # check that we cannot create e.g. MonthEnd(weeks=3)
  775. msg = rf"__init__\(\) got an unexpected keyword argument '{kwd}'"
  776. with pytest.raises(TypeError, match=msg):
  777. cls(**{kwd: 3})
  778. def test_month_offset_name(month_classes):
  779. # GH#33757 off.name with n != 1 should not raise AttributeError
  780. obj = month_classes(1)
  781. obj2 = month_classes(2)
  782. assert obj2.name == obj.name
  783. @pytest.mark.parametrize("kwd", sorted(liboffsets._relativedelta_kwds))
  784. def test_valid_relativedelta_kwargs(kwd, request):
  785. if kwd == "millisecond":
  786. request.node.add_marker(
  787. pytest.mark.xfail(
  788. raises=NotImplementedError,
  789. reason="Constructing DateOffset object with `millisecond` is not "
  790. "yet supported.",
  791. )
  792. )
  793. # Check that all the arguments specified in liboffsets._relativedelta_kwds
  794. # are in fact valid relativedelta keyword args
  795. DateOffset(**{kwd: 1})
  796. @pytest.mark.parametrize("kwd", sorted(liboffsets._relativedelta_kwds))
  797. def test_valid_tick_attributes(kwd, tick_classes):
  798. # GH#18226
  799. cls = tick_classes
  800. # check that we cannot create e.g. Hour(weeks=3)
  801. msg = rf"__init__\(\) got an unexpected keyword argument '{kwd}'"
  802. with pytest.raises(TypeError, match=msg):
  803. cls(**{kwd: 3})
  804. def test_validate_n_error():
  805. with pytest.raises(TypeError, match="argument must be an integer"):
  806. DateOffset(n="Doh!")
  807. with pytest.raises(TypeError, match="argument must be an integer"):
  808. MonthBegin(n=timedelta(1))
  809. with pytest.raises(TypeError, match="argument must be an integer"):
  810. BDay(n=np.array([1, 2], dtype=np.int64))
  811. def test_require_integers(offset_types):
  812. cls = offset_types
  813. with pytest.raises(ValueError, match="argument must be an integer"):
  814. cls(n=1.5)
  815. def test_tick_normalize_raises(tick_classes):
  816. # check that trying to create a Tick object with normalize=True raises
  817. # GH#21427
  818. cls = tick_classes
  819. msg = "Tick offset with `normalize=True` are not allowed."
  820. with pytest.raises(ValueError, match=msg):
  821. cls(n=3, normalize=True)
  822. @pytest.mark.parametrize(
  823. "offset_kwargs, expected_arg",
  824. [
  825. ({"nanoseconds": 1}, "1970-01-01 00:00:00.000000001"),
  826. ({"nanoseconds": 5}, "1970-01-01 00:00:00.000000005"),
  827. ({"nanoseconds": -1}, "1969-12-31 23:59:59.999999999"),
  828. ({"microseconds": 1}, "1970-01-01 00:00:00.000001"),
  829. ({"microseconds": -1}, "1969-12-31 23:59:59.999999"),
  830. ({"seconds": 1}, "1970-01-01 00:00:01"),
  831. ({"seconds": -1}, "1969-12-31 23:59:59"),
  832. ({"minutes": 1}, "1970-01-01 00:01:00"),
  833. ({"minutes": -1}, "1969-12-31 23:59:00"),
  834. ({"hours": 1}, "1970-01-01 01:00:00"),
  835. ({"hours": -1}, "1969-12-31 23:00:00"),
  836. ({"days": 1}, "1970-01-02 00:00:00"),
  837. ({"days": -1}, "1969-12-31 00:00:00"),
  838. ({"weeks": 1}, "1970-01-08 00:00:00"),
  839. ({"weeks": -1}, "1969-12-25 00:00:00"),
  840. ({"months": 1}, "1970-02-01 00:00:00"),
  841. ({"months": -1}, "1969-12-01 00:00:00"),
  842. ({"years": 1}, "1971-01-01 00:00:00"),
  843. ({"years": -1}, "1969-01-01 00:00:00"),
  844. ],
  845. )
  846. def test_dateoffset_add_sub(offset_kwargs, expected_arg):
  847. offset = DateOffset(**offset_kwargs)
  848. ts = Timestamp(0)
  849. result = ts + offset
  850. expected = Timestamp(expected_arg)
  851. assert result == expected
  852. result -= offset
  853. assert result == ts
  854. result = offset + ts
  855. assert result == expected
  856. def test_dateoffset_add_sub_timestamp_with_nano():
  857. offset = DateOffset(minutes=2, nanoseconds=9)
  858. ts = Timestamp(4)
  859. result = ts + offset
  860. expected = Timestamp("1970-01-01 00:02:00.000000013")
  861. assert result == expected
  862. result -= offset
  863. assert result == ts
  864. result = offset + ts
  865. assert result == expected
  866. @pytest.mark.parametrize(
  867. "attribute",
  868. [
  869. "hours",
  870. "days",
  871. "weeks",
  872. "months",
  873. "years",
  874. ],
  875. )
  876. def test_dateoffset_immutable(attribute):
  877. offset = DateOffset(**{attribute: 0})
  878. msg = "DateOffset objects are immutable"
  879. with pytest.raises(AttributeError, match=msg):
  880. setattr(offset, attribute, 5)
  881. def test_dateoffset_misc():
  882. oset = offsets.DateOffset(months=2, days=4)
  883. # it works
  884. oset.freqstr
  885. assert not offsets.DateOffset(months=2) == 2
  886. @pytest.mark.parametrize("n", [-1, 1, 3])
  887. def test_construct_int_arg_no_kwargs_assumed_days(n):
  888. # GH 45890, 45643
  889. offset = DateOffset(n)
  890. assert offset._offset == timedelta(1)
  891. result = Timestamp(2022, 1, 2) + offset
  892. expected = Timestamp(2022, 1, 2 + n)
  893. assert result == expected
  894. @pytest.mark.parametrize(
  895. "offset, expected",
  896. [
  897. (
  898. DateOffset(minutes=7, nanoseconds=18),
  899. Timestamp("2022-01-01 00:07:00.000000018"),
  900. ),
  901. (DateOffset(nanoseconds=3), Timestamp("2022-01-01 00:00:00.000000003")),
  902. ],
  903. )
  904. def test_dateoffset_add_sub_timestamp_series_with_nano(offset, expected):
  905. # GH 47856
  906. start_time = Timestamp("2022-01-01")
  907. teststamp = start_time
  908. testseries = Series([start_time])
  909. testseries = testseries + offset
  910. assert testseries[0] == expected
  911. testseries -= offset
  912. assert testseries[0] == teststamp
  913. testseries = offset + testseries
  914. assert testseries[0] == expected